diff --git a/TODO.md b/TODO.md index b216feb8218..257fd32d127 100644 --- a/TODO.md +++ b/TODO.md @@ -255,15 +255,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later - sysupdate: go through all components, and update them all, one by one. -- sysupdate: add concept for enabling/disabling specific components explicitly, - just like features. - -- sysupdate: add conditions to transfer files, copying what we have for unit - files and .network files - -- pid1,sysupdate,network: add support for a new "tags" condition, that checks - all of the above. - - pcrextend: we probably should measure /etc/machine-info during boot somehow - pcrextend: we should measure something when we enter developer mode, by some @@ -2657,7 +2648,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later - download multiple arbitrary patterns from same source - SHA256SUMS format with bearer tokens for each resource to download - decrypt SHA256SUMS with key from tpm - - clean up stuff on disk that disappears from SHA256SUMS - turn http backend stuff int plugin via varlink - for each transfer support looking at multiple sources, pick source with newest entry. If multiple sources have the same entry, use diff --git a/man/rules/meson.build b/man/rules/meson.build index 486f565815f..eac4465769b 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1277,7 +1277,8 @@ manpages = [ ''], ['systemd-sysupdate', '8', - ['systemd-sysupdate-reboot.service', + ['systemd-sysupdate-auto-enable.service', + 'systemd-sysupdate-reboot.service', 'systemd-sysupdate-reboot.timer', 'systemd-sysupdate-update.service', 'systemd-sysupdate-update.timer', @@ -1378,6 +1379,7 @@ manpages = [ ['systemd.timer', '5', [], ''], ['systemd.unit', '5', [], ''], ['systemd.v', '7', [], ''], + ['sysupdate.components', '5', [], 'ENABLE_SYSUPDATE'], ['sysupdate.d', '5', [], 'ENABLE_SYSUPDATE'], ['sysupdate.features', '5', [], 'ENABLE_SYSUPDATE'], ['sysusers.d', '5', [], 'ENABLE_SYSUSERS'], diff --git a/man/systemd-sysupdate.xml b/man/systemd-sysupdate.xml index 2855c650c88..7386a399e7c 100644 --- a/man/systemd-sysupdate.xml +++ b/man/systemd-sysupdate.xml @@ -22,6 +22,7 @@ systemd-sysupdate-update.timer systemd-sysupdate-reboot.service systemd-sysupdate-reboot.timer + systemd-sysupdate-auto-enable.service systemd-sysupdate.service @@ -87,6 +88,12 @@ appropriate time (i.e. typically at night). The two sets of service/timer units may be enabled separately. + The systemd-sysupdate-auto-enable.service system service automatically enables all + suggested components and all features of the enabled components before each update. It is ordered before and + pulled in by systemd-sysupdate-update.service, and is not enabled by default. Enable it + if newly suggested components and features shall be picked up automatically as they become available, rather + than requiring manual enable-component/enable-feature invocations. + For details about transfer files and examples see sysupdate.d5. @@ -121,6 +128,21 @@ + + FEATURE + FEATURE + + Enables or disables the specified optional features. This writes an + Enabled= setting into a drop-in next to the feature definition, see + sysupdate.features5 + for details. The features may be specified either as arguments or selected via the + or switches. Note that this only + changes the configuration: to actually download the newly enabled transfers, or remove the disabled + ones, invoke or afterwards. + + + + @@ -209,6 +231,18 @@ + + COMPONENT + COMPONENT + + Enables or disables the specified components. This writes an + Enabled= setting into a drop-in next to the component definition. The components + may be specified either as arguments or selected via the , + or switches. + + + + @@ -280,15 +314,57 @@ - Instead of operating on a single component, operate on all known components (as well as - the default, component-less installation). This is currently only supported for the - cleanup command; all other commands will fail if this switch is used. + Instead of operating on a single component, operate on all known components (as well + as the default, component-less installation). This is supported for the update, + acquire, cleanup, enable-feature, + disable-feature, enable-component and + disable-component commands; all other commands will fail if this switch is + used. This option may not be combined with . + + + + + Similar to , but operates only on the components that + are currently suggested for this system, as determined by the + Suggest= and SuggestOn…= settings in the component definition + files. This is supported for the enable-component and + disable-component commands. + + This option may not be combined with . + + + + + + + + + Instead of operating on features specified on the command line, operate on all + optional features known to the selected component(s). This is supported for the + enable-feature and disable-feature commands. + + + + + + + + + Similar to , but operates only on the optional features + that are currently suggested for this system, as determined by the + Suggest= and SuggestOn…= settings in the feature definition + files. This is supported for the enable-feature and + disable-feature commands. + + + + @@ -416,6 +492,8 @@ systemd1 sysupdate.d5 + sysupdate.features5 + sysupdate.components5 systemd-sysupdated.service8 systemd-repart8 diff --git a/man/sysupdate.components.xml b/man/sysupdate.components.xml new file mode 100644 index 00000000000..54cec98c7c8 --- /dev/null +++ b/man/sysupdate.components.xml @@ -0,0 +1,292 @@ + + + + + + + + sysupdate.components + systemd + + + + sysupdate.components + 5 + + + + sysupdate.components + Definition Files for Update Components + + + + + /etc/sysupdate.component.component + /run/sysupdate.component.component + /usr/local/lib/sysupdate.component.component + /usr/lib/sysupdate.component.component + + + + + Description + + A "Component" is a set of + sysupdate.d5 + transfer definitions that + systemd-sysupdate8 + updates independently from the rest of the OS. + Each component is a self-contained installation with its own version, and its own set of transfer files, + stored in a directory named after the component: + sysupdate.component.d/*.transfer (as opposed to the + default, component-less sysupdate.d/*.transfer). + Components are selected via the , or + switches of + systemd-sysupdate8, and + enumerated by its components command. + + Because components are updated separately, they are the right tool for OS resources that shall be + versioned and updated independently from the base OS — for example a container image or a portable service + image that follows its own release cycle. This is the primary difference between a component and an + sysupdate.features5 + Optional Feature: the transfers belonging to a feature are updated in lock-step with the target they are + part of, while a component is a wholly separate installation. Do not use a component for resources that + shall always be updated synchronously with something else; use a feature (or simply additional transfer + files in the same directory) for those cases. + + A component does not require a *.component file: as soon as a matching + sysupdate.component.d/ directory with transfer definitions + exists, the component is available for updating. The *.component file described here + is optional, and carries metadata about the component (such as a human-readable description and a + documentation URL) as well as its enablement state. + + When a component is disabled (via Enabled=false, see below), operations that + would download or install new versions — most importantly update — refuse to act on + it. Other operations, such as vacuum and cleanup, continue to + consider the component's transfers so that previously installed instances remain managed and can be + cleaned up. Components are enabled or disabled with the enable-component and + disable-component commands of + systemd-sysupdate8, + which write an Enabled= setting into a drop-in next to the component definition. + + Component definitions support drop-in files, which are most commonly used to override the + Enabled= setting. Drop-ins are read from + sysupdate.component.component.d/*.conf in the same set of + directories as the main file. They can also be masked to hide the component definition entirely. + + Each *.component file contains one section: [Component]. + + + + [Component] Section Options + + This section defines general properties of this component. + + + + Description= + + A short human-readable description of this component. + This may be used as a label for this component, so the string should meaningfully identify the + component among the components available on the system. + + + + + + Documentation= + + A user-presentable URL to documentation about this component. + This setting supports specifier expansion; see below for details on supported specifiers. + + + + + + Enabled= + + Whether or not this component is enabled. If unspecified, the component is enabled + by default. When disabled, systemd-sysupdate refuses to download or install new + versions of this component, but continues to consider its transfers while vacuuming and in other + situations where it needs to determine ownership over previously downloaded system resources. + + + + + + Suggest= + + Takes a boolean argument. If true, this component is suggested for enablement, if + false it is not. This does not enable the component 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 component is suggested. If neither this setting nor any + SuggestOn…= condition is specified, the component is not suggested. + + + + + + SuggestOnArchitecture= + SuggestOnFirmware= + SuggestOnVirtualization= + SuggestOnHost= + SuggestOnFraction= + SuggestOnKernelCommandLine= + SuggestOnVersion= + SuggestOnCredential= + SuggestOnSecurity= + SuggestOnOSRelease= + SuggestOnMachineTag= + + Suggest this component 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 component 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. + + + + + + + + Specifiers + + Specifiers may be used in the Documentation= setting. The following expansions + are understood: + + + Specifiers available + + + + + + + Specifier + Meaning + Details + + + + + + + + + + + + + + + + + + + + +
+
+ + + Examples + + + A Separately Updated Container Image + + We'll use the hypothetical "foobarOS" described in + sysupdate.d5 as our + example base OS. + foobarOS ships a container image that many, but not all, deployments want to run. The image has its own + release cadence and version number, entirely separate from the base OS, so it is a poor fit for an + Optional Feature (which would be version-locked to the OS). Instead, let's model it as a component named + webapp. + + First, the component definition, carrying its metadata and marking it as disabled by default: + + # /usr/lib/sysupdate.webapp.component +[Component] +Description=WebApp Container Image +Documentation=https://docs.example.com/foobarOS/webapp +Enabled=false + + + Next, the transfer definition for the component, placed in the component's own directory + sysupdate.webapp.d/: + + # /usr/lib/sysupdate.webapp.d/50-webapp.transfer +[Source] +Type=url-file +Path=https://download.example.com/ +MatchPattern=webapp_@v.raw.xz + +[Target] +Type=regular-file +Path=/var/lib/machines +MatchPattern=webapp_@v.raw +Mode=0444 +InstancesMax=2 + + + With these two files, updates for the webapp component can be checked and + downloaded independently of the base OS, for example with + systemd-sysupdate --component=webapp update. Because + Enabled=false was set, the administrator must first opt in with + systemd-sysupdate enable-component webapp, which writes the following drop-in: + + # /etc/sysupdate.webapp.component.d/50-systemd-sysupdate-enabled.conf +# Generated via 'systemd-sysupdate enable-component' + +[Component] +Enabled=true + + + + + A Component Suggested Only on Bare Metal + + Suppose foobarOS ships a component with firmware and microcode updates that is only relevant when + running on physical hardware, not inside a virtual machine. We can use SuggestOn…= to + hint that the component should be enabled in that case, without enabling it automatically: + + # /usr/lib/sysupdate.firmware.component +[Component] +Description=Firmware and Microcode Updates +Documentation=https://docs.example.com/foobarOS/firmware +Enabled=false +SuggestOnVirtualization=!vm + + + Higher-level tooling can now query the suggested components (via + systemd-sysupdate --component-suggested …) and enable them as appropriate. On bare + metal the firmware component is suggested; inside a VM it is not. + + + + + See Also + + systemd1 + systemd-sysupdate8 + sysupdate.d5 + sysupdate.features5 + + + +
diff --git a/man/sysupdate.d.xml b/man/sysupdate.d.xml index 29daeda81a0..9c27bf444a0 100644 --- a/man/sysupdate.d.xml +++ b/man/sysupdate.d.xml @@ -1068,6 +1068,8 @@ CurrentSymlink=myContainer systemd1 systemd-sysupdate8 + sysupdate.features5 + sysupdate.components5 systemd-repart8 diff --git a/man/sysupdate.features.xml b/man/sysupdate.features.xml index d3a7a638101..29f067f61ba 100644 --- a/man/sysupdate.features.xml +++ b/man/sysupdate.features.xml @@ -381,6 +381,7 @@ InstancesMax=2 systemd1 systemd-sysupdate8 sysupdate.d5 + sysupdate.components5 diff --git a/shell-completion/bash/meson.build b/shell-completion/bash/meson.build index cddf742059d..863ceb693bb 100644 --- a/shell-completion/bash/meson.build +++ b/shell-completion/bash/meson.build @@ -55,6 +55,7 @@ foreach item : [ ['systemd-run', ''], ['systemd-sysext', 'ENABLE_SYSEXT'], ['systemd-sysinstall', 'ENABLE_SYSINSTALL'], + ['systemd-sysupdate', 'ENABLE_SYSUPDATE'], ['systemd-vmspawn', 'ENABLE_VMSPAWN'], ['systemd-vpick', ''], ['timedatectl', 'ENABLE_TIMEDATED'], diff --git a/shell-completion/bash/systemd-sysupdate b/shell-completion/bash/systemd-sysupdate new file mode 100644 index 00000000000..fb5f5344251 --- /dev/null +++ b/shell-completion/bash/systemd-sysupdate @@ -0,0 +1,138 @@ +# shellcheck shell=bash +# systemd-sysupdate(8) completion -*- shell-script -*- +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# systemd is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with systemd; If not, see . + +__contains_word() { + local w word=$1; shift + for w in "$@"; do + [[ $w = "$word" ]] && return + done +} + +# List the components or features known to the installation. The argument is both +# the verb to invoke ('components' or 'features') and the JSON array key to extract. +__systemd_sysupdate_list() { + systemd-sysupdate --no-pager --json=short "$1" 2>/dev/null | + sed -n "s/.*\"$1\":\[\([^]]*\)\].*/\1/p" | tr ',' '\n' | tr -d '"' +} + +_systemd-sysupdate() { + local i verb comps + local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} words cword + local -A OPTS=( + [STANDALONE]='-h --help --version + --no-pager + --no-legend + -A --component-all + -S --component-suggested + -a --feature-all + -s --feature-suggested + --reboot + --offline' + [ARG]='-C --component + --definitions + --root + --image + --image-policy + --transfer-source + -m --instances-max + --sync + --verify + --cleanup + --json' + ) + + local -A VERBS=( + [STANDALONE]='check-new + vacuum + cleanup + pending + reboot + components' + [VERSION]='list + update + acquire' + [FEATURE]='features + enable-feature + disable-feature' + [COMPONENT]='enable-component + disable-component' + ) + + _init_completion || return + + if __contains_word "$prev" ${OPTS[ARG]}; then + case $prev in + -C|--component) + comps=$( __systemd_sysupdate_list components ) + ;; + --definitions|--root|--transfer-source) + comps=$(compgen -A directory -- "$cur" ) + compopt -o dirnames + ;; + --image) + comps=$(compgen -A file -- "$cur" ) + compopt -o filenames + ;; + --image-policy) + comps='' + ;; + --instances-max|-m) + comps='' + ;; + --sync|--verify|--cleanup) + comps='no yes' + ;; + --json) + comps='pretty short off' + ;; + esac + COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) + return 0 + fi + + if [[ "$cur" = -* ]]; then + COMPREPLY=( $(compgen -W '${OPTS[*]}' -- "$cur") ) + return 0 + fi + + for ((i=0; i < COMP_CWORD; i++)); do + if __contains_word "${COMP_WORDS[i]}" ${VERBS[*]} && + ! __contains_word "${COMP_WORDS[i-1]}" ${OPTS[ARG]}; then + verb=${COMP_WORDS[i]} + break + fi + done + + if [[ -z ${verb-} ]]; then + comps=${VERBS[*]} + elif __contains_word "$verb" ${VERBS[FEATURE]}; then + comps=$( __systemd_sysupdate_list features ) + elif __contains_word "$verb" ${VERBS[COMPONENT]}; then + comps=$( __systemd_sysupdate_list components ) + else + # STANDALONE verbs take no argument, VERSION verbs take a remote version + # that cannot be enumerated locally. + comps='' + fi + + COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) + return 0 +} + +complete -F _systemd-sysupdate systemd-sysupdate diff --git a/shell-completion/zsh/_systemd-sysupdate b/shell-completion/zsh/_systemd-sysupdate new file mode 100644 index 00000000000..e7930159e5a --- /dev/null +++ b/shell-completion/zsh/_systemd-sysupdate @@ -0,0 +1,92 @@ +#compdef systemd-sysupdate +# SPDX-License-Identifier: LGPL-2.1-or-later + +local context state state_descr line +typeset -A opt_args +local ret=1 + +# Pick the component/feature names out of the --json=short output of the +# 'components'/'features' verbs. (The tabular output is unsuitable here, since +# it prefixes each name with enabled/suggested checkmark columns.) +_systemd-sysupdate_components() { + local expl + local -a components=( ${(@)${(@)${(s:,:)${${"$(_call_program -l components systemd-sysupdate --no-pager --json=short components 2>/dev/null)"#*\"components\":\[}%%\]*}}#\"}%\"} ) + _wanted components expl "component" compadd "$@" -a - components +} + +_systemd-sysupdate_features() { + local expl + local -a features=( ${(@)${(@)${(s:,:)${${"$(_call_program -l features systemd-sysupdate --no-pager --json=short features 2>/dev/null)"#*\"features\":\[}%%\]*}}#\"}%\"} ) + _wanted features expl "feature" compadd "$@" -a - features +} + +local -a opts=( + {-h,--help}'[Show help message and exit]' + '--version[Show package version and exit]' + '--no-pager[Do not pipe output into a pager]' + '--no-legend[Do not show the headers and footers]' + '--json=[Show output as JSON]:mode:(pretty short off)' + '(-A --component-all -S --component-suggested)'{-C+,--component=}'[Select component to update]:component:_systemd-sysupdate_components' + '(-C --component -S --component-suggested)'{-A,--component-all}'[Select all components]' + '(-C --component -A --component-all)'{-S,--component-suggested}'[Select all suggested components]' + '(-s --feature-suggested)'{-a,--feature-all}'[Select all features]' + '(-a --feature-all)'{-s,--feature-suggested}'[Select all suggested features]' + '--definitions=[Find transfer definitions in specified directory]:directory:_directories' + '--root=[Operate on an alternate filesystem root]:directory:_directories' + '--image=[Operate on disk image as filesystem root]:image file:_files' + '--image-policy=[Specify disk image dissection policy]:policy' + '--transfer-source=[Specify the directory to transfer sources from]:directory:_directories' + {-m+,--instances-max=}'[How many instances to maintain]:number' + '--sync=[Control whether to sync data to disk]:bool:(yes no)' + '--verify=[Force signature verification on or off]:bool:(yes no)' + '--reboot[Reboot after updating to newer version]' + '--offline[Do not fetch metadata from the network]' + '--cleanup=[Clean up orphaned files after completing update]:bool:(yes no)' +) + +local -a commands=( + 'list:List available and installed versions' + 'features:List optional features' + 'enable-feature:Enable optional features' + 'disable-feature:Disable optional features' + 'check-new:Check if a newer version is available' + 'update:Install newest version' + 'acquire:Download newest version without installing' + 'vacuum:Make room by deleting old versions' + 'cleanup:Clean up orphaned files' + 'pending:Report whether a newer version is installed than booted' + 'reboot:Reboot if a newer version is installed than booted' + 'components:Show list of components' + 'enable-component:Enable components' + 'disable-component:Disable components' +) + +_arguments -s -A '-*' \ + "$opts[@]" \ + ':command:->command' \ + '*:: :->argument' && ret=0 + +case $state in + command) + _describe -t commands 'systemd-sysupdate command' commands && ret=0 + ;; + argument) + local curcontext=${curcontext%:*:*}:systemd-sysupdate-$words[1]: + case $words[1] in + features) + _arguments -s "$opts[@]" ':feature:_systemd-sysupdate_features' && ret=0 + ;; + enable-feature|disable-feature) + _arguments -s "$opts[@]" '*:feature:_systemd-sysupdate_features' && ret=0 + ;; + enable-component|disable-component) + _arguments -s "$opts[@]" '*:component:_systemd-sysupdate_components' && ret=0 + ;; + *) + _arguments -s "$opts[@]" && ret=0 + ;; + esac + ;; +esac + +return ret diff --git a/shell-completion/zsh/meson.build b/shell-completion/zsh/meson.build index edad8bcb325..7ffd4250acf 100644 --- a/shell-completion/zsh/meson.build +++ b/shell-completion/zsh/meson.build @@ -45,6 +45,7 @@ foreach item : [ ['_systemd-path', ''], ['_systemd-run', ''], ['_systemd-sysinstall', 'ENABLE_SYSINSTALL'], + ['_systemd-sysupdate', 'ENABLE_SYSUPDATE'], ['_systemd-tmpfiles', 'ENABLE_TMPFILES'], ['_timedatectl', 'ENABLE_TIMEDATED'], ['_udevadm', ''], diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index dadf307ff64..28ea65c0338 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -2591,44 +2591,20 @@ typedef enum { UPDATE_ACTION_INSTALL = 1 << 1, } UpdateActionFlags; -static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flags) { - _cleanup_free_ char *booted_version = NULL; +static int context_update( + Context *c, + const char *version, + const char *booted_version, + UpdateActionFlags action_flags) { + UpdateSet *applied = NULL; - const char *version; - int r; - - 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.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"); - - if (context.reboot) { - /* If automatic reboot on completion is requested, let's first determine the currently booted image */ - - r = parse_os_release(context.root, "IMAGE_VERSION", &booted_version); - if (r < 0) - return log_error_errno(r, "Failed to parse /etc/os-release: %m"); - if (!booted_version) - return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION field."); - } - bool installed = false; - int ret = 0; + int r, ret = 0; + + assert(c); r = context_load_online( - &context, + c, /* process_image_flags= */ 0, READ_DEFINITIONS_REQUIRES_ENABLED_TRANSFERS| READ_DEFINITIONS_REQUIRES_ANY_TRANSFERS| @@ -2640,15 +2616,15 @@ static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flag /* No transfer files found. In that case, still do the installdb cleanup below */ RET_GATHER(ret, r); } else { - if (action_flags & UPDATE_ACTION_ACQUIRE) - r = context_acquire(&context, version); + if (FLAGS_SET(action_flags, UPDATE_ACTION_ACQUIRE)) + r = context_acquire(c, version); else - r = context_process_partial_and_pending(&context, version); + r = context_process_partial_and_pending(c, version); if (r < 0) return r; if (FLAGS_SET(action_flags, UPDATE_ACTION_INSTALL) && r > 0) { /* installation of update indicated */ - r = context_install(&context, version, &applied); + r = context_install(c, version, &applied); if (r < 0) return r; @@ -2658,22 +2634,22 @@ static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flag /* context_install() returns > 0 (and emits a notification) only if it actually applied an update. If * nothing was applied but SYSTEMD_SYSUPDATE_FORCE_NOTIFY=1 is set, still notify subscribers (without a * resource list), so e.g. a kernel/policy refresh can be triggered unconditionally. */ - if ((action_flags & UPDATE_ACTION_INSTALL) && !installed) { + if (FLAGS_SET(action_flags, UPDATE_ACTION_INSTALL) && !installed) { int f = secure_getenv_bool("SYSTEMD_SYSUPDATE_FORCE_NOTIFY"); if (f < 0 && f != -ENXIO) log_debug_errno(f, "Failed to parse $SYSTEMD_SYSUPDATE_FORCE_NOTIFY, ignoring: %m"); if (f > 0) - (void) context_notify_subscribers(&context, /* us= */ NULL); + (void) context_notify_subscribers(c, /* us= */ NULL); } } - if (context.cleanup > 0) - RET_GATHER(ret, installdb_cleanup_component(&context)); + if (c->cleanup > 0) + RET_GATHER(ret, installdb_cleanup_component(c)); if (installed) { /* We installed something, yay */ - if (context.reboot) { + if (c->reboot) { assert(applied); assert(booted_version); @@ -2692,6 +2668,89 @@ static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flag return ret; } +static int verb_update_impl(int argc, char **argv, UpdateActionFlags action_flags) { + const char *version; + int r; + + 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.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]); + + if (context.instances_max < 2) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "The --instances-max= argument must be >= 2 while updating"); + + _cleanup_free_ char *booted_version = NULL; + if (context.reboot) { + /* We already checked in parse_argv() that --component=/--component-all/--component-suggested + * are not combined with --reboot */ + assert(!context.component); + assert(context.component_select == SELECT_EXPLICIT); + + /* If automatic reboot on completion is requested, let's first determine the currently booted image */ + + r = parse_os_release(context.root, "IMAGE_VERSION", &booted_version); + if (r < 0) + return log_error_errno(r, "Failed to parse /etc/os-release: %m"); + if (!booted_version) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "/etc/os-release lacks IMAGE_VERSION field."); + } + + switch (context.component_select) { + + case SELECT_EXPLICIT: + return context_update(&context, version, booted_version, action_flags); + + case SELECT_ALL: { + int ret = 0; + + /* Update the default, component-less installation first (if any). Running it before the + * enumeration below also ensures the image (if any) is mounted and context.root is set, so + * that we enumerate the components inside the image rather than on the host. A missing + * default installation (ENOENT) is not an error in this mode. */ + r = context_update(&context, version, booted_version, action_flags); + if (r != -ENOENT) + RET_GATHER(ret, r); + + _cleanup_strv_free_ char **component_names = NULL; + r = context_list_components(&context, &component_names, /* ret_has_default_component= */ NULL); + if (r < 0) { + RET_GATHER(ret, r); + return ret; + } + + 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_update(&cc, version, booted_version, action_flags); + if (r == -EHOSTDOWN) /* Component disabled → skip it in the "all" case. */ + continue; + RET_GATHER(ret, r); + } + + return ret; + } + + default: + assert_not_reached(); + } +} + VERB(verb_update, "update", "[VERSION]", VERB_ANY, 2, 0, "Install new version now"); static int verb_update(int argc, char *argv[], uintptr_t _data, void *userdata) { diff --git a/test/units/TEST-72-SYSUPDATE.sh b/test/units/TEST-72-SYSUPDATE.sh index 74af9296cc7..84c057c3723 100755 --- a/test/units/TEST-72-SYSUPDATE.sh +++ b/test/units/TEST-72-SYSUPDATE.sh @@ -918,8 +918,9 @@ test -f "$COMPALL/target-b/comp-b-v1.bin" test -d /var/lib/systemd/sysupdate/installdb.comp-a test -d /var/lib/systemd/sysupdate/installdb.comp-b -# --component-all is only supported for the "cleanup" verb, refuse it elsewhere. -(! "$SYSUPDATE" --component-all --verify=no update) +# --component-all is not supported for every verb, so it must be refused where it isn't (e.g. "vacuum"). +# (It *is* supported for update/acquire/cleanup/enable-*/disable-*, which is exercised further down.) +(! "$SYSUPDATE" --component-all --verify=no vacuum) # With the transfer files still in place "cleanup --component-all" is a no-op: # nothing is orphaned. @@ -1449,4 +1450,375 @@ rm -rf "$CONFIGDIR/01-manifest-yes-and-retry.transfer" \ "$SYSUPDATE" components |& grep "No components defined." >/dev/null [[ $(varlinkctl call "$VARLINK_SOCKET" io.systemd.SysUpdate.ListTargets | jq -r '.targets') == "[]" ]] +# ============================================================================ +# Components & features: enable/disable verbs, the --component-{all,suggested} +# and --feature-{all,suggested} switches, plus the Suggest=/SuggestOn…= +# settings. Everything below operates on freshly minted throw-away components +# and features, and cleans up after itself. +# +# Layout used throughout: +# * a component is defined by a transfer directory /run/sysupdate..d/ +# (which is what makes it show up in the 'components' list) plus an optional +# metadata file /run/sysupdate..component carrying Description=/Suggest=/… +# * the enable-component/disable-component verbs write their Enabled= override +# into a drop-in below /etc/sysupdate..component.d/ +# * the enable-feature/disable-feature verbs write their Enabled= override into +# a drop-in below /etc/sysupdate.d/.feature.d/ (default component) or +# /etc/sysupdate..d/.feature.d/ (named component) +# ============================================================================ +CF="$WORKDIR/compfeat" + +# The Suggest…MachineTag= tests below drive the machine tags via /etc/machine-info +# (which the condition logic reads directly); back up any pre-existing file so we +# can restore it afterwards, mirroring TEST-74-AUX-UTILS.machine-tags.sh. +MI_BAK="$WORKDIR/machine-info.orig" +rm -f "$MI_BAK" +[[ -e /etc/machine-info ]] && cp -a /etc/machine-info "$MI_BAK" + +set_machine_tags() { + if [[ -n "${1:-}" ]]; then + echo "TAGS=$1" >/etc/machine-info + else + rm -f /etc/machine-info + fi +} + +restore_machine_info() { + if [[ -e "$MI_BAK" ]]; then + cp -a "$MI_BAK" /etc/machine-info + else + rm -f /etc/machine-info + fi +} + +compfeat_cleanup() { + rm -rf /run/sysupdate.d \ + /run/sysupdate.compx.d /run/sysupdate.compx.component /run/sysupdate.compx.component.d \ + /run/sysupdate.compy.d /run/sysupdate.compy.component /run/sysupdate.compy.component.d \ + /run/sysupdate.compz.d /run/sysupdate.compz.component /run/sysupdate.compz.component.d \ + /etc/sysupdate.d \ + /etc/sysupdate.compx.d /etc/sysupdate.compx.component.d \ + /etc/sysupdate.compy.d /etc/sysupdate.compy.component.d \ + /etc/sysupdate.compz.d /etc/sysupdate.compz.component.d + rm -rf "$CF" +} + +compfeat_reset() { + compfeat_cleanup + mkdir -p "$CF/source" \ + "$CF/target-default" "$CF/target-compx" "$CF/target-compy" "$CF/target-compz" +} + +# (Re)generate the source payloads + SHA256SUMS for a given version. We create a +# payload for every component/feature so a single SHA256SUMS covers them all; +# individual transfers only ever match their own pattern. +compfeat_source() { + local v="${1:?}" + local n + for n in base compx compy compz feata featb featc; do + echo "$n-$v-$RANDOM" >"$CF/source/$n-$v.bin" + done + (cd "$CF/source" && sha256sum -- *.bin >SHA256SUMS) +} + +# Write a regular-file transfer; optional 4th argument gates it behind a feature. +compfeat_transfer() { + local file="${1:?}" pat="${2:?}" tgt="${3:?}" feature="${4:-}" + { + if [[ -n "$feature" ]]; then + printf '[Transfer]\nFeatures=%s\n\n' "$feature" + fi + printf '[Source]\nType=regular-file\nPath=%s\nMatchPattern=%s-@v.bin\n\n' "$CF/source" "$pat" + printf '[Target]\nType=regular-file\nPath=%s\nMatchPattern=%s-@v.bin\nInstancesMax=2\n' "$tgt" "$pat" + } >"$file" +} + +comp_enable_dropin() { echo "/etc/sysupdate.$1.component.d/50-systemd-sysupdate-enabled.conf"; } +feat_enable_dropin_default() { echo "/etc/sysupdate.d/$1.feature.d/50-systemd-sysupdate-enabled.conf"; } +feat_enable_dropin_comp() { echo "/etc/sysupdate.$1.d/$2.feature.d/50-systemd-sysupdate-enabled.conf"; } + +# Assert a generated Enabled= drop-in exists and carries the expected value. +assert_dropin() { + local file="${1:?}" val="${2:?}" + test -f "$file" + grep "^Enabled=$val$" "$file" >/dev/null +} + +# --------------------------------------------------------------------------- +# enable-component / disable-component: explicit selection + observable effect +# --------------------------------------------------------------------------- +compfeat_reset +compfeat_source v1 +mkdir -p /run/sysupdate.compx.d +compfeat_transfer /run/sysupdate.compx.d/01-compx.transfer compx "$CF/target-compx" +# A metadata file carrying a human-readable description (exercises loading of the +# per-component *.component file from the search dirs). +cat >/run/sysupdate.compx.component </dev/null + +# A brand new component (no Enabled= override anywhere) is enabled by default and +# can be updated. +"$SYSUPDATE" --component=compx --verify=no update +test -f "$CF/target-compx/compx-v1.bin" + +# Disable it: this must drop an Enabled=no override next to the definition, and +# subsequent updates for that component must be refused. +"$SYSUPDATE" disable-component compx +assert_dropin "$(comp_enable_dropin compx)" no +(! "$SYSUPDATE" --component=compx --verify=no update) |& grep -F "Component is disabled" >/dev/null + +# Re-enable via the "--component= + no positional argument" form and verify the +# update works again. +"$SYSUPDATE" --component=compx enable-component +assert_dropin "$(comp_enable_dropin compx)" yes +rm -f "$CF/target-compx/compx-v1.bin" +"$SYSUPDATE" --component=compx --verify=no update +test -f "$CF/target-compx/compx-v1.bin" + +# Enabling a component must not conjure a bogus ".component" pseudo-component +# out of the freshly created sysupdate.compx.component.d/ drop-in directory. +(! "$SYSUPDATE" --json=short components | grep -F '"compx.component"' >/dev/null) +"$SYSUPDATE" --json=short components | grep -F '"compx"' >/dev/null + +# --------------------------------------------------------------------------- +# enable-component / disable-component: argument validation & error handling +# --------------------------------------------------------------------------- +# Positional argument and --component= are mutually exclusive. +(! "$SYSUPDATE" --component=compx enable-component compx) |& grep -F "not both" >/dev/null +# Syntactically invalid component name. +(! "$SYSUPDATE" enable-component ../nope) |& grep -F "Component name invalid" >/dev/null +# Unknown component. +(! "$SYSUPDATE" enable-component doesnotexist) |& grep -F "Component not found" >/dev/null +# --definitions= is incompatible with component enablement. +(! "$SYSUPDATE" --definitions="$CF" enable-component compx) |& grep -F "may not be combined" >/dev/null +# The feature-selection switches make no sense here. +(! "$SYSUPDATE" --feature-all enable-component compx) |& grep -F "not supported" >/dev/null + +# --------------------------------------------------------------------------- +# --component-all for enable-component / disable-component +# --------------------------------------------------------------------------- +compfeat_reset +compfeat_source v1 +mkdir -p /run/sysupdate.compx.d /run/sysupdate.compy.d +compfeat_transfer /run/sysupdate.compx.d/01-compx.transfer compx "$CF/target-compx" +compfeat_transfer /run/sysupdate.compy.d/01-compy.transfer compy "$CF/target-compy" + +# Disable *all* components in one go, then re-enable them all. +"$SYSUPDATE" --component-all disable-component +assert_dropin "$(comp_enable_dropin compx)" no +assert_dropin "$(comp_enable_dropin compy)" no +(! "$SYSUPDATE" --component=compx --verify=no update) |& grep -F "Component is disabled" >/dev/null +(! "$SYSUPDATE" --component=compy --verify=no update) |& grep -F "Component is disabled" >/dev/null + +"$SYSUPDATE" --component-all enable-component +assert_dropin "$(comp_enable_dropin compx)" yes +assert_dropin "$(comp_enable_dropin compy)" yes +"$SYSUPDATE" --component=compx --verify=no update +"$SYSUPDATE" --component=compy --verify=no update +test -f "$CF/target-compx/compx-v1.bin" +test -f "$CF/target-compy/compy-v1.bin" + +# --------------------------------------------------------------------------- +# --component-suggested driven by Suggest= (compx suggested, compy not) +# --------------------------------------------------------------------------- +compfeat_reset +compfeat_source v1 +mkdir -p /run/sysupdate.compx.d /run/sysupdate.compy.d +compfeat_transfer /run/sysupdate.compx.d/01-compx.transfer compx "$CF/target-compx" +compfeat_transfer /run/sysupdate.compy.d/01-compy.transfer compy "$CF/target-compy" +cat >/run/sysupdate.compx.component </run/sysupdate.compy.component </dev/null + +# --------------------------------------------------------------------------- +# SuggestOnMachineTag= for a component +# --------------------------------------------------------------------------- +compfeat_reset +compfeat_source v1 +mkdir -p /run/sysupdate.compz.d +compfeat_transfer /run/sysupdate.compz.d/01-compz.transfer compz "$CF/target-compz" +cat >/run/sysupdate.compz.component </run/sysupdate.d/feata.feature </dev/null +"$SYSUPDATE" --verify=no update +test -f "$CF/target-default/base-v1.bin" +test ! -e "$CF/target-default/feata-v1.bin" + +# Enabling the feature must drop an Enabled=yes override and cause the gated +# transfer to be installed on the next update. +"$SYSUPDATE" enable-feature feata +assert_dropin "$(feat_enable_dropin_default feata)" yes +"$SYSUPDATE" --verify=no update +test -f "$CF/target-default/feata-v1.bin" + +# Disabling it again + vacuum must remove the now-orphaned feature resource. +"$SYSUPDATE" disable-feature feata +assert_dropin "$(feat_enable_dropin_default feata)" no +"$SYSUPDATE" --verify=no vacuum +test ! -e "$CF/target-default/feata-v1.bin" + +# Argument validation for the feature verbs. +(! "$SYSUPDATE" enable-feature --feature-all feata) |& grep -F "not both" >/dev/null +(! "$SYSUPDATE" enable-feature 'bad/name') |& grep -F "Feature name invalid" >/dev/null +(! "$SYSUPDATE" --component-suggested enable-feature feata) |& grep -F "not supported" >/dev/null + +# --------------------------------------------------------------------------- +# --feature-all / --feature-suggested (default component) +# --------------------------------------------------------------------------- +compfeat_reset +compfeat_source v1 +mkdir -p /run/sysupdate.d +compfeat_transfer /run/sysupdate.d/01-base.transfer base "$CF/target-default" +# feata: suggested (Suggest=yes); featb: not suggested (Suggest=no); +# featc: suggested on a machine tag we will set below. +cat >/run/sysupdate.d/feata.feature </run/sysupdate.d/featb.feature </run/sysupdate.d/featc.feature </run/sysupdate.d/feata.feature </run/sysupdate.compx.d/featx.feature <