diff --git a/man/systemd-sysupdate.xml b/man/systemd-sysupdate.xml index 778ff2e793e..cfc1c593077 100644 --- a/man/systemd-sysupdate.xml +++ b/man/systemd-sysupdate.xml @@ -190,6 +190,17 @@ + + + + Lists streams that can be updated. This enumerates the + /var/cache/systemd/sysupdate@*.d/ and /usr/lib/sysupdate@*.d/ + directories that contain transfer definitions. This command is useful to list possible parameters + for (see below). + + + + @@ -225,7 +236,8 @@ updated together in a synchronous fashion. Simply define multiple transfer files within the same sysupdate.d/ directory for these cases. - This option may not be combined with . + This option may not be combined with or + . @@ -237,11 +249,29 @@ are read from this directory instead of /usr/lib/sysupdate.d/*.conf, /etc/sysupdate.d/*.conf, and /run/sysupdate.d/*.conf. - This option may not be combined with . + This option may not be combined with or + . + + + + Selects the update stream to use. Takes a stream name as argument. This alters + the search logic for transfer definitions to look in + /usr/lib/sysupdate@stream.d/ and + /var/cache/systemd/sysupdate@stream.d/ instead of + /usr/lib/sysupdate.d. + Note that administrator-controlled directories (i.e. /etc/sysupdate.d/, etc) are + still loaded as usual. + + This option may not be combined with or + . + + + + diff --git a/man/sysupdate.d.xml b/man/sysupdate.d.xml index df3aaf7f387..8e3b9c047aa 100644 --- a/man/sysupdate.d.xml +++ b/man/sysupdate.d.xml @@ -65,6 +65,52 @@ sysupdate.features5. + Sometimes, distributions need to update certain parts of themselves independently from the normal + update cycle. + For example, the UEFI Shim loader (necessary for + UEFI Secure Boot support in many cases) has its own release cycle, requires code signatures from a + third-party, and in general is not tied to a distribution's update cycle. + Support for this scenario is provided by "components", which allow distributions to define transfer + definitions that receive updates independently from the base OS. + Components are defined in /usr/lib/sysupdate.component.d/, + and have corresponding override directories for administrators (i.e. + /etc/sysupdate.component.d/, etc). + + Some distributions may wish to maintain multiple update "streams" at a time, for example to offer + a beta/nightly update channel, or to distribute security updates to multiple major versions at a time. + Users of such distributions may wish to remain on their current stream, and switch streams at some future + point in time. + A distribution with multiple update streams should ship the transfer definitions for each stream in the + /usr/lib/sysupdate@stream.d/ or + /var/cache/systemd/sysupdate@stream.d/ directories. + For example, a distribution with multiple stable branches can ship the next major release's transfer + definitions in the current release's + /usr/lib/sysupdate@foobarOS-next.d/ directory, and users + can switch to it by updating the foobarOS-next stream. + How exactly these stream definition directories are delivered is up to distributions: they can stabilize + transfer definitions a version in advance and ship the stream definitions from day one, or they can ship + these files as part of a regular security patch that users will install anyway, or they can use a + component as described above to update the stream definitions under /var/cache/ + independently from the host system. + Note that the presence of a stream definition directory does not imply the availability of an upgrade on + that stream; it just defines where to look and if an update is found on the remote how to install it. + Also note that the normal administrator override files (i.e. transfer definitions, feature definitions, + or drop-ins found in /etc/sysupdate.d/, /run/sysupdate.d/, etc) + are applied over top of the definitions found in the stream definition directory. + This is done because a the stream definition directory turns into the normal definition directory + (/usr/lib/sysupdate.d/) when that stream is switched to. + + System Administrators must take extreme care when overriding any transfer or + optional feature definitions, other than to turn on or off features! + As with any configuration defined in /usr and overridden in + /etc, an update to the host system can break the administrator overrides. + However, systemd-sysupdate is uniquely destructive: a broken configuration could + prevent the system from updating (best case), or completely destroy an installation by wiping the wrong + partition. + Distributions must take care to avoid breaking systems where overrides exist only to turn on or off + optional features; supporting (or choosing not to) everything else is up to distribution policy. + You have been warned. + Each *.transfer file contains three sections: [Transfer], [Source] and [Target]. diff --git a/src/basic/constants.h b/src/basic/constants.h index 93e6efbdcae..871ea05e5ed 100644 --- a/src/basic/constants.h +++ b/src/basic/constants.h @@ -64,12 +64,18 @@ "/usr/local/lib/" n "\0" \ "/usr/lib/" n "\0" -#define CONF_PATHS(n) \ +#define CONF_PATHS_ADMIN(n) \ "/etc/" n, \ - "/run/" n, \ + "/run/" n + +#define CONF_PATHS_SYSTEM(n) \ "/usr/local/lib/" n, \ "/usr/lib/" n +#define CONF_PATHS(n) \ + CONF_PATHS_ADMIN(n), \ + CONF_PATHS_SYSTEM(n) + #define CONF_PATHS_STRV(n) \ STRV_MAKE(CONF_PATHS(n)) diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index 9ced67429ac..f56cc04ca72 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -47,6 +47,7 @@ char *arg_root = NULL; static char *arg_image = NULL; static bool arg_reboot = false; static char *arg_component = NULL; +static char *arg_stream = NULL; static int arg_verify = -1; static ImagePolicy *arg_image_policy = NULL; static bool arg_offline = false; @@ -56,6 +57,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_definitions, freep); STATIC_DESTRUCTOR_REGISTER(arg_root, freep); STATIC_DESTRUCTOR_REGISTER(arg_image, freep); STATIC_DESTRUCTOR_REGISTER(arg_component, freep); +STATIC_DESTRUCTOR_REGISTER(arg_stream, freep); STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_transfer_source, freep); @@ -180,7 +182,54 @@ static int context_read_definitions(Context *c, const char* node, bool requires_ if (arg_definitions) dirs = strv_new(arg_definitions); - else if (arg_component) { + else if (arg_stream) { + /* Ultimately we end up with a search path along the lines of: /etc/sysupdate.d/, + * /run/sysupdate.d/, /var/cache/systemd/sysupdate@.d, /usr/lib/sysupdate@.d. + * This is very unusual! It seems wrong! But this is the correct behavior. When a + * `systemd-sysupdate --stream=` update is completed, /usr/lib/sysupdate@.d (or its + * /var/cache alternative) turns into /usr/lib/sysupdate.d, but the admin overrides remain + * untouched. So if we did this any differently, we'd end up in a situation where the admin's + * settings are ignored when first installing a major upgrade but then suddenly considered + * again once the update is completed. In my opinion, that behavior would be more unexpected + * and dangerous than what is implemented here! + * + * Is this a big and surprising footgun for the admin? Yes. But frankly, so is overriding + * anything relating to sysupdate. If an admin has overrides that do anything other than + * turning on/off optional features, they've already aimed a ballistic missile at their + * installation. It'll detonate either immediately when trying to switch streams (as + * implemented now), or when updating to the first patch of the new stream (the alternative); + * the installation is doomed either way. And failing immediately during a major OS upgrade + * seems a lot more preferable, and something that admins will be more prepared for, than a + * subsequent security patch suddenly bricking installations. */ + + char **admin = STRV_MAKE(CONF_PATHS_ADMIN("sysupdate.d")); + char **system = STRV_MAKE("/var/cache/systemd/", CONF_PATHS_SYSTEM("")); + size_t i = 0; + + dirs = new0(char*, strv_length(admin) + strv_length(system) + 1); + if (!dirs) + return log_oom(); + + STRV_FOREACH(dir, admin) { + char *d; + + d = strdup(*dir); + if (!d) + return log_oom(); + + dirs[i++] = d; + } + + STRV_FOREACH(dir, system) { + char *j; + + j = strjoin(*dir, "sysupdate@", arg_stream, ".d"); + if (!j) + return log_oom(); + + dirs[i++] = j; + } + } else if (arg_component) { char **l = CONF_PATHS_STRV(""); size_t i = 0; @@ -247,6 +296,11 @@ static int context_read_definitions(Context *c, const char* node, bool requires_ "No transfer definitions for component '%s' found.", arg_component); + if (arg_stream) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), + "No transfer definitions for stream '%s' found.", + arg_stream); + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No transfer definitions found."); } @@ -1532,22 +1586,45 @@ static int component_name_valid(const char *c) { return filename_is_valid(j); } -static int verb_components(int argc, char **argv, void *userdata) { +static int stream_name_valid(const char *s) { + _cleanup_free_ char *j = NULL; + + /* See if the specified string enclosed in the directory prefix+suffix would be a valid file name */ + + if (isempty(s)) + return false; + + if (string_has_cc(s, NULL)) + return false; + + if (!utf8_is_valid(s)) + return false; + + j = strjoin("sysupdate@", s, ".d"); + if (!j) + return -ENOMEM; + + return filename_is_valid(j); +} + +static int walk_search_paths(char **paths, bool component, char ***ret, bool *ret_has_default) { _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; _cleanup_(umount_and_rmdir_and_freep) char *mounted_dir = NULL; _cleanup_set_free_ Set *names = NULL; - _cleanup_free_ char **z = NULL; /* We use simple free() rather than strv_free() here, since set_free() will free the strings for us */ - char **l = CONF_PATHS_STRV(""); - bool has_default_component = false; + _cleanup_free_ char **names_strv = NULL; /* free() b/c the set still owns the values */ + _cleanup_strv_free_ char **names_dup = NULL; + bool has_default = false; int r; - assert(argc <= 1); + assert(paths); + assert(ret); + assert(ret_has_default); r = process_image(/* ro= */ false, &mounted_dir, &loop_device); if (r < 0) return r; - STRV_FOREACH(i, l) { + STRV_FOREACH(i, paths) { _cleanup_closedir_ DIR *d = NULL; _cleanup_free_ char *p = NULL; @@ -1577,11 +1654,11 @@ static int verb_components(int argc, char **argv, void *userdata) { continue; if (streq(de->d_name, "sysupdate.d")) { - has_default_component = true; + has_default = true; continue; } - e = startswith(de->d_name, "sysupdate."); + e = startswith(de->d_name, component ? "sysupdate." : "sysupdate@"); if (!e) continue; @@ -1593,26 +1670,51 @@ static int verb_components(int argc, char **argv, void *userdata) { if (!n) return log_oom(); - r = component_name_valid(n); + if (component) + r = component_name_valid(n); + else + r = stream_name_valid(n); if (r < 0) - return log_error_errno(r, "Unable to validate component name: %m"); + return log_error_errno(r, "Unable to validate %s name: %m", + component ? "component" : "stream"); if (r == 0) continue; r = set_ensure_consume(&names, &string_hash_ops_free, TAKE_PTR(n)); if (r < 0 && r != -EEXIST) - return log_error_errno(r, "Failed to add component to set: %m"); + return log_error_errno(r, "Failed to add %s to set: %m", + component ? "component" : "stream"); } } - z = set_get_strv(names); - if (!z) + names_strv = set_get_strv(names); + if (!names_strv) return log_oom(); - strv_sort(z); + names_dup = strv_copy(names_strv); + if (!names_dup) + return log_oom(); + + strv_sort(names_dup); + + *ret = TAKE_PTR(names_dup); + *ret_has_default = has_default; + return 0; +} + +static int verb_components(int argc, char **argv, void *userdata) { + _cleanup_strv_free_ char **names = NULL; + bool has_default_component = false; + int r; + + assert(argc <= 1); + + r = walk_search_paths(CONF_PATHS_STRV(""), true, &names, &has_default_component); + if (r < 0) + return r; if (!sd_json_format_enabled(arg_json_format_flags)) { - if (!has_default_component && set_isempty(names)) { + if (!has_default_component && strv_isempty(names)) { log_info("No components defined."); return 0; } @@ -1621,13 +1723,53 @@ static int verb_components(int argc, char **argv, void *userdata) { printf("%s%s\n", ansi_highlight(), ansi_normal()); - STRV_FOREACH(i, z) + STRV_FOREACH(i, names) puts(*i); } 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", z)); + SD_JSON_BUILD_PAIR_STRV("components", names)); + if (r < 0) + return log_error_errno(r, "Failed to create JSON: %m"); + + 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; +} + +static int verb_streams(int argc, char **argv, void *userdata) { + char **dirs = STRV_MAKE("/var/cache/systemd/", CONF_PATHS_SYSTEM("")); + _cleanup_strv_free_ char **names = NULL; + bool has_default_stream = false; + int r; + + assert(argc <= 1); + + r = walk_search_paths(dirs, false, &names, &has_default_stream); + if (r < 0) + return r; + + if (FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) { + if (!has_default_stream && strv_isempty(names)) { + log_info("No streams defined."); + return 0; + } + + if (has_default_stream) + printf("%s%s\n", + ansi_highlight(), ansi_normal()); + + STRV_FOREACH(i, names) + puts(*i); + } else { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL; + + r = sd_json_buildo(&json, SD_JSON_BUILD_PAIR_BOOLEAN("default", has_default_stream), + SD_JSON_BUILD_PAIR_STRV("streams", names)); if (r < 0) return log_error_errno(r, "Failed to create JSON: %m"); @@ -1659,11 +1801,13 @@ static int verb_help(int argc, char **argv, void *userdata) { " currently booted\n" " reboot Reboot if a newer version is installed than booted\n" " components Show list of components\n" + " streams Show list of streams\n" " -h --help Show this help\n" " --version Show package version\n" "\n%3$sOptions:%4$s\n" " -C --component=NAME Select component to update\n" " --definitions=DIR Find transfer definitions in specified directory\n" + " --stream=STREAM Select stream to switch to\n" " --root=PATH Operate on an alternate filesystem root\n" " --image=PATH Operate on disk image as filesystem root\n" " --image-policy=POLICY\n" @@ -1698,6 +1842,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_NO_LEGEND, ARG_SYNC, ARG_DEFINITIONS, + ARG_STREAM, ARG_JSON, ARG_ROOT, ARG_IMAGE, @@ -1714,6 +1859,7 @@ static int parse_argv(int argc, char *argv[]) { { "no-pager", no_argument, NULL, ARG_NO_PAGER }, { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, { "definitions", required_argument, NULL, ARG_DEFINITIONS }, + { "stream", required_argument, NULL, ARG_STREAM }, { "instances-max", required_argument, NULL, 'm' }, { "sync", required_argument, NULL, ARG_SYNC }, { "json", required_argument, NULL, ARG_JSON }, @@ -1770,6 +1916,24 @@ static int parse_argv(int argc, char *argv[]) { return r; break; + case ARG_STREAM: + if (isempty(optarg)) { + arg_stream = mfree(arg_stream); + break; + } + + r = stream_name_valid(optarg); + if (r < 0) + return log_error_errno(r, "Failed to determine if stream name is valid: %m"); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Stream name invalid: %s", optarg); + + r = free_and_strdup_warn(&arg_stream, optarg); + if (r < 0) + return r; + + break; + case ARG_JSON: r = parse_json_argument(optarg, &arg_json_format_flags); if (r <= 0) @@ -1856,6 +2020,12 @@ static int parse_argv(int argc, char *argv[]) { if (arg_definitions && arg_component) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --component= switches may not be combined."); + if (arg_definitions && arg_stream) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --definitions= and --stream= switches may not be combined."); + + if (arg_component && arg_stream) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "The --component= and --stream= switches may not be combined."); + return 1; } @@ -1864,6 +2034,7 @@ static int sysupdate_main(int argc, char *argv[]) { static const Verb verbs[] = { { "list", VERB_ANY, 2, VERB_DEFAULT, verb_list }, { "components", VERB_ANY, 1, 0, verb_components }, + { "streams", VERB_ANY, 1, 0, verb_streams }, { "features", VERB_ANY, 2, 0, verb_features }, { "check-new", VERB_ANY, 1, 0, verb_check_new }, { "update", VERB_ANY, 2, 0, verb_update },