sysupdate: add "suggestion" concept to feature and component enablement (#42970)

This commit is contained in:
Luca Boccassi
2026-07-13 19:07:50 +01:00
committed by GitHub
8 changed files with 614 additions and 102 deletions
+47
View File
@@ -109,6 +109,53 @@
<xi:include href="version-info.xml" xpointer="v257"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>Suggest=</varname></term>
<listitem><para>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 <option>--feature-suggested</option> switch of
<citerefentry><refentrytitle>systemd-sysupdate</refentrytitle><manvolnum>8</manvolnum></citerefentry>)
so that the system administrator or higher-level tooling may act on it.</para>
<para>If this setting is not specified, the <varname>SuggestOn…=</varname> conditions described below
are evaluated instead to determine whether the feature is suggested. If neither this setting nor any
<varname>SuggestOn…=</varname> condition is specified, the feature is not suggested.</para>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>SuggestOnArchitecture=</varname></term>
<term><varname>SuggestOnFirmware=</varname></term>
<term><varname>SuggestOnVirtualization=</varname></term>
<term><varname>SuggestOnHost=</varname></term>
<term><varname>SuggestOnFraction=</varname></term>
<term><varname>SuggestOnKernelCommandLine=</varname></term>
<term><varname>SuggestOnVersion=</varname></term>
<term><varname>SuggestOnCredential=</varname></term>
<term><varname>SuggestOnSecurity=</varname></term>
<term><varname>SuggestOnOSRelease=</varname></term>
<term><varname>SuggestOnMachineTag=</varname></term>
<listitem><para>Suggest this feature for enablement depending on system properties. These settings
take the same arguments and implement the same semantics — including the leading <literal>!</literal>
for negation — as the identically-named
<varname>ConditionArchitecture=</varname>, <varname>ConditionFirmware=</varname>,
<varname>ConditionVirtualization=</varname>, <varname>ConditionHost=</varname>,
<varname>ConditionFraction=</varname>, <varname>ConditionKernelCommandLine=</varname>,
<varname>ConditionVersion=</varname>, <varname>ConditionCredential=</varname>,
<varname>ConditionSecurity=</varname>, <varname>ConditionOSRelease=</varname> and
<varname>ConditionMachineTag=</varname> settings for unit files, which are documented in
<citerefentry><refentrytitle>systemd.unit</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
The feature is suggested if all specified conditions apply. As with the <varname>Condition…=</varname>
settings, assigning an empty string to one of these resets the list.</para>
<para>These conditions are only evaluated if <varname>Suggest=</varname> is not specified.</para>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
+1
View File
@@ -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;
+73
View File
@@ -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;
}
+4
View File
@@ -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);
+53 -13
View File
@@ -1,12 +1,16 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <unistd.h>
#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);
}
+5
View File
@@ -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);
+418 -88
View File
@@ -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 ?: "<default>";
}
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, "<default>",
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();
+13 -1
View File
@@ -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;