mirror of
https://git.proxmox.com/git/fwupd
synced 2025-08-13 21:09:47 +00:00
redfish: Implement updates for Supermicro machines
This commit is contained in:
parent
f579c03223
commit
6054d09f35
@ -360,6 +360,8 @@ fwupd_device_problem_to_string(FwupdDeviceProblem device_problem)
|
||||
return "lid-is-closed";
|
||||
if (device_problem == FWUPD_DEVICE_PROBLEM_IS_EMULATED)
|
||||
return "is-emulated";
|
||||
if (device_problem == FWUPD_DEVICE_PROBLEM_MISSING_LICENSE)
|
||||
return "missing-license";
|
||||
if (device_problem == FWUPD_DEVICE_PROBLEM_UNKNOWN)
|
||||
return "unknown";
|
||||
return NULL;
|
||||
@ -394,6 +396,8 @@ fwupd_device_problem_from_string(const gchar *device_problem)
|
||||
return FWUPD_DEVICE_PROBLEM_LID_IS_CLOSED;
|
||||
if (g_strcmp0(device_problem, "is-emulated") == 0)
|
||||
return FWUPD_DEVICE_PROBLEM_IS_EMULATED;
|
||||
if (g_strcmp0(device_problem, "missing-license") == 0)
|
||||
return FWUPD_DEVICE_PROBLEM_MISSING_LICENSE;
|
||||
return FWUPD_DEVICE_PROBLEM_UNKNOWN;
|
||||
}
|
||||
|
||||
|
@ -607,6 +607,14 @@ typedef guint64 FwupdDeviceFlags;
|
||||
* Since 1.8.3
|
||||
*/
|
||||
#define FWUPD_DEVICE_PROBLEM_IS_EMULATED (1u << 6)
|
||||
/**
|
||||
* FWUPD_DEVICE_PROBLEM_MISSING_LICENSE:
|
||||
*
|
||||
* The device cannot be updated due to missing vendor's license.
|
||||
*
|
||||
* Since 1.8.6
|
||||
*/
|
||||
#define FWUPD_DEVICE_PROBLEM_MISSING_LICENSE (1u << 7)
|
||||
/**
|
||||
* FWUPD_DEVICE_PROBLEM_UNKNOWN:
|
||||
*
|
||||
|
@ -2827,6 +2827,8 @@ fu_device_problem_to_inhibit_reason(FuDevice *self, guint64 device_problem)
|
||||
return g_strdup("Device cannot be used while the lid is closed");
|
||||
if (device_problem == FWUPD_DEVICE_PROBLEM_IS_EMULATED)
|
||||
return g_strdup("Device is emulated");
|
||||
if (device_problem == FWUPD_DEVICE_PROBLEM_MISSING_LICENSE)
|
||||
return g_strdup("Device does not have the necessary license installed");
|
||||
if (device_problem == FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW) {
|
||||
if (priv->ctx == NULL)
|
||||
return g_strdup("System power is too low to perform the update");
|
||||
|
@ -16,6 +16,7 @@
|
||||
#include "fu-redfish-multipart-device.h"
|
||||
#include "fu-redfish-request.h"
|
||||
#include "fu-redfish-smbios.h"
|
||||
#include "fu-redfish-smc-device.h"
|
||||
|
||||
struct _FuRedfishBackend {
|
||||
FuBackend parent_instance;
|
||||
@ -221,6 +222,24 @@ fu_redfish_backend_set_push_uri_path(FuRedfishBackend *self, const gchar *push_u
|
||||
self->push_uri_path = g_strdup(push_uri_path);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
fu_redfish_backend_has_smc_update_path(JsonObject *update_svc)
|
||||
{
|
||||
JsonObject *tmp_obj;
|
||||
const gchar *tmp_str;
|
||||
|
||||
if (!json_object_has_member(update_svc, "Actions"))
|
||||
return FALSE;
|
||||
tmp_obj = json_object_get_object_member(update_svc, "Actions");
|
||||
if (tmp_obj == NULL || !json_object_has_member(tmp_obj, "#UpdateService.StartUpdate"))
|
||||
return FALSE;
|
||||
tmp_obj = json_object_get_object_member(tmp_obj, "#UpdateService.StartUpdate");
|
||||
if (tmp_obj == NULL || !json_object_has_member(tmp_obj, "target"))
|
||||
return FALSE;
|
||||
tmp_str = json_object_get_string_member(tmp_obj, "target");
|
||||
return g_str_equal(tmp_str, "/redfish/v1/UpdateService/Actions/UpdateService.StartUpdate");
|
||||
}
|
||||
|
||||
static gboolean
|
||||
fu_redfish_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **error)
|
||||
{
|
||||
@ -250,10 +269,15 @@ fu_redfish_backend_coldplug(FuBackend *backend, FuProgress *progress, GError **e
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
if (json_object_has_member(json_obj, "MultipartHttpPushUri")) {
|
||||
if (self->push_uri_path == NULL &&
|
||||
json_object_has_member(json_obj, "MultipartHttpPushUri")) {
|
||||
const gchar *tmp = json_object_get_string_member(json_obj, "MultipartHttpPushUri");
|
||||
if (tmp != NULL) {
|
||||
self->device_gtype = FU_TYPE_REDFISH_MULTIPART_DEVICE;
|
||||
if (fu_redfish_backend_has_smc_update_path(json_obj)) {
|
||||
self->device_gtype = FU_TYPE_REDFISH_SMC_DEVICE;
|
||||
} else {
|
||||
self->device_gtype = FU_TYPE_REDFISH_MULTIPART_DEVICE;
|
||||
}
|
||||
fu_redfish_backend_set_push_uri_path(self, tmp);
|
||||
}
|
||||
}
|
||||
|
@ -360,6 +360,26 @@ fu_redfish_device_set_vendor(FuRedfishDevice *self, const gchar *vendor)
|
||||
fu_device_add_vendor_id(FU_DEVICE(self), vendor_id);
|
||||
}
|
||||
|
||||
static void
|
||||
fu_redfish_backend_smc_license_check(FuDevice *device)
|
||||
{
|
||||
FuRedfishDevice *self = FU_REDFISH_DEVICE(device);
|
||||
FuRedfishBackend *backend = fu_redfish_device_get_backend(self);
|
||||
g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(backend);
|
||||
g_autoptr(GError) error_local = NULL;
|
||||
|
||||
/* see if we don't get an license error */
|
||||
if (!fu_redfish_request_perform(request,
|
||||
fu_redfish_backend_get_push_uri_path(backend),
|
||||
FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON,
|
||||
&error_local)) {
|
||||
if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED))
|
||||
fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_MISSING_LICENSE);
|
||||
else
|
||||
g_debug("supermicro license check returned %s\n", error_local->message);
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
fu_redfish_device_probe(FuDevice *dev, GError **error)
|
||||
{
|
||||
@ -504,6 +524,10 @@ fu_redfish_device_probe(FuDevice *dev, GError **error)
|
||||
}
|
||||
}
|
||||
|
||||
/* for Supermicro check whether we have a proper Redfish license installed */
|
||||
if (g_strcmp0("SMCI", fu_device_get_vendor(dev)) == 0)
|
||||
fu_redfish_backend_smc_license_check(dev);
|
||||
|
||||
/* success */
|
||||
return TRUE;
|
||||
}
|
||||
|
@ -126,6 +126,15 @@ fu_redfish_plugin_coldplug(FuPlugin *plugin, FuProgress *progress, GError **erro
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void
|
||||
fu_redfish_plugin_set_credentials(FuPlugin *plugin, const gchar *username, const gchar *password)
|
||||
{
|
||||
FuRedfishPlugin *self = FU_REDFISH_PLUGIN(plugin);
|
||||
|
||||
fu_redfish_backend_set_username(self->backend, username);
|
||||
fu_redfish_backend_set_password(self->backend, password);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
fu_redfish_plugin_discover_uefi_credentials(FuPlugin *plugin, GError **error)
|
||||
{
|
||||
|
@ -9,3 +9,6 @@
|
||||
#include <fwupdplugin.h>
|
||||
|
||||
G_DECLARE_FINAL_TYPE(FuRedfishPlugin, fu_redfish_plugin, FU, REDFISH_PLUGIN, FuPlugin)
|
||||
|
||||
void
|
||||
fu_redfish_plugin_set_credentials(FuPlugin *plugin, const gchar *username, const gchar *password);
|
||||
|
@ -140,6 +140,12 @@ fu_redfish_request_load_json(FuRedfishRequest *self, GByteArray *buf, GError **e
|
||||
error_code = FWUPD_ERROR_AUTH_FAILED;
|
||||
else if (g_strcmp0(id, "Base.1.8.PasswordChangeRequired") == 0)
|
||||
error_code = FWUPD_ERROR_AUTH_EXPIRED;
|
||||
else if (g_strcmp0(id, "SMC.1.0.OemLicenseNotPassed") == 0)
|
||||
error_code = FWUPD_ERROR_NOT_SUPPORTED;
|
||||
else if (g_strcmp0(id, "SMC.1.0.OemFirmwareAlreadyInUpdateMode") == 0)
|
||||
error_code = FWUPD_ERROR_ALREADY_PENDING;
|
||||
else if (g_strcmp0(id, "SMC.1.0.OemBiosUpdateIsInProgress") == 0)
|
||||
error_code = FWUPD_ERROR_ALREADY_PENDING;
|
||||
g_set_error_literal(error, FWUPD_ERROR, error_code, msg);
|
||||
return FALSE;
|
||||
}
|
||||
|
242
plugins/redfish/fu-redfish-smc-device.c
Normal file
242
plugins/redfish/fu-redfish-smc-device.c
Normal file
@ -0,0 +1,242 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1+
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "fu-redfish-backend.h"
|
||||
#include "fu-redfish-common.h"
|
||||
#include "fu-redfish-smc-device.h"
|
||||
|
||||
struct _FuRedfishSmcDevice {
|
||||
FuRedfishDevice parent_instance;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(FuRedfishSmcDevice, fu_redfish_smc_device, FU_TYPE_REDFISH_DEVICE)
|
||||
|
||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC(curl_mime, curl_mime_free)
|
||||
|
||||
static const gchar *
|
||||
fu_redfish_smc_device_get_task(JsonObject *json_obj)
|
||||
{
|
||||
JsonObject *tmp_obj;
|
||||
JsonArray *tmp_ary;
|
||||
const gchar *msgid;
|
||||
|
||||
if (!json_object_has_member(json_obj, "Accepted"))
|
||||
return NULL;
|
||||
tmp_obj = json_object_get_object_member(json_obj, "Accepted");
|
||||
if (tmp_obj == NULL || !json_object_has_member(tmp_obj, "@Message.ExtendedInfo"))
|
||||
return NULL;
|
||||
tmp_ary = json_object_get_array_member(tmp_obj, "@Message.ExtendedInfo");
|
||||
if (tmp_ary == NULL || json_array_get_length(tmp_ary) != 1)
|
||||
return NULL;
|
||||
tmp_obj = json_array_get_object_element(tmp_ary, 0);
|
||||
if (tmp_obj == NULL)
|
||||
return NULL;
|
||||
msgid = json_object_get_string_member(tmp_obj, "MessageId");
|
||||
if (g_strcmp0(msgid, "SMC.1.0.OemSimpleupdateAcceptedMessage") != 0)
|
||||
return NULL;
|
||||
tmp_ary = json_object_get_array_member(tmp_obj, "MessageArgs");
|
||||
if (tmp_ary == NULL)
|
||||
return NULL;
|
||||
if (json_array_get_length(tmp_ary) != 1)
|
||||
return NULL;
|
||||
return json_array_get_string_element(tmp_ary, 0);
|
||||
}
|
||||
|
||||
static GString *
|
||||
fu_redfish_smc_device_get_parameters(FuRedfishSmcDevice *self)
|
||||
{
|
||||
g_autoptr(GString) str = g_string_new(NULL);
|
||||
g_autoptr(JsonBuilder) builder = json_builder_new();
|
||||
g_autoptr(JsonGenerator) json_generator = json_generator_new();
|
||||
g_autoptr(JsonNode) json_root = NULL;
|
||||
|
||||
/* create header */
|
||||
/* https://supermicro.com/manuals/other/RedishRefGuide.pdf */
|
||||
json_builder_begin_object(builder);
|
||||
json_builder_set_member_name(builder, "Targets");
|
||||
json_builder_begin_array(builder);
|
||||
json_builder_add_string_value(builder, "/redfish/v1/Systems/1/Bios");
|
||||
json_builder_end_array(builder);
|
||||
json_builder_set_member_name(builder, "@Redfish.OperationApplyTime");
|
||||
json_builder_add_string_value(builder, "OnStartUpdateRequest");
|
||||
json_builder_set_member_name(builder, "Oem");
|
||||
json_builder_begin_object(builder);
|
||||
json_builder_set_member_name(builder, "Supermicro");
|
||||
json_builder_begin_object(builder);
|
||||
json_builder_set_member_name(builder, "BIOS");
|
||||
json_builder_begin_object(builder);
|
||||
json_builder_set_member_name(builder, "PreserveME");
|
||||
json_builder_add_boolean_value(builder, TRUE);
|
||||
json_builder_set_member_name(builder, "PreserveNVRAM");
|
||||
json_builder_add_boolean_value(builder, TRUE);
|
||||
json_builder_set_member_name(builder, "PreserveSMBIOS");
|
||||
json_builder_add_boolean_value(builder, TRUE);
|
||||
json_builder_set_member_name(builder, "BackupBIOS");
|
||||
json_builder_add_boolean_value(builder, FALSE);
|
||||
json_builder_end_object(builder);
|
||||
json_builder_end_object(builder);
|
||||
json_builder_end_object(builder);
|
||||
json_builder_end_object(builder);
|
||||
|
||||
/* export as a string */
|
||||
json_root = json_builder_get_root(builder);
|
||||
json_generator_set_pretty(json_generator, TRUE);
|
||||
json_generator_set_root(json_generator, json_root);
|
||||
json_generator_to_gstring(json_generator, str);
|
||||
|
||||
return g_steal_pointer(&str);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
fu_redfish_smc_device_start_update(FuDevice *device, FuProgress *progress, GError **error)
|
||||
{
|
||||
FuRedfishBackend *backend = fu_redfish_device_get_backend(FU_REDFISH_DEVICE(device));
|
||||
JsonObject *json_obj;
|
||||
CURL *curl;
|
||||
const gchar *location = NULL;
|
||||
g_autoptr(FuRedfishRequest) request = fu_redfish_backend_request_new(backend);
|
||||
|
||||
curl = fu_redfish_request_get_curl(request);
|
||||
(void)curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "");
|
||||
|
||||
if (!fu_redfish_request_perform(
|
||||
request,
|
||||
"/redfish/v1/UpdateService/Actions/UpdateService.StartUpdate",
|
||||
FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON,
|
||||
error)) {
|
||||
if (g_error_matches(*error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) {
|
||||
fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_UPDATE_PENDING);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
json_obj = fu_redfish_request_get_json_object(request);
|
||||
|
||||
location = fu_redfish_smc_device_get_task(json_obj);
|
||||
if (location == NULL) {
|
||||
g_set_error(error,
|
||||
FWUPD_ERROR,
|
||||
FWUPD_ERROR_INVALID_FILE,
|
||||
"no task returned for %s",
|
||||
fu_redfish_backend_get_push_uri_path(backend));
|
||||
return FALSE;
|
||||
}
|
||||
return fu_redfish_device_poll_task(FU_REDFISH_DEVICE(device), location, progress, error);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
fu_redfish_smc_device_write_firmware(FuDevice *device,
|
||||
FuFirmware *firmware,
|
||||
FuProgress *progress,
|
||||
FwupdInstallFlags flags,
|
||||
GError **error)
|
||||
{
|
||||
FuRedfishSmcDevice *self = FU_REDFISH_SMC_DEVICE(device);
|
||||
FuRedfishBackend *backend = fu_redfish_device_get_backend(FU_REDFISH_DEVICE(self));
|
||||
CURL *curl;
|
||||
JsonObject *json_obj;
|
||||
curl_mimepart *part;
|
||||
const gchar *location = NULL;
|
||||
gboolean ret;
|
||||
g_autoptr(curl_mime) mime = NULL;
|
||||
g_autoptr(FuRedfishRequest) request = NULL;
|
||||
g_autoptr(GBytes) fw = NULL;
|
||||
g_autoptr(GString) params = NULL;
|
||||
|
||||
fu_progress_set_id(progress, G_STRLOC);
|
||||
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 50, "write");
|
||||
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 50, "apply");
|
||||
|
||||
/* get default image */
|
||||
fw = fu_firmware_get_bytes(firmware, error);
|
||||
if (fw == NULL)
|
||||
return FALSE;
|
||||
|
||||
/* create the multipart for uploading the image request */
|
||||
request = fu_redfish_backend_request_new(backend);
|
||||
curl = fu_redfish_request_get_curl(request);
|
||||
mime = curl_mime_init(curl);
|
||||
(void)curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
|
||||
|
||||
params = fu_redfish_smc_device_get_parameters(self);
|
||||
part = curl_mime_addpart(mime);
|
||||
curl_mime_name(part, "UpdateParameters");
|
||||
(void)curl_mime_type(part, "application/json");
|
||||
(void)curl_mime_data(part, params->str, CURL_ZERO_TERMINATED);
|
||||
if (g_getenv("FWUPD_REDFISH_VERBOSE") != NULL)
|
||||
g_debug("request: %s", params->str);
|
||||
|
||||
part = curl_mime_addpart(mime);
|
||||
curl_mime_name(part, "UpdateFile");
|
||||
(void)curl_mime_type(part, "application/octet-stream");
|
||||
(void)curl_mime_filedata(part, "firmware.bin");
|
||||
(void)curl_mime_data(part, g_bytes_get_data(fw, NULL), g_bytes_get_size(fw));
|
||||
|
||||
fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
|
||||
if (!fu_redfish_request_perform(request,
|
||||
fu_redfish_backend_get_push_uri_path(backend),
|
||||
FU_REDFISH_REQUEST_PERFORM_FLAG_LOAD_JSON,
|
||||
error)) {
|
||||
if (g_error_matches(*error, FWUPD_ERROR, FWUPD_ERROR_ALREADY_PENDING)) {
|
||||
fu_device_add_problem(device, FWUPD_DEVICE_PROBLEM_UPDATE_PENDING);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
if (fu_redfish_request_get_status_code(request) != 202) {
|
||||
g_set_error(error,
|
||||
FWUPD_ERROR,
|
||||
FWUPD_ERROR_INVALID_FILE,
|
||||
"failed to upload: %li",
|
||||
fu_redfish_request_get_status_code(request));
|
||||
return FALSE;
|
||||
}
|
||||
json_obj = fu_redfish_request_get_json_object(request);
|
||||
|
||||
/* poll the verify task for progress */
|
||||
location = fu_redfish_smc_device_get_task(json_obj);
|
||||
if (location == NULL) {
|
||||
g_set_error(error,
|
||||
FWUPD_ERROR,
|
||||
FWUPD_ERROR_INVALID_FILE,
|
||||
"no task returned for %s",
|
||||
fu_redfish_backend_get_push_uri_path(backend));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!fu_redfish_device_poll_task(FU_REDFISH_DEVICE(self),
|
||||
location,
|
||||
fu_progress_get_child(progress),
|
||||
error))
|
||||
return FALSE;
|
||||
fu_progress_step_done(progress);
|
||||
|
||||
ret = fu_redfish_smc_device_start_update(device, fu_progress_get_child(progress), error);
|
||||
fu_progress_step_done(progress);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
fu_redfish_smc_device_set_progress(FuDevice *self, FuProgress *progress)
|
||||
{
|
||||
fu_progress_set_id(progress, G_STRLOC);
|
||||
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "detach");
|
||||
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 98, "write");
|
||||
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 1, "attach");
|
||||
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 0, "reload");
|
||||
}
|
||||
|
||||
static void
|
||||
fu_redfish_smc_device_init(FuRedfishSmcDevice *self)
|
||||
{
|
||||
fu_device_set_summary(FU_DEVICE(self), "Redfish Supermicro device");
|
||||
}
|
||||
|
||||
static void
|
||||
fu_redfish_smc_device_class_init(FuRedfishSmcDeviceClass *klass)
|
||||
{
|
||||
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
|
||||
klass_device->write_firmware = fu_redfish_smc_device_write_firmware;
|
||||
klass_device->set_progress = fu_redfish_smc_device_set_progress;
|
||||
}
|
21
plugins/redfish/fu-redfish-smc-device.h
Normal file
21
plugins/redfish/fu-redfish-smc-device.h
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1+
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <fwupdplugin.h>
|
||||
|
||||
#include "fu-redfish-backend.h"
|
||||
#include "fu-redfish-device.h"
|
||||
|
||||
#define FU_TYPE_REDFISH_SMC_DEVICE (fu_redfish_smc_device_get_type())
|
||||
G_DECLARE_FINAL_TYPE(FuRedfishSmcDevice,
|
||||
fu_redfish_smc_device,
|
||||
FU,
|
||||
REDFISH_SMC_DEVICE,
|
||||
FuRedfishDevice)
|
||||
|
||||
struct _FuRedfishSmcDeviceClass {
|
||||
FuRedfishDeviceClass parent_class;
|
||||
};
|
@ -17,9 +17,12 @@
|
||||
#include "fu-redfish-common.h"
|
||||
#include "fu-redfish-network.h"
|
||||
#include "fu-redfish-plugin.h"
|
||||
#include "fu-redfish-smc-device.h"
|
||||
|
||||
typedef struct {
|
||||
FuPlugin *plugin;
|
||||
FuPlugin *smc_plugin;
|
||||
FuPlugin *unlicensed_plugin;
|
||||
} FuTest;
|
||||
|
||||
static void
|
||||
@ -36,17 +39,50 @@ fu_test_self_init(FuTest *self)
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(ret);
|
||||
|
||||
/* generic BMC */
|
||||
self->plugin = fu_plugin_new_from_gtype(fu_redfish_plugin_get_type(), ctx);
|
||||
ret = fu_plugin_runner_startup(self->plugin, progress, &error);
|
||||
if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) {
|
||||
g_test_skip("no redfish.py running");
|
||||
return;
|
||||
g_clear_error(&error);
|
||||
} else {
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(ret);
|
||||
ret = fu_plugin_runner_coldplug(self->plugin, progress, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(ret);
|
||||
}
|
||||
|
||||
/* supermicro BMC */
|
||||
self->smc_plugin = fu_plugin_new_from_gtype(fu_redfish_plugin_get_type(), ctx);
|
||||
ret = fu_plugin_runner_startup(self->smc_plugin, progress, &error);
|
||||
if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) {
|
||||
g_test_skip("no redfish.py running");
|
||||
g_clear_error(&error);
|
||||
} else {
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(ret);
|
||||
fu_redfish_plugin_set_credentials(self->smc_plugin, "smc_username", "password2");
|
||||
ret = fu_plugin_runner_coldplug(self->smc_plugin, progress, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(ret);
|
||||
}
|
||||
|
||||
/* unlicensed supermicro BMC */
|
||||
self->unlicensed_plugin = fu_plugin_new_from_gtype(fu_redfish_plugin_get_type(), ctx);
|
||||
ret = fu_plugin_runner_startup(self->unlicensed_plugin, progress, &error);
|
||||
if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) {
|
||||
g_test_skip("no redfish.py running");
|
||||
} else {
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(ret);
|
||||
fu_redfish_plugin_set_credentials(self->unlicensed_plugin,
|
||||
"unlicensed_username",
|
||||
"password2");
|
||||
ret = fu_plugin_runner_coldplug(self->unlicensed_plugin, progress, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(ret);
|
||||
}
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(ret);
|
||||
ret = fu_plugin_runner_coldplug(self->plugin, progress, &error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -288,6 +324,56 @@ fu_test_redfish_devices_func(gconstpointer user_data)
|
||||
g_assert_true(fu_device_has_vendor_id(dev, "REDFISH:CONTOSO"));
|
||||
}
|
||||
|
||||
static void
|
||||
fu_test_redfish_unlicensed_devices_func(gconstpointer user_data)
|
||||
{
|
||||
FuDevice *dev;
|
||||
FuTest *self = (FuTest *)user_data;
|
||||
GPtrArray *devices;
|
||||
|
||||
devices = fu_plugin_get_devices(self->unlicensed_plugin);
|
||||
g_assert_nonnull(devices);
|
||||
if (devices->len == 0) {
|
||||
g_test_skip("no redfish support");
|
||||
return;
|
||||
}
|
||||
g_assert_cmpint(devices->len, ==, 2);
|
||||
|
||||
dev = g_ptr_array_index(devices, 0);
|
||||
g_assert_true(FU_IS_REDFISH_SMC_DEVICE(dev));
|
||||
g_assert_true(fu_device_has_inhibit(
|
||||
dev,
|
||||
fwupd_device_problem_to_string(FWUPD_DEVICE_PROBLEM_MISSING_LICENSE)));
|
||||
|
||||
dev = g_ptr_array_index(devices, 1);
|
||||
g_assert_true(FU_IS_REDFISH_SMC_DEVICE(dev));
|
||||
g_assert_true(fu_device_has_inhibit(
|
||||
dev,
|
||||
fwupd_device_problem_to_string(FWUPD_DEVICE_PROBLEM_MISSING_LICENSE)));
|
||||
}
|
||||
|
||||
static void
|
||||
fu_test_redfish_smc_devices_func(gconstpointer user_data)
|
||||
{
|
||||
FuDevice *dev;
|
||||
FuTest *self = (FuTest *)user_data;
|
||||
GPtrArray *devices;
|
||||
|
||||
devices = fu_plugin_get_devices(self->smc_plugin);
|
||||
g_assert_nonnull(devices);
|
||||
if (devices->len == 0) {
|
||||
g_test_skip("no redfish support");
|
||||
return;
|
||||
}
|
||||
g_assert_cmpint(devices->len, ==, 2);
|
||||
|
||||
dev = g_ptr_array_index(devices, 0);
|
||||
g_assert_true(FU_IS_REDFISH_SMC_DEVICE(dev));
|
||||
|
||||
dev = g_ptr_array_index(devices, 1);
|
||||
g_assert_true(FU_IS_REDFISH_SMC_DEVICE(dev));
|
||||
}
|
||||
|
||||
static void
|
||||
fu_test_redfish_update_func(gconstpointer user_data)
|
||||
{
|
||||
@ -331,6 +417,51 @@ fu_test_redfish_update_func(gconstpointer user_data)
|
||||
g_assert_false(ret);
|
||||
}
|
||||
|
||||
static void
|
||||
fu_test_redfish_smc_update_func(gconstpointer user_data)
|
||||
{
|
||||
FuDevice *dev;
|
||||
FuTest *self = (FuTest *)user_data;
|
||||
GPtrArray *devices;
|
||||
gboolean ret;
|
||||
g_autoptr(GError) error = NULL;
|
||||
g_autoptr(GBytes) blob_fw = NULL;
|
||||
g_autoptr(FuProgress) progress = fu_progress_new(G_STRLOC);
|
||||
|
||||
devices = fu_plugin_get_devices(self->smc_plugin);
|
||||
g_assert_nonnull(devices);
|
||||
if (devices->len == 0) {
|
||||
g_test_skip("no redfish support");
|
||||
return;
|
||||
}
|
||||
g_assert_cmpint(devices->len, ==, 2);
|
||||
|
||||
/* BMC */
|
||||
dev = g_ptr_array_index(devices, 1);
|
||||
blob_fw = g_bytes_new_static("hello", 5);
|
||||
ret = fu_plugin_runner_write_firmware(self->plugin,
|
||||
dev,
|
||||
blob_fw,
|
||||
progress,
|
||||
FWUPD_INSTALL_FLAG_NO_SEARCH,
|
||||
&error);
|
||||
g_assert_no_error(error);
|
||||
g_assert_true(ret);
|
||||
|
||||
/* stuck update */
|
||||
blob_fw = g_bytes_new_static("stuck", 5);
|
||||
ret = fu_plugin_runner_write_firmware(self->plugin,
|
||||
dev,
|
||||
blob_fw,
|
||||
progress,
|
||||
FWUPD_INSTALL_FLAG_NO_SEARCH,
|
||||
&error);
|
||||
g_assert_false(ret);
|
||||
g_assert_true(fu_device_has_inhibit(
|
||||
dev,
|
||||
fwupd_device_problem_to_string(FWUPD_DEVICE_PROBLEM_UPDATE_PENDING)));
|
||||
}
|
||||
|
||||
static void
|
||||
fu_test_self_free(FuTest *self)
|
||||
{
|
||||
@ -370,6 +501,13 @@ main(int argc, char **argv)
|
||||
g_test_add_func("/redfish/common{lenovo}", fu_test_redfish_common_lenovo_func);
|
||||
g_test_add_func("/redfish/network{mac_addr}", fu_test_redfish_network_mac_addr_func);
|
||||
g_test_add_func("/redfish/network{vid_pid}", fu_test_redfish_network_vid_pid_func);
|
||||
g_test_add_data_func("/redfish/unlicensed_plugin{devices}",
|
||||
self,
|
||||
fu_test_redfish_unlicensed_devices_func);
|
||||
g_test_add_data_func("/redfish/smc_plugin{devices}",
|
||||
self,
|
||||
fu_test_redfish_smc_devices_func);
|
||||
g_test_add_data_func("/redfish/smc_plugin{update}", self, fu_test_redfish_smc_update_func);
|
||||
g_test_add_data_func("/redfish/plugin{devices}", self, fu_test_redfish_devices_func);
|
||||
g_test_add_data_func("/redfish/plugin{update}", self, fu_test_redfish_update_func);
|
||||
return g_test_run();
|
||||
|
@ -15,6 +15,7 @@ plugin_builtin_redfish = static_library('fu_plugin_redfish',
|
||||
'fu-redfish-backend.c',
|
||||
'fu-redfish-common.c', # fuzzing
|
||||
'fu-redfish-device.c',
|
||||
'fu-redfish-smc-device.c',
|
||||
'fu-redfish-legacy-device.c',
|
||||
'fu-redfish-multipart-device.c',
|
||||
'fu-redfish-network.c',
|
||||
|
@ -19,3 +19,6 @@ ParentGuid = REDFISH\VENDOR_Lenovo&ID_LXPM
|
||||
[REDFISH\VENDOR_Lenovo&ID_LXPMWindowsDriver]
|
||||
Flags = ~updatable
|
||||
ParentGuid = REDFISH\VENDOR_Lenovo&ID_LXPM
|
||||
|
||||
[REDFISH\VENDOR_SMCI&ID_Backup_BIOS]
|
||||
Flags = is-backup,no-probe
|
||||
|
@ -11,10 +11,13 @@ from flask import Flask, Response, request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
HARDCODED_USERNAME = "username2"
|
||||
HARDCODED_SMC_USERNAME = "smc_username"
|
||||
HARDCODED_UNL_USERNAME = "unlicensed_username"
|
||||
HARDCODED_USERNAMES = {"username2", HARDCODED_SMC_USERNAME, HARDCODED_UNL_USERNAME}
|
||||
HARDCODED_PASSWORD = "password2"
|
||||
|
||||
app._percentage: int = 0
|
||||
app._percentage545: int = 0
|
||||
app._percentage546: int = 0
|
||||
|
||||
|
||||
def _failure(msg: str, status=400):
|
||||
@ -28,12 +31,13 @@ def _failure(msg: str, status=400):
|
||||
def index():
|
||||
|
||||
# reset counter
|
||||
app._percentage = 0
|
||||
app._percentage545 = 0
|
||||
app._percentage546 = 0
|
||||
|
||||
# check password from the config file
|
||||
try:
|
||||
if (
|
||||
request.authorization["username"] != HARDCODED_USERNAME
|
||||
not request.authorization["username"] in HARDCODED_USERNAMES
|
||||
or request.authorization["password"] != HARDCODED_PASSWORD
|
||||
):
|
||||
return _failure("unauthorised", status=401)
|
||||
@ -58,7 +62,6 @@ def update_service():
|
||||
"FirmwareInventory": {
|
||||
"@odata.id": "/redfish/v1/UpdateService/FirmwareInventory"
|
||||
},
|
||||
"MultipartHttpPushUri": "/FWUpdate",
|
||||
"HttpPushUri": "/FWUpdate",
|
||||
"HttpPushUriOptions": {
|
||||
"HttpPushUriApplyTime": {
|
||||
@ -68,6 +71,24 @@ def update_service():
|
||||
"HttpPushUriOptionsBusy": False,
|
||||
"ServiceEnabled": True,
|
||||
}
|
||||
|
||||
if request.authorization["username"] == HARDCODED_UNL_USERNAME:
|
||||
res["MultipartHttpPushUri"] = "/FWUpdate-unlicensed"
|
||||
res["Actions"] = {
|
||||
"#UpdateService.StartUpdate": {
|
||||
"target": "/redfish/v1/UpdateService/Actions/UpdateService.StartUpdate"
|
||||
}
|
||||
}
|
||||
elif request.authorization["username"] == HARDCODED_SMC_USERNAME:
|
||||
res["MultipartHttpPushUri"] = "/FWUpdate-smc"
|
||||
res["Actions"] = {
|
||||
"#UpdateService.StartUpdate": {
|
||||
"target": "/redfish/v1/UpdateService/Actions/UpdateService.StartUpdate"
|
||||
}
|
||||
}
|
||||
else:
|
||||
res["MultipartHttpPushUri"] = "/FWUpdate"
|
||||
|
||||
return Response(json.dumps(res), status=200, mimetype="application/json")
|
||||
|
||||
|
||||
@ -94,7 +115,6 @@ def firmware_inventory_bmc():
|
||||
"@odata.type": "#SoftwareInventory.v1_2_3.SoftwareInventory",
|
||||
"Id": "BMC",
|
||||
"LowestSupportedVersion": "11A-0.12",
|
||||
"Manufacturer": "Lenovo",
|
||||
"Name": "Lenovo BMC Firmware",
|
||||
"RelatedItem": [{"@odata.id": "/redfish/v1/Managers/BMC"}],
|
||||
"SoftwareId": "UEFI-AFE1-6",
|
||||
@ -103,6 +123,13 @@ def firmware_inventory_bmc():
|
||||
"Version": "11A-1.02",
|
||||
"ReleaseDate": "2019-03-15T00:00:00",
|
||||
}
|
||||
if request.authorization["username"] in {
|
||||
HARDCODED_UNL_USERNAME,
|
||||
HARDCODED_SMC_USERNAME,
|
||||
}:
|
||||
res["Manufacturer"] = "SMCI"
|
||||
else:
|
||||
res["Manufacturer"] = "Lenovo"
|
||||
return Response(json.dumps(res), status=200, mimetype="application/json")
|
||||
|
||||
|
||||
@ -164,7 +191,6 @@ def firmware_inventory_bios():
|
||||
"@odata.type": "#SoftwareInventory.v1_2_3.SoftwareInventory",
|
||||
"Id": "BIOS",
|
||||
"LowestSupportedVersion": "P79 v1.10",
|
||||
"Manufacturer": "Contoso",
|
||||
"Name": "Contoso BIOS Firmware",
|
||||
"RelatedItem": [{"@odata.id": "/redfish/v1/Systems/437XR1138R2"}],
|
||||
"ReleaseDate": "2017-12-06T12:00:00",
|
||||
@ -173,6 +199,13 @@ def firmware_inventory_bios():
|
||||
"Version": "P79 v1.45",
|
||||
"ReleaseDate": "2019-03-15T00:00:00Z",
|
||||
}
|
||||
if request.authorization["username"] in {
|
||||
HARDCODED_UNL_USERNAME,
|
||||
HARDCODED_SMC_USERNAME,
|
||||
}:
|
||||
res["Manufacturer"] = "SMCI"
|
||||
else:
|
||||
res["Manufacturer"] = "Contoso"
|
||||
return Response(json.dumps(res), status=200, mimetype="application/json")
|
||||
|
||||
|
||||
@ -188,18 +221,18 @@ def task_manager():
|
||||
|
||||
|
||||
@app.route("/redfish/v1/TaskService/Tasks/545")
|
||||
def task_status():
|
||||
def task_status_545():
|
||||
|
||||
res = {
|
||||
"@odata.id": "/redfish/v1/TaskService/Tasks/545",
|
||||
"@odata.type": "#Task.v1_4_3.Task",
|
||||
"Id": "545",
|
||||
"Name": "Task 545",
|
||||
"PercentComplete": app._percentage,
|
||||
"PercentComplete": app._percentage545,
|
||||
}
|
||||
if app._percentage == 0:
|
||||
if app._percentage545 == 0:
|
||||
res["TaskState"] = "Running"
|
||||
elif app._percentage in [25, 50, 75]:
|
||||
elif app._percentage545 in [25, 50, 75]:
|
||||
res["TaskState"] = "Running"
|
||||
res["TaskStatus"] = "OK"
|
||||
res["Messages"] = [
|
||||
@ -208,7 +241,7 @@ def task_status():
|
||||
"MessageId": "Update.1.1.TransferringToComponent",
|
||||
}
|
||||
]
|
||||
elif app._percentage == 100:
|
||||
elif app._percentage545 == 100:
|
||||
res["TaskState"] = "Completed"
|
||||
res["TaskStatus"] = "OK"
|
||||
res["Messages"] = [
|
||||
@ -235,10 +268,150 @@ def task_status():
|
||||
"Severity": "Warning",
|
||||
}
|
||||
]
|
||||
app._percentage += 25
|
||||
app._percentage545 += 25
|
||||
return Response(response=json.dumps(res), status=200, mimetype="application/json")
|
||||
|
||||
|
||||
@app.route("/redfish/v1/TaskService/Tasks/546")
|
||||
def task_status_546():
|
||||
|
||||
res = {
|
||||
"@odata.type": "#Task.v1_4_3.Task",
|
||||
"@odata.id": "/redfish/v1/TaskService/Tasks/546",
|
||||
"Id": "546",
|
||||
"Name": "BIOS Verify",
|
||||
"TaskState": "Running",
|
||||
"StartTime": "2022-09-29T14:50:54+00:00",
|
||||
"PercentComplete": app._percentage546,
|
||||
"HidePayload": True,
|
||||
"TaskMonitor": "/redfish/v1/TaskMonitor/gd5n5ffS4gi9r6YKVZmgIIaj8ECLfnc",
|
||||
"TaskStatus": "OK",
|
||||
"Messages": [
|
||||
{
|
||||
"MessageId": "",
|
||||
"RelatedProperties": [""],
|
||||
"Message": "",
|
||||
"MessageArgs": [""],
|
||||
"Severity": "",
|
||||
}
|
||||
],
|
||||
"Oem": {},
|
||||
}
|
||||
if app._percentage546 == 0:
|
||||
res["TaskState"] = "Running"
|
||||
elif app._percentage546 in [25, 50, 75]:
|
||||
res["TaskState"] = "Running"
|
||||
elif app._percentage546 == 100:
|
||||
res["TaskState"] = "Completed"
|
||||
app._percentage546 += 25
|
||||
return Response(response=json.dumps(res), status=200, mimetype="application/json")
|
||||
|
||||
|
||||
@app.route("/FWUpdate-unlicensed", methods=["GET"])
|
||||
def fwupdate_unlicensed():
|
||||
|
||||
res = {
|
||||
"error": {
|
||||
"code": "Base.v1_4_0.GeneralError",
|
||||
"Message": "A general error has occurred. See ExtendedInfo for more information.",
|
||||
"@Message.ExtendedInfo": [
|
||||
{
|
||||
"MessageId": "SMC.1.0.OemLicenseNotPassed",
|
||||
"Severity": "Warning",
|
||||
"Resolution": "Please check if there was the next step with respective API to execute.",
|
||||
"Message": "The BIOS firmware update was already in update mode.",
|
||||
"MessageArgs": ["BIOS"],
|
||||
"RelatedProperties": ["EnterUpdateMode_StatusCheck"],
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
return Response(json.dumps(res), status=405, mimetype="application/json")
|
||||
data = json.loads(request.form["UpdateParameters"])
|
||||
if data["@Redfish.OperationApplyTime"] != "Immediate":
|
||||
return _failure("apply invalid")
|
||||
if data["Targets"][0] != "/redfish/v1/UpdateService/FirmwareInventory/BMC":
|
||||
return _failure("id invalid")
|
||||
fileitem = request.files["UpdateFile"]
|
||||
if not fileitem.filename.endswith(".bin"):
|
||||
return _failure("filename invalid")
|
||||
if fileitem.read().decode() != "hello":
|
||||
return _failure("data invalid")
|
||||
res = {
|
||||
"Version": "P79 v1.45",
|
||||
"@odata.id": "/redfish/v1/TaskService/Tasks/545",
|
||||
"TaskMonitor": "/redfish/v1/TaskService/999",
|
||||
}
|
||||
# Location set to the URI of a task monitor.
|
||||
return Response(
|
||||
json.dumps(res),
|
||||
status=202,
|
||||
mimetype="application/json",
|
||||
headers={"Location": "http://localhost:4661/redfish/v1/TaskService/Tasks/545"},
|
||||
)
|
||||
|
||||
|
||||
@app.route("/FWUpdate-smc", methods=["POST"])
|
||||
def fwupdate_smc():
|
||||
|
||||
data = json.loads(request.form["UpdateParameters"])
|
||||
if data["@Redfish.OperationApplyTime"] != "OnStartUpdateRequest":
|
||||
return _failure("apply invalid")
|
||||
if data["Targets"][0] != "/redfish/v1/Systems/1/Bios":
|
||||
return _failure("id invalid")
|
||||
fileitem = request.files["UpdateFile"]
|
||||
if not fileitem.filename.endswith(".bin"):
|
||||
return _failure("filename invalid")
|
||||
filecontents = fileitem.read().decode()
|
||||
if filecontents == "hello":
|
||||
app._percentage546 = 0
|
||||
res = {
|
||||
"Accepted": {
|
||||
"code": "Base.v1_4_0.Accepted",
|
||||
"Message": "Successfully Accepted Request. Please see the location header and ExtendedInfo for more information.",
|
||||
"@Message.ExtendedInfo": [
|
||||
{
|
||||
"MessageId": "SMC.1.0.OemSimpleupdateAcceptedMessage",
|
||||
"Severity": "Ok",
|
||||
"Resolution": "No resolution was required.",
|
||||
"Message": "Please also check Task Resource /redfish/v1/TaskService/Tasks/546 to see more information.",
|
||||
"MessageArgs": ["/redfish/v1/TaskService/Tasks/546"],
|
||||
"RelatedProperties": ["BiosVerifyAccepted"],
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
# Location set to the URI of a task monitor.
|
||||
return Response(
|
||||
json.dumps(res),
|
||||
status=202,
|
||||
mimetype="application/json",
|
||||
headers={
|
||||
"Location": "http://localhost:4661/redfish/v1/TaskService/Tasks/546"
|
||||
},
|
||||
)
|
||||
elif filecontents == "stuck":
|
||||
res = {
|
||||
"error": {
|
||||
"code": "Base.v1_4_0.GeneralError",
|
||||
"Message": "A general error has occurred. See ExtendedInfo for more information.",
|
||||
"@Message.ExtendedInfo": [
|
||||
{
|
||||
"MessageId": "SMC.1.0.OemFirmwareAlreadyInUpdateMode",
|
||||
"Severity": "Warning",
|
||||
"Resolution": "Please check if there was the next step with respective API to execute.",
|
||||
"Message": "The BIOS firmware update was already in update mode.",
|
||||
"MessageArgs": ["BIOS"],
|
||||
"RelatedProperties": ["EnterUpdateMode_StatusCheck"],
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
return Response(json.dumps(res), status=405, mimetype="application/json")
|
||||
else:
|
||||
return _failure("data invalid")
|
||||
|
||||
|
||||
@app.route("/FWUpdate", methods=["POST"])
|
||||
def fwupdate():
|
||||
|
||||
@ -266,5 +439,34 @@ def fwupdate():
|
||||
)
|
||||
|
||||
|
||||
@app.route(
|
||||
"/redfish/v1/UpdateService/Actions/UpdateService.StartUpdate", methods=["POST"]
|
||||
)
|
||||
def startupdate():
|
||||
res = {
|
||||
"Accepted": {
|
||||
"code": "Base.v1_4_0.Accepted",
|
||||
"Message": "Successfully Accepted Request. Please see the location header and ExtendedInfo for more information.",
|
||||
"@Message.ExtendedInfo": [
|
||||
{
|
||||
"MessageId": "SMC.1.0.OemSimpleupdateAcceptedMessage",
|
||||
"Severity": "Ok",
|
||||
"Resolution": "No resolution was required.",
|
||||
"Message": "Please also check Task Resource /redfish/v1/TaskService/Tasks/546 to see more information.",
|
||||
"MessageArgs": ["/redfish/v1/TaskService/Tasks/546"],
|
||||
"RelatedProperties": ["BiosUpdateAccepted"],
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
app._percentage546 = 0
|
||||
return Response(
|
||||
json.dumps(res),
|
||||
status=202,
|
||||
mimetype="application/json",
|
||||
headers={"Location": "http://localhost:4661/redfish/v1/TaskService/Tasks/546"},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=4661)
|
||||
|
@ -1374,6 +1374,10 @@ fu_util_device_problem_to_string(FwupdClient *client, FwupdDevice *dev, FwupdDev
|
||||
/* TRANSLATORS: emulated means we are pretending to be a different model */
|
||||
return g_strdup(_("Device is emulated"));
|
||||
}
|
||||
if (problem == FWUPD_DEVICE_PROBLEM_MISSING_LICENSE) {
|
||||
/* TRANSLATORS: The device cannot be updated due to missing vendor's license." */
|
||||
return g_strdup(_("Device requires a software license to update"));
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user