diff --git a/NEWS b/NEWS index f9316e21c0..3968feeb29 100644 --- a/NEWS +++ b/NEWS @@ -143,6 +143,12 @@ CHANGES WITH 262 in spe: "none" returned previously. In the human-readable table output a hierarchy that is not merged is now shown as "-" instead of "none". + * The JSON payload returned by systemd-sysupdated's DescribeFeature() + method changed the "name" and "enabled" keys to "id" and + "isEnabled", respectively. The "appStream" key is now "appstream", + and the "documentation" key is now an array of strings. Consumers + of this interface must update their JSON parsing accordingly. + Changes in the system and service manager: * The manager now embeds a basic set of unit files (basic.target, diff --git a/man/org.freedesktop.sysupdate1.xml b/man/org.freedesktop.sysupdate1.xml index 2063bda26b..09d55c3c6b 100644 --- a/man/org.freedesktop.sysupdate1.xml +++ b/man/org.freedesktop.sysupdate1.xml @@ -322,8 +322,8 @@ node /org/freedesktop/sysupdate1/target/host { - name - A string containing the feature's name. + id + A string containing the feature's ID. @@ -333,18 +333,24 @@ node /org/freedesktop/sysupdate1/target/host { - enabled + isEnabled A boolean indicating whether this feature is enabled. - documentationUrl - An optional string that contains a user-presentable HTTP/HTTPS URL to documentation + suggested + An optional boolean indicating whether this feature is suggested for enablement. If + omitted, it could not be determined whether the feature is suggested. + + + + documentation + An optional array of strings containing user-presentable HTTP/HTTPS URLs to documentation about this feature. - appstreamUrl + appstream An optional string that contains an HTTP/HTTPS URL to an appstream catalog XML file containing metadata about this feature. diff --git a/man/sysupdate.components.xml b/man/sysupdate.components.xml index 54cec98c7c..c199b3f170 100644 --- a/man/sysupdate.components.xml +++ b/man/sysupdate.components.xml @@ -98,8 +98,9 @@ Documentation= - A user-presentable URL to documentation about this component. - This setting supports specifier expansion; see below for details on supported specifiers. + One or more user-presentable URLs to documentation about this component. This setting may be + specified more than once. If the empty string is assigned to this option, the list is reset, and all prior + assignments have no effect. It supports specifier expansion; see below for details on supported specifiers. diff --git a/man/sysupdate.features.xml b/man/sysupdate.features.xml index 29f067f61b..f3f7693e0f 100644 --- a/man/sysupdate.features.xml +++ b/man/sysupdate.features.xml @@ -81,8 +81,11 @@ Documentation= - A user-presentable URL to documentation about this feature. - This setting supports specifier expansion; see below for details on supported specifiers. + One or more user-presentable URLs to documentation about this feature. This setting may be + specified more than once. If the empty string is assigned to this option, the list is reset, and all prior + assignments have no effect. It supports specifier expansion; see below for details on supported specifiers. + + Support for specifying this setting more than once was added in version 262. diff --git a/src/analyze/analyze-condition.c b/src/analyze/analyze-condition.c index 8704e0a6e3..293d82c100 100644 --- a/src/analyze/analyze-condition.c +++ b/src/analyze/analyze-condition.c @@ -125,11 +125,11 @@ static int verify_conditions(char **lines, RuntimeScope scope, const char *unit, } condition_test_logger_t logger = arg_quiet ? NULL : log_helper; - r = condition_test_list(u->asserts, environ, assert_type_to_string, logger, u); + r = condition_test_list(u->asserts, environ, assert_type_to_string, logger, u) > 0; if (u->asserts) log_full(arg_quiet ? LOG_DEBUG : LOG_NOTICE, "Asserts %s.", r > 0 ? "succeeded" : "failed"); - q = condition_test_list(u->conditions, environ, condition_type_to_string, logger, u); + q = condition_test_list(u->conditions, environ, condition_type_to_string, logger, u) > 0; if (u->conditions) log_full(arg_quiet ? LOG_DEBUG : LOG_NOTICE, "Conditions %s.", q > 0 ? "succeeded" : "failed"); diff --git a/src/core/unit.c b/src/core/unit.c index 28a4af186b..805642f2d3 100644 --- a/src/core/unit.c +++ b/src/core/unit.c @@ -1819,7 +1819,7 @@ static bool unit_test_condition(Unit *u) { env, condition_type_to_string, log_unit_internal, - u); + u) > 0; unit_add_to_dbus_queue(u); return u->condition_result; @@ -1843,7 +1843,7 @@ static bool unit_test_assert(Unit *u) { env, assert_type_to_string, log_unit_internal, - u); + u) > 0; unit_add_to_dbus_queue(u); return u->assert_result; diff --git a/src/shared/condition.c b/src/shared/condition.c index cc13f7bd0b..f00f3132e6 100644 --- a/src/shared/condition.c +++ b/src/shared/condition.c @@ -28,6 +28,7 @@ #include "efivars.h" #include "env-file.h" #include "env-util.h" +#include "errno-util.h" #include "extract-word.h" #include "fd-util.h" #include "fileio.h" @@ -1420,7 +1421,7 @@ int condition_test(Condition *c, char **env) { return condition_test_impl(c, env, table); } -static bool condition_test_list_impl( +static int condition_test_list_impl( Condition *first, char **env, condition_to_string_t to_string, @@ -1428,7 +1429,8 @@ static bool condition_test_list_impl( condition_test_func_t tester, void *userdata) { - int triggered = -1; + int non_trigger_error = 0, trigger_error = 0; + bool has_trigger = false, trigger_success = false; /* If the condition list is empty, then it is true */ if (!first) @@ -1440,12 +1442,14 @@ static bool condition_test_list_impl( LIST_FOREACH(conditions, c, first) { int r; + has_trigger = has_trigger || c->trigger; + r = tester(c, env); if (logger) { if (r < 0) logger(userdata, LOG_WARNING, r, PROJECT_FILE, __LINE__, __func__, - "Couldn't determine result for %s=%s%s%s, assuming failed: %m", + "Couldn't determine result for %s=%s%s%s: %m", to_string(c->type), c->trigger ? "|" : "", c->negate ? "!" : "", @@ -1460,14 +1464,26 @@ static bool condition_test_list_impl( condition_result_to_string(c->result)); } - if (!c->trigger && r <= 0) + if (r < 0) { + if (c->trigger) + RET_GATHER(trigger_error, r); + else + RET_GATHER(non_trigger_error, r); + + continue; + } + + if (!c->trigger && r == 0) return false; - if (c->trigger && triggered <= 0) - triggered = r > 0; + if (c->trigger && r > 0) + trigger_success = true; } - return triggered != 0; + if (!has_trigger || trigger_success) + return non_trigger_error < 0 ? non_trigger_error : true; + + return trigger_error < 0 ? trigger_error : false; } bool condition_test_list_net( @@ -1477,10 +1493,10 @@ bool condition_test_list_net( condition_test_logger_t logger, void *userdata) { - return condition_test_list_impl(first, env, to_string, logger, condition_test_net, userdata); + return condition_test_list_impl(first, env, to_string, logger, condition_test_net, userdata) > 0; } -bool condition_test_list( +int condition_test_list( Condition *first, char **env, condition_to_string_t to_string, diff --git a/src/shared/condition.h b/src/shared/condition.h index 26f3ba407f..21eceea4a1 100644 --- a/src/shared/condition.h +++ b/src/shared/condition.h @@ -86,7 +86,7 @@ int condition_test(Condition *c, char **env); typedef int (*condition_test_logger_t)(void *userdata, int level, int error, const char *file, int line, const char *func, const char *format, ...) _printf_(7, 8); typedef const char* (*condition_to_string_t)(ConditionType t) _const_; bool condition_test_list_net(Condition *first, char **env, condition_to_string_t to_string, condition_test_logger_t logger, void *userdata); -bool condition_test_list(Condition *first, char **env, condition_to_string_t to_string, condition_test_logger_t logger, void *userdata); +int condition_test_list(Condition *first, char **env, condition_to_string_t to_string, condition_test_logger_t logger, void *userdata); void condition_dump(Condition *c, FILE *f, const char *prefix, condition_to_string_t to_string); void condition_dump_list(Condition *first, FILE *f, const char *prefix, condition_to_string_t to_string); diff --git a/src/shared/varlink-io.systemd.SysUpdate.c b/src/shared/varlink-io.systemd.SysUpdate.c index 0fca89adf0..68aa337a0e 100644 --- a/src/shared/varlink-io.systemd.SysUpdate.c +++ b/src/shared/varlink-io.systemd.SysUpdate.c @@ -42,6 +42,8 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE( SD_VARLINK_DEFINE_FIELD(appstream, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Whether the feature is enabled."), SD_VARLINK_DEFINE_FIELD(isEnabled, SD_VARLINK_BOOL, 0), + SD_VARLINK_FIELD_COMMENT("Whether the feature is suggested for enablement."), + SD_VARLINK_DEFINE_FIELD(suggested, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Array of IDs of the transfers (including currently disabled ones) which are controlled by this feature."), SD_VARLINK_DEFINE_FIELD(transfers, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY)); diff --git a/src/sysupdate/sysupdate-feature.c b/src/sysupdate/sysupdate-feature.c index 26e0564a97..e8df17f3af 100644 --- a/src/sysupdate/sysupdate-feature.c +++ b/src/sysupdate/sysupdate-feature.c @@ -8,6 +8,7 @@ #include "hash-funcs.h" #include "path-util.h" #include "string-util.h" +#include "strv.h" #include "sysupdate-config.h" #include "sysupdate-feature.h" #include "sysupdate-util.h" @@ -19,7 +20,7 @@ static Feature *feature_free(Feature *f) { free(f->id); free(f->description); - free(f->documentation); + strv_free(f->documentation); free(f->appstream); condition_free_list(f->suggest_on); @@ -72,22 +73,22 @@ int feature_read_definition(Feature *f, const char *root, const char *path, cons return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Malformed feature filename '%s': %m", filename); ConfigTableItem table[] = { - { "Feature", "Description", config_parse_string, 0, &f->description }, - { "Feature", "Documentation", config_parse_url_specifiers, 0, &f->documentation }, - { "Feature", "AppStream", config_parse_url_specifiers, 0, &f->appstream }, - { "Feature", "Enabled", config_parse_bool, 0, &f->enabled }, - { "Feature", "Suggest", config_parse_tristate, 0, &f->suggest }, - { "Feature", "SuggestOnArchitecture", config_parse_condition, CONDITION_ARCHITECTURE, &f->suggest_on }, - { "Feature", "SuggestOnFirmware", config_parse_condition, CONDITION_FIRMWARE, &f->suggest_on }, - { "Feature", "SuggestOnVirtualization", config_parse_condition, CONDITION_VIRTUALIZATION, &f->suggest_on }, - { "Feature", "SuggestOnHost", config_parse_condition, CONDITION_HOST, &f->suggest_on }, - { "Feature", "SuggestOnFraction", config_parse_condition, CONDITION_FRACTION, &f->suggest_on }, - { "Feature", "SuggestOnKernelCommandLine", config_parse_condition, CONDITION_KERNEL_COMMAND_LINE, &f->suggest_on }, - { "Feature", "SuggestOnVersion", config_parse_condition, CONDITION_VERSION, &f->suggest_on }, - { "Feature", "SuggestOnCredential", config_parse_condition, CONDITION_CREDENTIAL, &f->suggest_on }, - { "Feature", "SuggestOnSecurity", config_parse_condition, CONDITION_SECURITY, &f->suggest_on }, - { "Feature", "SuggestOnOSRelease", config_parse_condition, CONDITION_OS_RELEASE, &f->suggest_on }, - { "Feature", "SuggestOnMachineTag", config_parse_condition, CONDITION_MACHINE_TAG, &f->suggest_on }, + { "Feature", "Description", config_parse_string, 0, &f->description }, + { "Feature", "Documentation", config_parse_url_specifiers_many, 0, &f->documentation }, + { "Feature", "AppStream", config_parse_url_specifiers, 0, &f->appstream }, + { "Feature", "Enabled", config_parse_bool, 0, &f->enabled }, + { "Feature", "Suggest", config_parse_tristate, 0, &f->suggest }, + { "Feature", "SuggestOnArchitecture", config_parse_condition, CONDITION_ARCHITECTURE, &f->suggest_on }, + { "Feature", "SuggestOnFirmware", config_parse_condition, CONDITION_FIRMWARE, &f->suggest_on }, + { "Feature", "SuggestOnVirtualization", config_parse_condition, CONDITION_VIRTUALIZATION, &f->suggest_on }, + { "Feature", "SuggestOnHost", config_parse_condition, CONDITION_HOST, &f->suggest_on }, + { "Feature", "SuggestOnFraction", config_parse_condition, CONDITION_FRACTION, &f->suggest_on }, + { "Feature", "SuggestOnKernelCommandLine", config_parse_condition, CONDITION_KERNEL_COMMAND_LINE, &f->suggest_on }, + { "Feature", "SuggestOnVersion", config_parse_condition, CONDITION_VERSION, &f->suggest_on }, + { "Feature", "SuggestOnCredential", config_parse_condition, CONDITION_CREDENTIAL, &f->suggest_on }, + { "Feature", "SuggestOnSecurity", config_parse_condition, CONDITION_SECURITY, &f->suggest_on }, + { "Feature", "SuggestOnOSRelease", config_parse_condition, CONDITION_OS_RELEASE, &f->suggest_on }, + { "Feature", "SuggestOnMachineTag", config_parse_condition, CONDITION_MACHINE_TAG, &f->suggest_on }, {} }; @@ -111,7 +112,7 @@ int feature_read_definition(Feature *f, const char *root, const char *path, cons return 0; } -int feature_is_suggested(Feature *f) { +int feature_is_suggested(const Feature *f) { assert(f); if (f->suggest >= 0) diff --git a/src/sysupdate/sysupdate-feature.h b/src/sysupdate/sysupdate-feature.h index 3f46ded7f9..a525f1ab84 100644 --- a/src/sysupdate/sysupdate-feature.h +++ b/src/sysupdate/sysupdate-feature.h @@ -9,7 +9,7 @@ typedef struct Feature { char *id; char *description; - char *documentation; + char **documentation; char *appstream; bool enabled; @@ -27,4 +27,4 @@ extern const struct hash_ops feature_hash_ops; int feature_read_definition(Feature *f, const char *root, const char *path, const char *const *conf_file_dirs); -int feature_is_suggested(Feature *f); +int feature_is_suggested(const Feature *f); diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 73ae5cebe8..4f55ccdafb 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -2019,6 +2019,8 @@ static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) { } } +static int feature_to_json(Context *context, const Feature *f, sd_json_variant **ret); + VERB(verb_features, "features", "[FEATURE]\0", VERB_ANY, 2, 0, "Show optional features"); static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata) { @@ -2056,6 +2058,20 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata "Optional feature not found: %s", feature_id); + if (sd_json_format_enabled(arg_json_format_flags)) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; + + r = feature_to_json(&context, f, &json); + if (r < 0) + return r; + + r = sd_json_variant_dump(json, arg_json_format_flags, stdout, NULL); + if (r < 0) + return log_error_errno(r, "Failed to print JSON: %m"); + + return 0; + } + table = table_new_vertical(); if (!table) return log_oom(); @@ -2092,11 +2108,10 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata return table_log_add_error(r); } - if (f->documentation) { + if (!strv_isempty(f->documentation)) { r = table_add_many(table, TABLE_FIELD, "Documentation", - TABLE_STRING, f->documentation, - TABLE_SET_URL, f->documentation); + TABLE_STRV_WRAPPED, f->documentation); if (r < 0) return table_log_add_error(r); } @@ -2142,8 +2157,7 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata r = table_add_many(table, TABLE_STRING, f->id, TABLE_STRING, f->description, - TABLE_STRING, f->documentation, - TABLE_SET_URL, f->documentation); + TABLE_STRV_WRAPPED, f->documentation); if (r < 0) return table_log_add_error(r); } @@ -2248,8 +2262,10 @@ static int context_enable_feature( Feature *f; HASHMAP_FOREACH(f, c->features) { r = feature_is_suggested(f); - if (r < 0) - return log_error_errno(r, "Failed to determine if feature '%s' of component '%s' is suggested: %m", f->id, context_component_display(c)); + if (r < 0) { + log_warning_errno(r, "Failed to determine if feature '%s' of component '%s' is suggested, skipping: %m", f->id, context_component_display(c)); + continue; + } if (r == 0) { log_debug("Feature '%s' of component '%s' is not suggested, skipping.", f->id, context_component_display(c)); continue; @@ -2404,7 +2420,7 @@ static int verb_enable_feature(int argc, char *argv[], uintptr_t _data, void *us static int feature_to_json(Context *context, const Feature *f, sd_json_variant **ret) { _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; _cleanup_strv_free_ char **transfers = NULL; - const char *documentation_strv[2] = { NULL, }; + int suggested; int r; assert(context); @@ -2415,17 +2431,20 @@ static int feature_to_json(Context *context, const Feature *f, sd_json_variant * if (r < 0) return r; - /* FIXME: Long term we’d like to support an array of documentation, but currently the D-Bus interface - * doesn’t support that and neither do the internals of sysupdate. So just expose 0 or 1 URLs for now. */ - documentation_strv[0] = f->documentation; + suggested = feature_is_suggested(f); + if (suggested < 0) + log_debug_errno(suggested, + "Failed to determine whether feature '%s' is suggested, omitting field: %m", + f->id); r = sd_json_variant_merge_objectbo( &v, SD_JSON_BUILD_PAIR_STRING("id", f->id), JSON_BUILD_PAIR_STRING_NON_EMPTY("description", f->description), - JSON_BUILD_PAIR_STRV_NON_EMPTY("documentation", (char **) documentation_strv), + JSON_BUILD_PAIR_STRV_NON_EMPTY("documentation", f->documentation), JSON_BUILD_PAIR_STRING_NON_EMPTY("appstream", f->appstream), SD_JSON_BUILD_PAIR_BOOLEAN("isEnabled", f->enabled), + SD_JSON_BUILD_PAIR_CONDITION(suggested >= 0, "suggested", SD_JSON_BUILD_BOOLEAN(suggested > 0)), JSON_BUILD_PAIR_STRV_NON_EMPTY("transfers", transfers)); if (r < 0) return log_oom(); @@ -3073,14 +3092,11 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda if (r < 0) return table_log_add_error(r); - const char *doc = cc.component_documentation ? cc.component_documentation[0] : NULL; - r = table_add_many( t, TABLE_STRING, *i, TABLE_STRING, cc.component_description, - TABLE_STRING, doc, - TABLE_SET_URL, doc); + TABLE_STRV_WRAPPED, cc.component_documentation); if (r < 0) return table_log_add_error(r); } @@ -3089,8 +3105,9 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda } else { _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; - r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_BOOLEAN("default", has_default_component), - SD_JSON_BUILD_PAIR_STRV("components", component_names)); + r = sd_json_buildo(&json, + SD_JSON_BUILD_PAIR_BOOLEAN("default", has_default_component), + SD_JSON_BUILD_PAIR_STRV("components", component_names)); if (r < 0) return log_error_errno(r, "Failed to create JSON: %m"); diff --git a/src/sysupdate/updatectl.c b/src/sysupdate/updatectl.c index 315d0e7381..d9ecf69818 100644 --- a/src/sysupdate/updatectl.c +++ b/src/sysupdate/updatectl.c @@ -1386,18 +1386,26 @@ static int verb_vacuum(int argc, char *argv[], uintptr_t _data, void *userdata) } typedef struct Feature { - char *name; + char *id; char *description; bool enabled; - char *documentation; + int suggested; + char **documentation; + char *appstream; char **transfers; } Feature; +#define FEATURE_NULL \ + (Feature) { \ + .suggested = -1, \ + } + static void feature_done(Feature *f) { assert(f); - f->name = mfree(f->name); + f->id = mfree(f->id); f->description = mfree(f->description); - f->documentation = mfree(f->documentation); + f->documentation = strv_free(f->documentation); + f->appstream = mfree(f->appstream); f->transfers = strv_free(f->transfers); } @@ -1405,16 +1413,18 @@ static int describe_feature(sd_bus *bus, const char *feature, Feature *ret) { _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; - _cleanup_(feature_done) Feature f = {}; + _cleanup_(feature_done) Feature f = FEATURE_NULL; char *json; int r; static const sd_json_dispatch_field dispatch_table[] = { - { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, name), SD_JSON_MANDATORY }, - { "description", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, description), 0 }, - { "enabled", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(Feature, enabled), SD_JSON_MANDATORY }, - { "documentationUrl", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, documentation), 0 }, - { "transfers", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(Feature, transfers), 0 }, + { "id", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, id), SD_JSON_MANDATORY }, + { "description", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, description), SD_JSON_NULLABLE }, + { "isEnabled", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(Feature, enabled), SD_JSON_MANDATORY }, + { "suggested", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(Feature, suggested), SD_JSON_NULLABLE }, + { "documentation", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(Feature, documentation), SD_JSON_NULLABLE }, + { "appstream", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(Feature, appstream), SD_JSON_NULLABLE }, + { "transfers", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(Feature, transfers), SD_JSON_NULLABLE }, {} }; @@ -1443,7 +1453,7 @@ static int describe_feature(sd_bus *bus, const char *feature, Feature *ret) { if (r < 0) return log_error_errno(r, "Failed to parse JSON: %m"); - r = sd_json_dispatch(v, dispatch_table, 0, &f); + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &f); if (r < 0) return log_error_errno(r, "Failed to dispatch JSON: %m"); @@ -1481,15 +1491,15 @@ static int list_features(sd_bus *bus) { return bus_log_parse_error(r); STRV_FOREACH(feature, features) { - _cleanup_(feature_done) Feature f = {}; + _cleanup_(feature_done) Feature f = FEATURE_NULL; _cleanup_free_ char *name_link = NULL; r = describe_feature(bus, *feature, &f); if (r < 0) return r; - if (urlify_enabled() && f.documentation) { - name_link = strjoin(f.name, glyph(GLYPH_EXTERNAL_LINK)); + if (urlify_enabled() && !strv_isempty(f.documentation)) { + name_link = strjoin(f.id, glyph(GLYPH_EXTERNAL_LINK)); if (!name_link) return log_oom(); } @@ -1497,8 +1507,8 @@ static int list_features(sd_bus *bus) { r = table_add_many(table, TABLE_BOOLEAN_CHECKMARK, f.enabled, TABLE_SET_COLOR, ansi_highlight_green_red(f.enabled), - TABLE_STRING, name_link ?: f.name, - TABLE_SET_URL, f.documentation, + TABLE_STRING, name_link ?: f.id, + TABLE_SET_URL, strv_isempty(f.documentation) ? NULL : f.documentation[0], TABLE_STRING, f.description); if (r < 0) return table_log_add_error(r); @@ -1512,7 +1522,7 @@ VERB(verb_features, "features", "[FEATURE]\0", VERB_ANY, 2, VERB_ONLINE_ONLY, static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata) { sd_bus *bus = ASSERT_PTR(userdata); _cleanup_(table_unrefp) Table *table = NULL; - _cleanup_(feature_done) Feature f = {}; + _cleanup_(feature_done) Feature f = FEATURE_NULL; int r; if (argc == 1) @@ -1528,23 +1538,39 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata r = table_add_many(table, TABLE_FIELD, "Name", - TABLE_STRING, f.name, + TABLE_STRING, f.id, TABLE_FIELD, "Enabled", TABLE_BOOLEAN, f.enabled); if (r < 0) return table_log_add_error(r); + if (f.suggested >= 0) { + r = table_add_many(table, + TABLE_FIELD, "Suggested", + TABLE_BOOLEAN, f.suggested); + if (r < 0) + return table_log_add_error(r); + } + if (f.description) { r = table_add_many(table, TABLE_FIELD, "Description", TABLE_STRING, f.description); if (r < 0) return table_log_add_error(r); } - if (f.documentation) { + if (!strv_isempty(f.documentation)) { r = table_add_many(table, TABLE_FIELD, "Documentation", - TABLE_STRING, f.documentation, - TABLE_SET_URL, f.documentation); + TABLE_STRV_WRAPPED, f.documentation); + if (r < 0) + return table_log_add_error(r); + } + + if (f.appstream) { + r = table_add_many(table, + TABLE_FIELD, "AppStream", + TABLE_STRING, f.appstream, + TABLE_SET_URL, f.appstream); if (r < 0) return table_log_add_error(r); } diff --git a/src/test/test-condition.c b/src/test/test-condition.c index f196dce18a..59b96976d0 100644 --- a/src/test/test-condition.c +++ b/src/test/test-condition.c @@ -44,6 +44,78 @@ #include "user-util.h" #include "virt.h" +TEST(condition_test_list) { + Condition *condition, *sibling; + Architecture arch, other_arch; + + ASSERT_OK(arch = uname_architecture()); + + ASSERT_OK_POSITIVE(condition_test_list(NULL, environ, NULL, NULL, NULL)); + + ASSERT_NOT_NULL((condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", false, false))); + ASSERT_ERROR(condition_test_list(condition, environ, NULL, NULL, NULL), EINVAL); + condition_free(condition); + + ASSERT_NOT_NULL((condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", true, false))); + ASSERT_NOT_NULL((sibling = condition_new(CONDITION_ARCHITECTURE, architecture_to_string(arch), true, false))); + LIST_APPEND(conditions, condition, sibling); + ASSERT_OK_POSITIVE(condition_test_list(condition, environ, NULL, NULL, NULL)); + condition_free_list(condition); + + ASSERT_NOT_NULL((condition = condition_new(CONDITION_ARCHITECTURE, architecture_to_string(arch), true, false))); + ASSERT_NOT_NULL((sibling = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", true, false))); + LIST_APPEND(conditions, condition, sibling); + ASSERT_OK_POSITIVE(condition_test_list(condition, environ, NULL, NULL, NULL)); + condition_free_list(condition); + + ASSERT_NOT_NULL((condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", true, false))); + ASSERT_ERROR(condition_test_list(condition, environ, NULL, NULL, NULL), EINVAL); + condition_free(condition); + + ASSERT_NOT_NULL((condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", true, false))); + ASSERT_NOT_NULL((sibling = condition_new(CONDITION_PATH_EXISTS, "/thiscertainlywontexist", false, false))); + LIST_APPEND(conditions, condition, sibling); + ASSERT_OK_ZERO(condition_test_list(condition, environ, NULL, NULL, NULL)); + condition_free_list(condition); + + ASSERT_NOT_NULL((condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", true, false))); + ASSERT_NOT_NULL((sibling = condition_new(CONDITION_PATH_EXISTS, "/bin/sh", false, false))); + LIST_APPEND(conditions, condition, sibling); + ASSERT_ERROR(condition_test_list(condition, environ, NULL, NULL, NULL), EINVAL); + condition_free_list(condition); + + ASSERT_NOT_NULL((condition = condition_new(CONDITION_PATH_EXISTS, "/thiscertainlywontexist", false, false))); + ASSERT_NOT_NULL((sibling = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", false, false))); + LIST_APPEND(conditions, condition, sibling); + ASSERT_OK_ZERO(condition_test_list(condition, environ, NULL, NULL, NULL)); + condition_free_list(condition); + + ASSERT_NOT_NULL((condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", false, false))); + ASSERT_NOT_NULL((sibling = condition_new(CONDITION_PATH_EXISTS, "/thiscertainlywontexist", false, false))); + LIST_APPEND(conditions, condition, sibling); + ASSERT_OK_ZERO(condition_test_list(condition, environ, NULL, NULL, NULL)); + condition_free_list(condition); + + ASSERT_NOT_NULL((condition = condition_new(CONDITION_PATH_EXISTS, "/bin/sh", false, false))); + ASSERT_NOT_NULL((sibling = condition_new(CONDITION_ARCHITECTURE, architecture_to_string(arch), false, false))); + LIST_APPEND(conditions, condition, sibling); + ASSERT_OK_POSITIVE(condition_test_list(condition, environ, NULL, NULL, NULL)); + condition_free_list(condition); + + ASSERT_NOT_NULL((condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", false, false))); + ASSERT_NOT_NULL((sibling = condition_new(CONDITION_ARCHITECTURE, architecture_to_string(arch), true, false))); + LIST_APPEND(conditions, condition, sibling); + ASSERT_ERROR(condition_test_list(condition, environ, NULL, NULL, NULL), EINVAL); + condition_free_list(condition); + + other_arch = arch == ARCHITECTURE_X86_64 ? ARCHITECTURE_X86 : ARCHITECTURE_X86_64; + ASSERT_NOT_NULL((condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", false, false))); + ASSERT_NOT_NULL((sibling = condition_new(CONDITION_ARCHITECTURE, architecture_to_string(other_arch), true, false))); + LIST_APPEND(conditions, condition, sibling); + ASSERT_OK_ZERO(condition_test_list(condition, environ, NULL, NULL, NULL)); + condition_free_list(condition); +} + TEST(condition_test_path) { Condition *condition; diff --git a/test/units/TEST-72-SYSUPDATE.sh b/test/units/TEST-72-SYSUPDATE.sh index b5fd26f3a7..0de5ecd370 100755 --- a/test/units/TEST-72-SYSUPDATE.sh +++ b/test/units/TEST-72-SYSUPDATE.sh @@ -254,7 +254,10 @@ verify_version_current() { verify_object_fields() { local updatectl_output="${1:?}" - [[ "${updatectl_output}" != *"Unrecognized object field"* ]] + [[ "${updatectl_output}" != *"Unexpected object field"* && + "${updatectl_output}" != *"Missing object field"* && + "${updatectl_output}" != *"Failed to dispatch JSON"* && + "${updatectl_output}" != *"Failed to parse JSON"* ]] } for sector_size in "${SECTOR_SIZES[@]}"; do @@ -397,6 +400,26 @@ EOF cat >"$CONFIGDIR/optional.feature" <"$CONFIGDIR/undocumented.feature" <"$CONFIGDIR/malformed.feature" <"$CONFIGDIR/suggested.feature" <"$CONFIGDIR/99-optional.transfer" </dev/null + jq -e ' + .documentation == [ + "https://example.com/optional", + "https://example.com/optional-more" + ] + ' <<<"$feature_json" >/dev/null + jq -e '.isEnabled == false' <<<"$feature_json" >/dev/null + jq -e '.suggested == false' <<<"$feature_json" >/dev/null + jq -e '.appstream == "https://example.com/optional.appstream.xml"' <<<"$feature_json" >/dev/null + + suggested_json="$("$SYSUPDATE" --json=short features suggested)" + jq -e '.id == "suggested" and .suggested == true' <<<"$suggested_json" >/dev/null + + undocumented_json="$("$SYSUPDATE" --json=short features undocumented)" + jq -e '.id == "undocumented" and (.documentation == null)' <<<"$undocumented_json" >/dev/null + "$SYSUPDATE" features undocumented | grep -F "Feature Without Documentation" >/dev/null + + malformed_json="$("$SYSUPDATE" --json=short features malformed)" + jq -e '.id == "malformed" and (has("suggested") | not)' <<<"$malformed_json" >/dev/null + "$SYSUPDATE" features malformed | grep -F "error (Invalid argument)" >/dev/null + all_features_output="$("$SYSUPDATE" features)" + grep -F "malformed" <<<"$all_features_output" >/dev/null + test ! -f "$WORKDIR/xbootldr/EFI/Linux/uki_v5.efi.extra.d/optional.efi" mkdir "$CONFIGDIR/optional.feature.d" echo -e "[Feature]\nEnabled=true" > "$CONFIGDIR/optional.feature.d/enable.conf" @@ -521,9 +569,25 @@ EOF if [[ -x "$UPDATECTL" ]]; then mkdir -p /run/sysupdate.test.d/ cp "$CONFIGDIR/01-first.transfer" /run/sysupdate.test.d/01-first.transfer - verify_object_fields "$("$UPDATECTL" list 2>&1)" - verify_object_fields "$("$UPDATECTL" list host 2>&1)" - verify_object_fields "$("$UPDATECTL" list host@v6 2>&1)" + updatectl_output="$("$UPDATECTL" list 2>&1)" + verify_object_fields "$updatectl_output" + updatectl_output="$("$UPDATECTL" list host 2>&1)" + verify_object_fields "$updatectl_output" + updatectl_output="$("$UPDATECTL" list host@v6 2>&1)" + verify_object_fields "$updatectl_output" + features_output="$("$UPDATECTL" features)" + grep "optional" <<<"$features_output" >/dev/null + grep "undocumented" <<<"$features_output" >/dev/null + feature_output="$("$UPDATECTL" features optional)" + grep "Optional Feature" <<<"$feature_output" >/dev/null + grep "Suggested: no" <<<"$feature_output" >/dev/null + grep "https://example.com/optional-more" <<<"$feature_output" >/dev/null + grep "https://example.com/optional.appstream.xml" <<<"$feature_output" >/dev/null + grep -F "99-optional" <<<"$feature_output" >/dev/null + grep "malformed" <<<"$features_output" >/dev/null + verify_object_fields "$features_output" + malformed_feature_output="$("$UPDATECTL" features malformed)" + (! grep -F "Suggested:" <<<"$malformed_feature_output") "$UPDATECTL" check rm -r /run/sysupdate.test.d fi @@ -635,6 +699,11 @@ done # that a ‘default’ component is only listed by sysupdate if it’s fully configured mv "$CONFIGDIR" "$CONFIGDIR.backup" mkdir -p /run/sysupdate.some-component.d +cat >/run/sysupdate.some-component.component </dev/null +component_output="$("$SYSUPDATE" components)" +grep -F "https://example.com/some-component" <<<"$component_output" >/dev/null +grep -F "https://example.com/some-component-more" <<<"$component_output" >/dev/null varlinkctl call "$VARLINK_SOCKET" io.systemd.SysUpdate.ListTargets | jq -e '.targets | all(.id.class != "host")' >/dev/null mkdir /run/sysupdate.d "$SYSUPDATE" --json=short components | grep -F '{"default":false,"components":["some-component"]}' >/dev/null @@ -670,6 +742,7 @@ varlinkctl call "$VARLINK_SOCKET" io.systemd.SysUpdate.ListTargets | jq -e '.tar # Clean up regression test rmdir /run/sysupdate.d rm -rf /run/sysupdate.some-component.d +rm -f /run/sysupdate.some-component.component mv "$CONFIGDIR.backup" "$CONFIGDIR" # Make sure the processing of compressed streams still handles uncompressed streams shorter than @@ -1950,7 +2023,8 @@ compfeat_source v1 mkdir -p /run/sysupdate.d compfeat_transfer /run/sysupdate.d/01-base.transfer base "$CF/target-default" # feata: suggested (Suggest=yes); featb: not suggested (Suggest=no); -# featc: suggested on a machine tag we will set below. +# featc: suggested on a machine tag we will set below; featd: malformed +# suggestion condition, which should be skipped without aborting the operation. cat >/run/sysupdate.d/feata.feature </run/sysupdate.d/featc.feature </run/sysupdate.d/featd.feature <