diff --git a/man/sysupdate.features.xml b/man/sysupdate.features.xml index 606292e269..d3a7a63810 100644 --- a/man/sysupdate.features.xml +++ b/man/sysupdate.features.xml @@ -109,6 +109,53 @@ + + + Suggest= + + Takes a boolean argument. If true, this feature is suggested for enablement, if + false it is not. This does not enable the feature on its own, but is a hint surfaced by the update + tools (for example via the switch of + systemd-sysupdate8) + so that the system administrator or higher-level tooling may act on it. + + If this setting is not specified, the SuggestOn…= conditions described below + are evaluated instead to determine whether the feature is suggested. If neither this setting nor any + SuggestOn…= condition is specified, the feature is not suggested. + + + + + + SuggestOnArchitecture= + SuggestOnFirmware= + SuggestOnVirtualization= + SuggestOnHost= + SuggestOnFraction= + SuggestOnKernelCommandLine= + SuggestOnVersion= + SuggestOnCredential= + SuggestOnSecurity= + SuggestOnOSRelease= + SuggestOnMachineTag= + + Suggest this feature for enablement depending on system properties. These settings + take the same arguments and implement the same semantics — including the leading ! + for negation — as the identically-named + ConditionArchitecture=, ConditionFirmware=, + ConditionVirtualization=, ConditionHost=, + ConditionFraction=, ConditionKernelCommandLine=, + ConditionVersion=, ConditionCredential=, + ConditionSecurity=, ConditionOSRelease= and + ConditionMachineTag= settings for unit files, which are documented in + systemd.unit5. + The feature is suggested if all specified conditions apply. As with the Condition…= + settings, assigning an empty string to one of these resets the list. + + These conditions are only evaluated if Suggest= is not specified. + + + diff --git a/src/shared/shared-forward.h b/src/shared/shared-forward.h index 751a6f71dc..a9c7101e55 100644 --- a/src/shared/shared-forward.h +++ b/src/shared/shared-forward.h @@ -19,6 +19,7 @@ typedef enum BusPrintPropertyFlags BusPrintPropertyFlags; typedef enum BusTransport BusTransport; typedef enum CatFlags CatFlags; typedef enum CertificateSourceType CertificateSourceType; +typedef enum ConditionType ConditionType; typedef enum DnsAnswerFlags DnsAnswerFlags; typedef enum DnsCacheMode DnsCacheMode; typedef enum DnsOverTlsMode DnsOverTlsMode; diff --git a/src/sysupdate/sysupdate-config.c b/src/sysupdate/sysupdate-config.c index 7f553a95f0..45794b5a21 100644 --- a/src/sysupdate/sysupdate-config.c +++ b/src/sysupdate/sysupdate-config.c @@ -1,8 +1,10 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "alloc-util.h" +#include "condition.h" #include "log.h" #include "specifier.h" +#include "string-table.h" #include "string-util.h" #include "strv.h" #include "sysupdate-config.h" @@ -48,6 +50,22 @@ int config_parse_url_specifiers( return free_and_replace(*s, resolved); } +static const char* const suggest_on_type_table[_CONDITION_TYPE_MAX] = { + [CONDITION_ARCHITECTURE] = "SuggestOnArchitecture", + [CONDITION_FIRMWARE] = "SuggestOnFirmware", + [CONDITION_VIRTUALIZATION] = "SuggestOnVirtualization", + [CONDITION_HOST] = "SuggestOnHost", + [CONDITION_FRACTION] = "SuggestOnFraction", + [CONDITION_KERNEL_COMMAND_LINE] = "SuggestOnKernelCommandLine", + [CONDITION_VERSION] = "SuggestOnVersion", + [CONDITION_CREDENTIAL] = "SuggestOnCredential", + [CONDITION_SECURITY] = "SuggestOnSecurity", + [CONDITION_OS_RELEASE] = "SuggestOnOSRelease", + [CONDITION_MACHINE_TAG] = "SuggestOnMachineTag", +}; + +DEFINE_STRING_TABLE_LOOKUP_TO_STRING(suggest_on_type, ConditionType); + int config_parse_url_specifiers_many( const char *unit, const char *filename, @@ -90,3 +108,58 @@ int config_parse_url_specifiers_many( return 0; } + +int config_parse_condition( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + const char *root = userdata; + ConditionType cond = ltype; + Condition **list = data, *c; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + if (isempty(rvalue)) { + *list = condition_free_list_type(*list, cond); + return 0; + } + + bool trigger = rvalue[0] == '|'; + if (trigger) { + rvalue++; + rvalue += strspn(rvalue, WHITESPACE); + } + + bool negate = rvalue[0] == '!'; + if (negate) { + rvalue++; + rvalue += strspn(rvalue, WHITESPACE); + } + + _cleanup_free_ char *resolved = NULL; + r = specifier_printf(rvalue, SIZE_MAX, system_and_tmp_specifier_table, root, /* userdata= */ NULL, &resolved); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to expand specifiers in %s=, ignoring: %s", lvalue, rvalue); + return 0; + } + + c = condition_new(cond, resolved, trigger, negate); + if (!c) + return log_oom(); + + LIST_PREPEND(conditions, *list, c); + return 0; +} diff --git a/src/sysupdate/sysupdate-config.h b/src/sysupdate/sysupdate-config.h index bd972d598d..271430c73d 100644 --- a/src/sysupdate/sysupdate-config.h +++ b/src/sysupdate/sysupdate-config.h @@ -2,6 +2,10 @@ #pragma once #include "conf-parser-forward.h" +#include "shared-forward.h" CONFIG_PARSER_PROTOTYPE(config_parse_url_specifiers); CONFIG_PARSER_PROTOTYPE(config_parse_url_specifiers_many); +CONFIG_PARSER_PROTOTYPE(config_parse_condition); + +DECLARE_STRING_TABLE_LOOKUP_TO_STRING(suggest_on_type, ConditionType); diff --git a/src/sysupdate/sysupdate-feature.c b/src/sysupdate/sysupdate-feature.c index 49c2e77988..26e0564a97 100644 --- a/src/sysupdate/sysupdate-feature.c +++ b/src/sysupdate/sysupdate-feature.c @@ -1,12 +1,16 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + #include "alloc-util.h" +#include "condition.h" #include "conf-parser.h" #include "hash-funcs.h" #include "path-util.h" #include "string-util.h" #include "sysupdate-config.h" #include "sysupdate-feature.h" +#include "sysupdate-util.h" static Feature *feature_free(Feature *f) { if (!f) @@ -18,6 +22,8 @@ static Feature *feature_free(Feature *f) { free(f->documentation); free(f->appstream); + condition_free_list(f->suggest_on); + return mfree(f); } @@ -30,6 +36,7 @@ Feature *feature_new(void) { *f = (Feature) { .n_ref = 1, + .suggest = -1, }; return f; @@ -42,26 +49,48 @@ DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(feature_hash_ops, Feature, feature_unref); int feature_read_definition(Feature *f, const char *root, const char *path, const char *const *dirs) { - assert(f); - - 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 }, - {} - }; - - _cleanup_free_ char *filename = NULL; int r; + assert(f); assert(path); assert(dirs); + _cleanup_free_ char *filename = NULL; r = path_extract_filename(path, &filename); if (r < 0) return log_error_errno(r, "Failed to extract filename from path '%s': %m", path); + char *e = endswith(filename, ".feature"); + if (!e) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Feature file has bad suffix: %s", filename); + + _cleanup_free_ char *id = strndup(filename, e - filename); + if (!id) + return log_oom(); + + if (!feature_name_valid(id)) + 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 }, + {} + }; + r = config_parse_many_full( STRV_MAKE_CONST(path), dirs, @@ -77,8 +106,19 @@ int feature_read_definition(Feature *f, const char *root, const char *path, cons if (r < 0) return r; - *ASSERT_PTR(endswith(filename, ".feature")) = 0; /* Remove the file extension */ - f->id = TAKE_PTR(filename); + f->id = TAKE_PTR(id); return 0; } + +int feature_is_suggested(Feature *f) { + assert(f); + + if (f->suggest >= 0) + return f->suggest; + + if (!f->suggest_on) /* no condition → false */ + return false; + + return condition_test_list(f->suggest_on, environ, suggest_on_type_to_string, /* logger= */ NULL, /* userdata= */ NULL); +} diff --git a/src/sysupdate/sysupdate-feature.h b/src/sysupdate/sysupdate-feature.h index 7d7495087c..3f46ded7f9 100644 --- a/src/sysupdate/sysupdate-feature.h +++ b/src/sysupdate/sysupdate-feature.h @@ -13,6 +13,9 @@ typedef struct Feature { char *appstream; bool enabled; + + int suggest; + Condition *suggest_on; } Feature; Feature *feature_new(void); @@ -23,3 +26,5 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(Feature*, feature_unref); 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); diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 56f0281162..01b4a03f4d 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -9,6 +9,7 @@ #include "build.h" #include "bus-polkit.h" +#include "condition.h" #include "conf-files.h" #include "conf-parser.h" #include "constants.h" @@ -38,6 +39,7 @@ #include "pretty-print.h" #include "runtime-scope.h" #include "sort-util.h" +#include "stdio-util.h" #include "string-util.h" #include "strv.h" #include "sysupdate.h" @@ -63,8 +65,9 @@ static char *arg_root = NULL; static char *arg_image = NULL; static bool arg_reboot = false; static int arg_cleanup = -1; +static SelectMode arg_feature_select = SELECT_EXPLICIT; static char *arg_component = NULL; -static bool arg_component_all = false; +static SelectMode arg_component_select = SELECT_EXPLICIT; static int arg_verify = -1; static ImagePolicy *arg_image_policy = NULL; static bool arg_offline = false; @@ -87,6 +90,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep); .cleanup = -1, \ .installdb_fd = -EBADF, \ .target_identifier.class = _TARGET_CLASS_INVALID, \ + .component_suggest = -1, \ } void context_done(Context *c) { @@ -126,6 +130,7 @@ void context_done(Context *c) { c->transfer_source = mfree(c->transfer_source); target_identifier_done(&c->target_identifier); + condition_free_list(c->component_suggest_on); } static int context_from_cmdline(Context *ret) { @@ -139,7 +144,8 @@ static int context_from_cmdline(Context *ret) { context.verify = arg_verify; context.offline = arg_offline; context.cleanup = arg_cleanup; - context.component_all = arg_component_all; + context.component_select = arg_component_select; + context.feature_select = arg_feature_select; if (strdup_to(&context.definitions, arg_definitions) < 0) return log_oom(); @@ -181,6 +187,7 @@ static int context_from_base_with_component(const Context *base, const char *com context.verify = base->verify; context.offline = base->offline; context.cleanup = base->cleanup; + context.feature_select = base->feature_select; if (strdup_to(&context.root, base->root) < 0) return log_oom(); @@ -346,9 +353,21 @@ static int read_component(Context *c) { return log_oom(); ConfigTableItem table[] = { - { "Component", "Description", config_parse_string, 0, &c->component_description }, - { "Component", "Documentation", config_parse_url_specifiers_many, 0, &c->component_documentation }, - { "Component", "Enabled", config_parse_bool, 0, &c->component_enabled }, + { "Component", "Description", config_parse_string, 0, &c->component_description }, + { "Component", "Documentation", config_parse_url_specifiers_many, 0, &c->component_documentation }, + { "Component", "Enabled", config_parse_bool, 0, &c->component_enabled }, + { "Component", "Suggest", config_parse_tristate, 0, &c->component_suggest }, + { "Component", "SuggestOnArchitecture", config_parse_condition, CONDITION_ARCHITECTURE, &c->component_suggest_on }, + { "Component", "SuggestOnFirmware", config_parse_condition, CONDITION_FIRMWARE, &c->component_suggest_on }, + { "Component", "SuggestOnVirtualization", config_parse_condition, CONDITION_VIRTUALIZATION, &c->component_suggest_on }, + { "Component", "SuggestOnHost", config_parse_condition, CONDITION_HOST, &c->component_suggest_on }, + { "Component", "SuggestOnFraction", config_parse_condition, CONDITION_FRACTION, &c->component_suggest_on }, + { "Component", "SuggestOnKernelCommandLine", config_parse_condition, CONDITION_KERNEL_COMMAND_LINE, &c->component_suggest_on }, + { "Component", "SuggestOnVersion", config_parse_condition, CONDITION_VERSION, &c->component_suggest_on }, + { "Component", "SuggestOnCredential", config_parse_condition, CONDITION_CREDENTIAL, &c->component_suggest_on }, + { "Component", "SuggestOnSecurity", config_parse_condition, CONDITION_SECURITY, &c->component_suggest_on }, + { "Component", "SuggestOnOSRelease", config_parse_condition, CONDITION_OS_RELEASE, &c->component_suggest_on }, + { "Component", "SuggestOnMachineTag", config_parse_condition, CONDITION_MACHINE_TAG, &c->component_suggest_on }, {} }; @@ -1747,8 +1766,10 @@ static int verb_list(int argc, char *argv[], uintptr_t _data, void *userdata) { if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); + if (context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); r = context_load_online( &context, @@ -1823,8 +1844,10 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); + if (context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); r = context_load_offline( &context, @@ -1876,6 +1899,20 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata if (r < 0) return table_log_add_error(r); + r = feature_is_suggested(f); + if (r < 0) { + errno = -r; /* Let's make %m below show this error */ + _cleanup_free_ char *k = asprintf_safe("error (%m)"); + r = table_add_many(table, + TABLE_FIELD, "Suggested", + TABLE_STRING, k); + } else + r = table_add_many(table, + TABLE_FIELD, "Suggested", + TABLE_BOOLEAN, r); + 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) @@ -1908,14 +1945,28 @@ static int verb_features(int argc, char *argv[], uintptr_t _data, void *userdata return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend); } else if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) { - table = table_new("", "feature", "description", "documentation"); + table = table_new("enabled", "suggested", "feature", "description", "documentation"); if (!table) return log_oom(); + table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + HASHMAP_FOREACH(f, context.features) { r = table_add_many(table, TABLE_BOOLEAN_CHECKMARK, f->enabled, - TABLE_SET_COLOR, ansi_highlight_green_red(f->enabled), + TABLE_SET_COLOR, ansi_highlight_green_red(f->enabled)); + if (r < 0) + return table_log_add_error(r); + + r = feature_is_suggested(f); + if (r < 0) + r = table_add_many(table, TABLE_EMPTY); + else + r = table_add_many(table, TABLE_BOOLEAN_CHECKMARK, r); + if (r < 0) + return table_log_add_error(r); + + r = table_add_many(table, TABLE_STRING, f->id, TABLE_STRING, f->description, TABLE_STRING, f->documentation, @@ -1974,9 +2025,114 @@ static int make_dropin_dir(Context *c, char **ret) { return 0; } -VERB(verb_enable_feature, "enable-feature", "FEATURE…", 2, VERB_ANY, 0, +static const char *context_component_display(const Context *c) { + assert(c); + + return c->component ?: ""; +} + +static int context_enable_feature( + Context *c, + const char *argv0, + bool enable, + char **features) { + + int r; + + assert(c); + assert(argv0); + + _cleanup_free_ char **l = NULL; + switch (c->feature_select) { + + case SELECT_EXPLICIT: + break; + + case SELECT_ALL: { + assert(!features); + + Feature *f; + HASHMAP_FOREACH(f, c->features) + if (strv_push(&l, f->id) < 0) + return log_oom(); + + features = l; + break; + } + + case SELECT_SUGGESTED: { + assert(!features); + + 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' shall be enabled: %m", f->id, context_component_display(c)); + if (!!r != !!enable) { + log_debug("Skipping feature '%s' of component '%s'.", f->id, context_component_display(c)); + continue; + } + + log_info("%s feature '%s' of component '%s'.", enable ? "Enabling" : "Disabling", f->id, context_component_display(c)); + + if (strv_push(&l, f->id) < 0) + return log_oom(); + } + + features = l; + break; + } + + default: + assert_not_reached(); + } + + if (strv_isempty(features)) { + log_debug("No features selected."); + return 0; + } + + _cleanup_free_ char *dropin_dir = NULL; + r = make_dropin_dir(c, &dropin_dir); + if (r < 0) + return r; + + int ret = 0; + STRV_FOREACH(name, features) { + if (!hashmap_contains(c->features, *name)) { + RET_GATHER(ret, log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Optional feature not found in component '%s': %s", context_component_display(c), *name)); + continue; + } + + _cleanup_free_ char *fname = strjoin(*name, ".feature"); + if (!fname) + return log_oom(); + + /* We assume that no sysadmin will name their config 50-systemd-sysupdate-enabled.conf */ + r = write_drop_in_format( + dropin_dir, + fname, + 50, + "systemd-sysupdate-enabled", + "# Generated via 'systemd-sysupdate %s'\n\n" + "[Feature]\n" + "Enabled=%s\n", + argv0, + yes_no(enable)); + if (r < 0) { + RET_GATHER(ret, log_error_errno(r, "Failed to write drop-in for feature '%s': %m", *name)); + continue; + } + + log_info("Feature '%s' %s.", *name, enabled_disabled(enable)); + } + + return ret; +} + +VERB(verb_enable_feature, "enable-feature", "FEATURE…", 1, VERB_ANY, 0, "Enable optional feature"); -VERB(verb_enable_feature, "disable-feature", "FEATURE…", 2, VERB_ANY, 0, +VERB(verb_enable_feature, "disable-feature", "FEATURE…", 1, VERB_ANY, 0, "Disable optional feature"); static int verb_enable_feature(int argc, char *argv[], uintptr_t _data, void *userdata) { bool enable = streq(argv[0], "enable-feature"); @@ -1987,8 +2143,24 @@ static int verb_enable_feature(int argc, char *argv[], uintptr_t _data, void *us if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (!IN_SET(context.component_select, SELECT_EXPLICIT, SELECT_ALL)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--component-suggested is not supported for '%s'.", argv[0]); + if (context.definitions) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "The --definitions= switch may not be combined with '%s'.", argv[0]); + + char **features; + if (argc > 1 && context.feature_select == SELECT_EXPLICIT) { + features = strv_skip(argv, 1); + + STRV_FOREACH(name, features) + if (!feature_name_valid(*name)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Feature name invalid: %s", *name); + + } else if (argc <= 1 && context.feature_select != SELECT_EXPLICIT) + features = NULL; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either specify features as positional parameter or via --feature-all/--feature-suggested, not both."); r = context_load_offline( &context, @@ -1997,57 +2169,72 @@ static int verb_enable_feature(int argc, char *argv[], uintptr_t _data, void *us if (r < 0) return r; - _cleanup_free_ char *dropin_dir = NULL; - r = make_dropin_dir(&context, &dropin_dir); - if (r < 0) - return r; + switch (context.component_select) { - STRV_FOREACH(name, strv_skip(argv, 1)) { + case SELECT_EXPLICIT: + return context_enable_feature(&context, argv[0], enable, features); - if (!feature_name_valid(*name)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Feature name invalid: %s", *name); - - if (!hashmap_contains(context.features, *name)) - return log_error_errno(SYNTHETIC_ERRNO(ENOENT), - "Optional feature not found: %s", *name); - - _cleanup_free_ char *fname = strjoin(*name, ".feature"); - if (!fname) - return log_oom(); - - /* We assume that no sysadmin will name their config 50-systemd-sysupdate-enabled.conf */ - r = write_drop_in_format( - dropin_dir, - fname, - 50, "systemd-sysupdate-enabled", - "# Generated via 'systemd-sysupdate %s'\n\n" - "[Feature]\n" - "Enabled=%s\n", - argv[0], - yes_no(enable)); + case SELECT_ALL: { + _cleanup_strv_free_ char **component_names = NULL; + bool has_default_component; + r = context_list_components(&context, &component_names, &has_default_component); if (r < 0) - return log_error_errno(r, "Failed to write drop-in for feature '%s': %m", *name); + return r; - log_info("Feature '%s' %s.", *name, enable ? "enabled" : "disabled"); + if (strv_isempty(component_names) && !has_default_component) { + log_debug("No components selected."); + return 0; + } + + int ret = 0; + if (has_default_component) + RET_GATHER(ret, context_enable_feature(&context, argv[0], enable, features)); + + STRV_FOREACH(name, component_names) { + _cleanup_(context_done) Context cc = CONTEXT_NULL; + + r = context_from_base_with_component(&context, *name, &cc); + if (r < 0) { + RET_GATHER(ret, r); + continue; + } + + r = context_load_offline( + &cc, + /* process_image_flags= */ 0, + /* read_definitions_flags= */ 0); + if (r < 0) { + RET_GATHER(ret, r); + continue; + } + + RET_GATHER(ret, context_enable_feature(&cc, argv[0], enable, features)); + } + + return ret; } - return 0; + default: + assert_not_reached(); + } } VERB_NOARG(verb_check_new, "check-new", "Check if there's a new version available"); static int verb_check_new(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(context_done) Context context = CONTEXT_NULL; int r; assert(argc <= 1); + _cleanup_(context_done) Context context = CONTEXT_NULL; r = context_from_cmdline(&context); if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); + if (context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); r = context_load_online( &context, @@ -2138,7 +2325,6 @@ typedef enum { } UpdateActionFlags; static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flags) { - _cleanup_(context_done) Context context = CONTEXT_NULL; _cleanup_free_ char *booted_version = NULL; UpdateSet *applied = NULL; const char *version; @@ -2147,16 +2333,19 @@ static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flag assert(argc <= 2); version = argc >= 2 ? argv[1] : NULL; + _cleanup_(context_done) Context context = CONTEXT_NULL; r = context_from_cmdline(&context); if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); + if (context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); if (context.instances_max < 2) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "The --instances-max argument must be >= 2 while updating"); + "The --instances-max= argument must be >= 2 while updating"); if (context.reboot) { /* If automatic reboot on completion is requested, let's first determine the currently booted image */ @@ -2265,12 +2454,14 @@ static int verb_vacuum(int argc, char *argv[], uintptr_t _data, void *userdata) if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); + if (context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); if (context.instances_max < 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "The --instances-max argument must be >= 1 while vacuuming"); + "The --instances-max= argument must be >= 1 while vacuuming"); r = context_load_offline( &context, @@ -2296,6 +2487,11 @@ static int verb_cleanup(int argc, char *argv[], uintptr_t _data, void *userdata) if (context.cleanup == 0) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invocation of 'cleanup' with --cleanup=no is contradictory, refusing."); + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); + if (!IN_SET(context.component_select, SELECT_EXPLICIT, SELECT_ALL)) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-suggested currently not supported for '%s'.", argv[0]); + r = context_load_offline( &context, /* process_image_flags= */ 0, @@ -2306,7 +2502,7 @@ static int verb_cleanup(int argc, char *argv[], uintptr_t _data, void *userdata) int ret = 0; RET_GATHER(ret, installdb_cleanup_component(&context)); - if (context.component_all) { + if (context.component_select == SELECT_ALL) { _cleanup_strv_free_ char **z = NULL; r = installdb_list_components(&context, &z); if (r < 0) @@ -2358,9 +2554,11 @@ static int verb_pending_or_reboot(int argc, char *argv[], uintptr_t _data, void return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --root=/--image= switches may not be combined with the '%s' operation.", argv[0]); - if (context.component || context.component_all) + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); + if (context.component || context.component_select != SELECT_EXPLICIT) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "The --component= and --component-all switches may not be combined with the '%s' operation, which only applies to the booted OS version.", argv[0]); + "The --component=, --component-all and --component-suggested switches may not be combined with the '%s' operation, which only applies to the booted OS version.", argv[0]); r = context_load_offline( &context, @@ -2435,22 +2633,41 @@ static int context_list_components(Context *context, char ***ret_component_names return 0; } +static int context_component_is_suggested(Context *c) { + assert(c); + + /* Only applies to components, not to the main system */ + if (!c->component) + return -ENOTTY; + + if (c->component_suggest >= 0) + return c->component_suggest; + + if (!c->component_suggest_on) /* no condition → false */ + return false; + + return condition_test_list(c->component_suggest_on, environ, suggest_on_type_to_string, /* logger= */ NULL, /* userdata= */ NULL); +} + VERB_NOARG(verb_components, "components", "Show list of components"); static int verb_components(int argc, char *argv[], uintptr_t _data, void *userdata) { - _cleanup_(context_done) Context context = CONTEXT_NULL; - _cleanup_strv_free_ char **component_names = NULL; - bool has_default_component = false; int r; assert(argc <= 1); + _cleanup_(context_done) Context context = CONTEXT_NULL; r = context_from_cmdline(&context); if (r < 0) return r; - if (context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all currently not supported for '%s'.", argv[0]); + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); + if (context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "--component-all/--component-suggested currently not supported for '%s'.", argv[0]); + if (context.definitions) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "The --definitions= switch may not be combined with '%s'.", argv[0]); r = context_load_offline( &context, @@ -2459,6 +2676,8 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda if (r < 0) return r; + _cleanup_strv_free_ char **component_names = NULL; + bool has_default_component = false; r = context_list_components(&context, &component_names, &has_default_component); if (r < 0) return log_error_errno(r, "Failed to enumerate components: %m"); @@ -2469,7 +2688,7 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda return 0; } - _cleanup_(table_unrefp) Table *t = table_new("", "component", "description", "documentation"); + _cleanup_(table_unrefp) Table *t = table_new("enabled", "suggested", "component", "description", "documentation"); if (!t) return log_oom(); @@ -2479,6 +2698,7 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda r = table_add_many( t, TABLE_EMPTY, + TABLE_EMPTY, TABLE_STRING, "", TABLE_SET_COLOR, ansi_highlight(), TABLE_STRING, "Default Component", @@ -2506,12 +2726,25 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda if (r < 0) continue; + r = table_add_many( + t, + TABLE_BOOLEAN_CHECKMARK, cc.component_enabled, + TABLE_SET_COLOR, ansi_highlight_green_red(cc.component_enabled)); + if (r < 0) + return table_log_add_error(r); + + r = context_component_is_suggested(&cc); + if (r < 0) + r = table_add_many(t, TABLE_EMPTY); + else + r = table_add_many(t, TABLE_BOOLEAN_CHECKMARK, r); + 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_BOOLEAN_CHECKMARK, cc.component_enabled, - TABLE_SET_COLOR, ansi_highlight_green_red(cc.component_enabled), TABLE_STRING, *i, TABLE_STRING, cc.component_description, TABLE_STRING, doc, @@ -2537,9 +2770,9 @@ static int verb_components(int argc, char *argv[], uintptr_t _data, void *userda return 0; } -VERB(verb_enable_component, "enable-component", "COMPONENT…", 2, VERB_ANY, 0, +VERB(verb_enable_component, "enable-component", "COMPONENT…", 1, VERB_ANY, 0, "Enable component"); -VERB(verb_enable_component, "disable-component", "COMPONENT…", 2, VERB_ANY, 0, +VERB(verb_enable_component, "disable-component", "COMPONENT…", 1, VERB_ANY, 0, "Disable component"); static int verb_enable_component(int argc, char *argv[], uintptr_t _data, void *userdata) { bool enable = streq(argv[0], "enable-component"); @@ -2550,15 +2783,35 @@ static int verb_enable_component(int argc, char *argv[], uintptr_t _data, void * if (r < 0) return r; - if (context.component || context.component_all) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "The --component= and --component-all switches may not be combined with '%s', " - "specify the component(s) as arguments instead.", argv[0]); - + if (context.feature_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--feature-all/--feature-suggested is not supported for '%s'.", argv[0]); if (context.definitions) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= switch may not be combined with '%s'.", argv[0]); + char **arguments, *array[2]; + if (argc > 1) { + if (context.component || context.component_select != SELECT_EXPLICIT) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either specify component names as positional parameter or via --component=/--component-all/--component-suggested, not both."); + + arguments = strv_skip(argv, 1); + + } else if (context.component_select == SELECT_EXPLICIT) { + if (!context.component) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No component specified."); + + array[0] = context.component; + array[1] = NULL; + arguments = array; + } else { + assert(!context.component); + arguments = NULL; + } + + STRV_FOREACH(name, arguments) + if (!component_name_valid(*name)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", *name); + r = context_load_offline( &context, /* process_image_flags= */ 0, @@ -2571,19 +2824,76 @@ static int verb_enable_component(int argc, char *argv[], uintptr_t _data, void * if (r < 0) return r; + _cleanup_strv_free_ char **suggested = NULL; + + switch (context.component_select) { + + case SELECT_EXPLICIT: + STRV_FOREACH(name, arguments) + if (!strv_contains(component_names, *name)) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Component not found: %s", *name); + break; + + case SELECT_ALL: + assert(!arguments); + arguments = component_names; + break; + + case SELECT_SUGGESTED: + assert(!arguments); + STRV_FOREACH(name, component_names) { + _cleanup_(context_done) Context cc = CONTEXT_NULL; + + r = context_from_base_with_component(&context, *name, &cc); + if (r < 0) + return r; + + r = context_load_offline( + &cc, + /* process_image_flags= */ 0, + /* read_definitions_flags= */ 0); + if (r < 0) + continue; + + r = context_component_is_suggested(&cc); + if (r < 0) { + log_warning_errno(r, "Failed to determine whether '%s' shall be enabled, skipping: %m", *name); + continue; + } + + /* This reconciles the system with the suggestions: on 'enable-component' we act on + * the components that are suggested, on 'disable-component' we act on the ones that + * are not. Hence pick the components whose suggestion state matches the operation. */ + if (!!r != !!enable) { + log_debug("Skipping '%s'.", *name); + continue; + } + + log_info("%s '%s'.", enable ? "Enabling" : "Disabling", *name); + + if (strv_extend(&suggested, *name) < 0) + return log_oom(); + } + + arguments = suggested; + break; + + default: + assert_not_reached(); + } + + if (strv_isempty(arguments)) { + log_info("No components selected."); + return 0; + } + /* Component definition files live directly below the configuration directories, hence the drop-in * goes right next to them below /etc. */ _cleanup_free_ char *dropin_dir = path_join(context.root, SYSCONF_DIR); if (!dropin_dir) return log_oom(); - STRV_FOREACH(name, strv_skip(argv, 1)) { - if (!component_name_valid(*name)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", *name); - - if (!strv_contains(component_names, *name)) - return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Component not found: %s", *name); - + STRV_FOREACH(name, arguments) { _cleanup_free_ char *fname = strjoin("sysupdate.", *name, ".component"); if (!fname) return log_oom(); @@ -2672,7 +2982,7 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { "Select component to update"): if (isempty(opts.arg)) { arg_component = mfree(arg_component); - arg_component_all = false; + arg_component_select = SELECT_EXPLICIT; break; } @@ -2683,13 +2993,25 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { if (r < 0) return r; - arg_component_all = false; + arg_component_select = SELECT_EXPLICIT; break; - OPTION('A', "component-all", NULL, "Process all components"): - + OPTION('A', "component-all", NULL, "Select all components"): arg_component = mfree(arg_component); - arg_component_all = true; + arg_component_select = SELECT_ALL; + break; + + OPTION('S', "component-suggested", NULL, "Select all suggested components"): + arg_component = mfree(arg_component); + arg_component_select = SELECT_SUGGESTED; + break; + + OPTION('a', "feature-all", NULL, "Select all features"): + arg_feature_select = SELECT_ALL; + break; + + OPTION('s', "feature-suggested", NULL, "Select all suggested features"): + arg_feature_select = SELECT_SUGGESTED; break; OPTION_LONG("definitions", "DIR", @@ -2803,11 +3125,13 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= switch may not be combined with --root= or --image=."); } - if (arg_reboot && arg_component) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --component=, as automatic reboots only apply to the booted OS version."); + if ((arg_component || arg_component_select != SELECT_EXPLICIT)) { + if (arg_reboot) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --reboot switch may not be combined with --component=/--component-all/--component-suggested, as automatic reboots only apply to the booted OS version."); - if (arg_definitions && arg_component) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined."); + if (arg_definitions) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component=/--component-all/--component-suggested switches may not be combined."); + } r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT); if (r < 0) @@ -2850,10 +3174,16 @@ static int vl_server(void) { static int run(int argc, char *argv[]) { int r; + LIBAPPARMOR_NOTE(recommended); + LIBAUDIT_NOTE(recommended); LIBBLKID_NOTE(recommended); LIBCRYPTO_NOTE(suggested); LIBCRYPTSETUP_NOTE(suggested); LIBMOUNT_NOTE(recommended); + LIBSELINUX_NOTE(recommended); + LIBTSS2_ESYS_NOTE(suggested); + LIBTSS2_MU_NOTE(suggested); + LIBTSS2_RC_NOTE(suggested); log_setup(); diff --git a/src/sysupdate/sysupdate.h b/src/sysupdate/sysupdate.h index 6088bde669..1930367093 100644 --- a/src/sysupdate/sysupdate.h +++ b/src/sysupdate/sysupdate.h @@ -4,6 +4,14 @@ #include "sysupdate-forward.h" #include "sysupdate-target.h" +typedef enum SelectMode { + SELECT_EXPLICIT, /* Only explicitly specified features/components */ + SELECT_ALL, /* All available features/components */ + SELECT_SUGGESTED, /* All suggested features/components */ + _SELECT_MODE_MAX, + _SELECT_MODE_INVALID = -EINVAL, +} SelectMode; + typedef struct Context { /* Parameters/Command line arguments: */ char *definitions; @@ -14,7 +22,8 @@ typedef struct Context { bool reboot; int cleanup; char *component; - bool component_all; + SelectMode component_select; + SelectMode feature_select; int verify; ImagePolicy *image_policy; bool offline; @@ -28,6 +37,9 @@ typedef struct Context { char **component_documentation; bool component_enabled; + int component_suggest; + Condition *component_suggest_on; + Transfer **transfers; size_t n_transfers;