diff --git a/libfwupd/fwupd-enums.c b/libfwupd/fwupd-enums.c index 297df8d97..4d457b5ed 100644 --- a/libfwupd/fwupd-enums.c +++ b/libfwupd/fwupd-enums.c @@ -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; } diff --git a/libfwupd/fwupd-enums.h b/libfwupd/fwupd-enums.h index 09560b949..6593455c9 100644 --- a/libfwupd/fwupd-enums.h +++ b/libfwupd/fwupd-enums.h @@ -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: * diff --git a/libfwupdplugin/fu-device.c b/libfwupdplugin/fu-device.c index 8014533bd..1c62d588c 100644 --- a/libfwupdplugin/fu-device.c +++ b/libfwupdplugin/fu-device.c @@ -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"); diff --git a/plugins/redfish/fu-redfish-backend.c b/plugins/redfish/fu-redfish-backend.c index c73611216..239efe024 100644 --- a/plugins/redfish/fu-redfish-backend.c +++ b/plugins/redfish/fu-redfish-backend.c @@ -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); } } diff --git a/plugins/redfish/fu-redfish-device.c b/plugins/redfish/fu-redfish-device.c index 8e6a2b076..4bc41ec36 100644 --- a/plugins/redfish/fu-redfish-device.c +++ b/plugins/redfish/fu-redfish-device.c @@ -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; } diff --git a/plugins/redfish/fu-redfish-plugin.c b/plugins/redfish/fu-redfish-plugin.c index cfd39007b..9c91c0b18 100644 --- a/plugins/redfish/fu-redfish-plugin.c +++ b/plugins/redfish/fu-redfish-plugin.c @@ -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) { diff --git a/plugins/redfish/fu-redfish-plugin.h b/plugins/redfish/fu-redfish-plugin.h index ce4378ffb..a3ed913ab 100644 --- a/plugins/redfish/fu-redfish-plugin.h +++ b/plugins/redfish/fu-redfish-plugin.h @@ -9,3 +9,6 @@ #include 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); diff --git a/plugins/redfish/fu-redfish-request.c b/plugins/redfish/fu-redfish-request.c index 67b8b1979..fc18c827e 100644 --- a/plugins/redfish/fu-redfish-request.c +++ b/plugins/redfish/fu-redfish-request.c @@ -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; } diff --git a/plugins/redfish/fu-redfish-smc-device.c b/plugins/redfish/fu-redfish-smc-device.c new file mode 100644 index 000000000..2626ec892 --- /dev/null +++ b/plugins/redfish/fu-redfish-smc-device.c @@ -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; +} diff --git a/plugins/redfish/fu-redfish-smc-device.h b/plugins/redfish/fu-redfish-smc-device.h new file mode 100644 index 000000000..7c0b337ac --- /dev/null +++ b/plugins/redfish/fu-redfish-smc-device.h @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#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; +}; diff --git a/plugins/redfish/fu-self-test.c b/plugins/redfish/fu-self-test.c index 8c53b8a12..d793c4d2c 100644 --- a/plugins/redfish/fu-self-test.c +++ b/plugins/redfish/fu-self-test.c @@ -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(); diff --git a/plugins/redfish/meson.build b/plugins/redfish/meson.build index 87aeab02d..1e35b6ce9 100644 --- a/plugins/redfish/meson.build +++ b/plugins/redfish/meson.build @@ -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', diff --git a/plugins/redfish/redfish.quirk b/plugins/redfish/redfish.quirk index 5e9722fda..3f2790c9d 100644 --- a/plugins/redfish/redfish.quirk +++ b/plugins/redfish/redfish.quirk @@ -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 diff --git a/plugins/redfish/tests/redfish.py b/plugins/redfish/tests/redfish.py index 4a3c7682c..30d662775 100755 --- a/plugins/redfish/tests/redfish.py +++ b/plugins/redfish/tests/redfish.py @@ -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) diff --git a/src/fu-util-common.c b/src/fu-util-common.c index 013727f72..b17ac73bc 100644 --- a/src/fu-util-common.c +++ b/src/fu-util-common.c @@ -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; }