From: Philip Withnall Date: Fri, 29 May 2026 14:34:56 +0000 (+0100) Subject: sysupdate: Add varlink CheckNew() method X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=7757ec3317312f3ce0ceb69f394258bbf066c48f;p=thirdparty%2Fsystemd.git sysupdate: Add varlink CheckNew() method This is the first varlink method added to sysupdate. The D-Bus interface (via sysupdated) will remain for now; the varlink interface will exist in parallel. This method can be called via: ``` varlinkctl call ./path/to/systemd-sysupdate \ io.systemd.SysUpdate.CheckNew \ '{"target":{"class":"host"}}' SYSTEMD_SYSUPDATE_NO_VERIFY=1 \ varlinkctl call ./path/to/systemd-sysupdate \ io.systemd.SysUpdate.CheckNew \ '{"target":{"class":"component","name":"some-component"}}' ``` This includes some changes to run the integration tests again using the varlink interface rather than running `systemd-sysupdate` directly, to test the new interface. --- diff --git a/src/shared/varlink-io.systemd.SysUpdate.c b/src/shared/varlink-io.systemd.SysUpdate.c index 67595fe9ee2..e80142f9c51 100644 --- a/src/shared/varlink-io.systemd.SysUpdate.c +++ b/src/shared/varlink-io.systemd.SysUpdate.c @@ -3,7 +3,56 @@ #include "bus-polkit.h" #include "varlink-io.systemd.SysUpdate.h" +static SD_VARLINK_DEFINE_ENUM_TYPE( + TargetClass, + SD_VARLINK_FIELD_COMMENT("Container or machine managed by systemd-machined.service(8)."), + SD_VARLINK_DEFINE_ENUM_VALUE(machine), + SD_VARLINK_FIELD_COMMENT("Portable service."), + SD_VARLINK_DEFINE_ENUM_VALUE(portable), + SD_VARLINK_FIELD_COMMENT("System extension managed by systemd-sysext.service(8)."), + SD_VARLINK_DEFINE_ENUM_VALUE(sysext), + SD_VARLINK_FIELD_COMMENT("Configuration extension managed by systemd-confext.service(8)."), + SD_VARLINK_DEFINE_ENUM_VALUE(confext), + SD_VARLINK_FIELD_COMMENT("Host system."), + SD_VARLINK_DEFINE_ENUM_VALUE(host), + SD_VARLINK_FIELD_COMMENT("Component managed by systemd-sysupdate.service(8)."), + SD_VARLINK_DEFINE_ENUM_VALUE(component)); + +static SD_VARLINK_DEFINE_STRUCT_TYPE( + TargetIdentifier, + SD_VARLINK_FIELD_COMMENT("Where the target was enumerated."), + SD_VARLINK_DEFINE_FIELD_BY_TYPE(class, TargetClass, 0), + SD_VARLINK_FIELD_COMMENT("Name of the target, unique within a class."), + SD_VARLINK_DEFINE_FIELD(name, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); + +static SD_VARLINK_DEFINE_METHOD( + CheckNew, + SD_VARLINK_FIELD_COMMENT("Target to check for updates for."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(target, TargetIdentifier, 0), + VARLINK_DEFINE_POLKIT_INPUT, + SD_VARLINK_FIELD_COMMENT("The version string for the new version which is available."), + SD_VARLINK_DEFINE_OUTPUT(available, SD_VARLINK_STRING, 0)); + +static SD_VARLINK_DEFINE_ERROR(NoUpdateNeeded); +static SD_VARLINK_DEFINE_ERROR(NoSuchTarget); + SD_VARLINK_DEFINE_INTERFACE( io_systemd_SysUpdate, "io.systemd.SysUpdate", - SD_VARLINK_INTERFACE_COMMENT("APIs to manage system updates")); + SD_VARLINK_INTERFACE_COMMENT("APIs to manage system updates"), + + /* Methods */ + SD_VARLINK_SYMBOL_COMMENT("Check if there’s a new version available"), + &vl_method_CheckNew, + + /* Types */ + SD_VARLINK_SYMBOL_COMMENT("Class of a Target."), + &vl_type_TargetClass, + SD_VARLINK_SYMBOL_COMMENT("Identifier for a component of the system (i.e. the host itself, a sysext, a confext, etc.) that can be updated by systemd-sysupdate(8)."), + &vl_type_TargetIdentifier, + + /* Errors */ + SD_VARLINK_SYMBOL_COMMENT("Error indicating that no update is currently available to update to."), + &vl_error_NoUpdateNeeded, + SD_VARLINK_SYMBOL_COMMENT("Error indicating the specified target doesn’t exist"), + &vl_error_NoSuchTarget); diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c index f1ba327c5db..d3874c377d3 100644 --- a/src/sysupdate/sysupdate.c +++ b/src/sysupdate/sysupdate.c @@ -11,6 +11,7 @@ #include "bus-polkit.h" #include "conf-files.h" #include "constants.h" +#include "discover-image.h" #include "dissect-image.h" #include "env-util.h" #include "errno-util.h" @@ -31,6 +32,7 @@ #include "parse-argument.h" #include "parse-util.h" #include "pretty-print.h" +#include "runtime-scope.h" #include "sort-util.h" #include "specifier.h" #include "string-util.h" @@ -39,6 +41,7 @@ #include "sysupdate-cleanup.h" #include "sysupdate-feature.h" #include "sysupdate-instance.h" +#include "sysupdate-target.h" #include "sysupdate-transfer.h" #include "sysupdate-update-set.h" #include "sysupdate-util.h" @@ -84,6 +87,7 @@ const Specifier specifier_table[] = { .verify = -1, \ .cleanup = -1, \ .installdb_fd = -EBADF, \ + .target_identifier.class = _TARGET_CLASS_INVALID, \ } void context_done(Context *c) { @@ -119,6 +123,8 @@ void context_done(Context *c) { c->component = mfree(c->component); c->image_policy = image_policy_free(c->image_policy); c->transfer_source = mfree(c->transfer_source); + + target_identifier_done(&c->target_identifier); } static int context_from_cmdline(Context *ret) { @@ -1047,6 +1053,8 @@ static int process_image( return 0; } +static int context_list_components(Context *context, char ***component_names, bool *has_default_component); + static int context_load_offline( Context *context, ProcessImageFlags process_image_flags, @@ -1103,6 +1111,144 @@ static int context_load_online( return 0; } +static bool image_type_can_sysupdate(ImageType image_type) { + switch (image_type) { + case IMAGE_DIRECTORY: + case IMAGE_SUBVOLUME: + case IMAGE_RAW: + case IMAGE_BLOCK: + return true; + + /* systemd-sysupdate doesn't support mstack images yet */ + case IMAGE_MSTACK: + default: + return false; + } +} + +static int context_load_paths_from_image(Context *context, Image *image) { + assert(context); + assert(image); + + assert(!context->root); + assert(!context->image); + + switch (image->type) { + case IMAGE_DIRECTORY: + case IMAGE_SUBVOLUME: + context->root = strdup(image->path); + if (!context->root) + return log_oom(); + return 0; + case IMAGE_RAW: + case IMAGE_BLOCK: + context->image = strdup(image->path); + if (!context->image) + return log_oom(); + return 0; + default: + assert_not_reached(); + } +} + +/* Load a Context to point to the target given by the TargetIdentifier. The TargetIdentifier will have been + * syntactically validated by dispatch_target_identifier(), but might still point to components which don’t + * exist, images which the user isn’t privileged to access, etc. This function validates the TargetIdentifier + * against an enumerated list of known targets, which are safe to update without additional permissions. */ +static int context_load_online_from_target(Context *context, ProcessImageFlags process_image_flags) { + int r; + + assert(context); + assert(context->target_identifier.class != _TARGET_CLASS_INVALID); + + /* These shouldn’t have been set up some other way first */ + assert(!context->component); + assert(!context->root); + assert(!context->image); + + switch (context->target_identifier.class) { + case TARGET_MACHINE: + case TARGET_PORTABLE: + case TARGET_SYSEXT: + case TARGET_CONFEXT: { + _cleanup_hashmap_free_ Hashmap *images = NULL; + Image *image, *selected_image = NULL; + + /* These are all image-based target classes, so first find the corresponding image. */ + r = image_discover(RUNTIME_SCOPE_SYSTEM, (ImageClass) context->target_identifier.class, NULL, &images); + if (r < 0) + return r; + + HASHMAP_FOREACH(image, images) { + bool have = false; + _cleanup_(context_done) Context image_context = CONTEXT_NULL; + + if (image_is_host(image)) + continue; /* We already enroll the host ourselves */ + + if (!image_type_can_sysupdate(image->type)) + continue; + + if (!streq(image->name, context->target_identifier.name)) + continue; + + r = context_load_paths_from_image(&image_context, image); + if (r < 0) + return r; + + /* Load the components in a separate Context specific to the given Image before + * committing to loading that state to the main Context. */ + r = context_load_offline(&image_context, 0, 0); + if (r < 0) + return r; + + r = context_list_components(&image_context, /* component_names= */ NULL, &have); + if (r < 0) + return r; + if (!have) { + log_debug("Skipping %s because it has no default component", image->path); + continue; + } + + /* This is the match we were looking for */ + selected_image = image; + break; + } + + if (!selected_image) + return -ENOENT; + + r = context_load_paths_from_image(context, selected_image); + if (r < 0) + return r; + + break; + } + case TARGET_HOST: + /* No additional setup needed */ + break; + case TARGET_COMPONENT: { + _cleanup_strv_free_ char **component_names = NULL; + + r = context_list_components(context, &component_names, /* has_default_component= */ NULL); + if (r < 0) + return r; + + if (!strv_contains(component_names, context->target_identifier.name)) + return -ENOENT; + + context->component = strdup(context->target_identifier.name); + if (!context->component) + return log_oom(); + break; + } + default: + assert_not_reached(); + } + + return context_load_online(context, process_image_flags); +} + static int context_on_acquire_progress(const Transfer *t, const Instance *inst, unsigned percentage) { const Context *c = ASSERT_PTR(t->context); size_t i, n = c->n_transfers; @@ -1437,6 +1583,31 @@ static int context_install( return 1; } +static JSON_DISPATCH_ENUM_DEFINE(dispatch_target_class, TargetClass, target_class_from_string); + +static int dispatch_target_identifier(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + TargetIdentifier *t = ASSERT_PTR(userdata); + static const sd_json_dispatch_field dispatch[] = { + { "class", SD_JSON_VARIANT_STRING, dispatch_target_class, voffsetof(*t, class), SD_JSON_MANDATORY }, + { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, voffsetof(*t, name), SD_JSON_NULLABLE }, + {} + }; + int r; + + r = sd_json_dispatch(variant, dispatch, flags, t); + if (r < 0) + return r; + + /* Name is mandatory unless class is `host` */ + if ((t->class == TARGET_HOST) != (!t->name)) + return json_log(variant, flags, SYNTHETIC_ERRNO(ENXIO), "Target name does not match class."); + + if (t->class == TARGET_COMPONENT && !component_name_valid(t->name)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Component name invalid: %s", t->name); + + return 0; +} + static int verify_polkit(Context *context, sd_varlink *link, const char *action, const char **details) { int r; Server *s = ASSERT_PTR(sd_varlink_get_userdata(ASSERT_PTR(link))); @@ -1717,6 +1888,52 @@ static int verb_check_new(int argc, char *argv[], uintptr_t _data, void *userdat return EXIT_SUCCESS; } +static int vl_method_check_new(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + _cleanup_(context_done) Context context = CONTEXT_NULL; + int r; + + assert(link); + + static const sd_json_dispatch_field dispatch_table[] = { + { "target", SD_JSON_VARIANT_OBJECT, dispatch_target_identifier, voffsetof(context, target_identifier), SD_JSON_MANDATORY }, + VARLINK_DISPATCH_POLKIT_FIELD, + {}, + }; + + r = sd_varlink_dispatch(link, parameters, dispatch_table, &context); + if (r != 0) + return r; + + r = verify_polkit(&context, link, "org.freedesktop.sysupdate1.check", + (const char**) STRV_MAKE( + "class", target_class_to_string(context.target_identifier.class), + "offline", "0", + context.target_identifier.name ? "name" : NULL, context.target_identifier.name)); + if (r <= 0) + return r; + + if (getenv_bool("SYSTEMD_SYSUPDATE_NO_VERIFY") > 0) + context.verify = 0; + + /* CheckNew is always online */ + context.offline = false; + + r = context_load_online_from_target(&context, PROCESS_IMAGE_READ_ONLY); + if (r == -ENOENT) + return sd_varlink_error(link, "io.systemd.SysUpdate.NoSuchTarget", NULL); + else if (r < 0) + return r; + + if (context.candidate) + r = sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRING("available", context.candidate->version)); + else + r = sd_varlink_error(link, "io.systemd.SysUpdate.NoUpdateNeeded", NULL); + if (r < 0) + return r; + + return 0; +} + typedef enum { UPDATE_ACTION_ACQUIRE = 1 << 0, UPDATE_ACTION_INSTALL = 1 << 1, @@ -2293,7 +2510,8 @@ static int vl_server(void) { return log_error_errno(r, "Failed to add Varlink interface: %m"); r = sd_varlink_server_bind_method_many( - varlink_server); + varlink_server, + "io.systemd.SysUpdate.CheckNew", vl_method_check_new); if (r < 0) return log_error_errno(r, "Failed to bind Varlink method: %m"); diff --git a/src/sysupdate/sysupdate.h b/src/sysupdate/sysupdate.h index 0d0a8e9fbfa..02a9f7c8f34 100644 --- a/src/sysupdate/sysupdate.h +++ b/src/sysupdate/sysupdate.h @@ -3,6 +3,7 @@ #include "specifier.h" #include "sysupdate-forward.h" +#include "sysupdate-target.h" typedef struct Context { /* Parameters/Command line arguments: */ @@ -40,6 +41,8 @@ typedef struct Context { Hashmap *web_cache; /* Cache for downloaded resources, keyed by URL */ int installdb_fd; + + TargetIdentifier target_identifier; } Context; void context_done(Context *c); diff --git a/test/units/TEST-72-SYSUPDATE.sh b/test/units/TEST-72-SYSUPDATE.sh index 928f110b69d..f8ba6035df7 100755 --- a/test/units/TEST-72-SYSUPDATE.sh +++ b/test/units/TEST-72-SYSUPDATE.sh @@ -8,6 +8,7 @@ set -o pipefail SYSUPDATE=/usr/bin/systemd-sysupdate SYSUPDATED=/lib/systemd/systemd-sysupdated UPDATECTL="" +VARLINK_SOCKET=/run/systemd/io.systemd.SysUpdate SECTOR_SIZES=(512 4096) WORKDIR="$(mktemp -d /var/tmp/test-72-XXXXXX)" CONFIGDIR="/run/sysupdate.d" @@ -35,8 +36,8 @@ if [[ ! -e /dev/loop-control ]]; then SECTOR_SIZES=(512) fi -# Set up sysupdated drop-in pointing at the correct definitions and setting -# no verification of images. +# Set up sysupdated and varlink drop-ins pointing at the correct definitions and +# setting no verification of images. mkdir -p /run/systemd/system/systemd-sysupdated.service.d cat >/run/systemd/system/systemd-sysupdated.service.d/override.conf</run/systemd/system/systemd-sysupdate@.service.d/override.conf</dev/null + else + exit 1 + fi } check_new_update_available() { - "$SYSUPDATE" --verify=no check-new + local client="${1:?}" + + if [[ "$client" == "sysupdate-cli" ]]; then + "$SYSUPDATE" --verify=no check-new + elif [[ "$client" == "varlink" ]]; then + varlinkctl call "$VARLINK_SOCKET" io.systemd.SysUpdate.CheckNew '{"target":{"class":"host"}}' | grep available + else + exit 1 + fi } update_now() { local update_type="${1:?}" - local checks="${2:-}" + local client="${2:?}" + local checks="${3:-}" # Update to newest version. First there should be an update ready, then we # do the update, and then there should not be any ready anymore @@ -143,7 +170,7 @@ update_now() { # repairing an installation), so that can be overridden via the local. if [[ "$checks" != "no-checks" ]]; then - check_new_update_available + check_new_update_available "$client" fi if [[ "$update_type" == "monolithic" ]]; then @@ -167,7 +194,7 @@ update_now() { fi if [[ "$checks" != "no-checks" ]]; then - check_no_new_update_available + check_no_new_update_available "$client" fi } @@ -216,6 +243,7 @@ verify_object_fields() { } for sector_size in "${SECTOR_SIZES[@]}"; do +for client in sysupdate-cli varlink; do for update_type in monolithic split-offline split updatectl; do # Disk size of: # - 1MB for GPT @@ -379,18 +407,18 @@ EOF # Install initial version and verify new_version "$sector_size" v1 - update_now "$update_type" + update_now "$update_type" "$client" verify_version_current "$blockdev" "$sector_size" v1 1 # Create second version, update and verify that it is added new_version "$sector_size" v2 - update_now "$update_type" + update_now "$update_type" "$client" verify_version "$blockdev" "$sector_size" v1 1 verify_version_current "$blockdev" "$sector_size" v2 2 # Create third version, update and verify it replaced the first version new_version "$sector_size" v3 - update_now "$update_type" + update_now "$update_type" "$client" verify_version_current "$blockdev" "$sector_size" v3 1 verify_version "$blockdev" "$sector_size" v2 2 test ! -f "$WORKDIR/xbootldr/EFI/Linux/uki_v1+3-0.efi" @@ -402,12 +430,12 @@ EOF new_version "$sector_size" v4 rm "$WORKDIR/source/uki-extra-v4.efi" update_checksums - check_no_new_update_available + check_no_new_update_available "$client" # Create a fifth version, that's complete on the server side. We should # completely skip the incomplete v4 and install v5 instead. new_version "$sector_size" v5 - update_now "$update_type" + update_now "$update_type" "$client" verify_version "$blockdev" "$sector_size" v3 1 verify_version_current "$blockdev" "$sector_size" v5 2 @@ -417,7 +445,7 @@ EOF # Always do this as a monolithic update for the repair to work. rm -r "$WORKDIR/xbootldr/EFI/Linux/uki_v5.efi.extra.d" "$SYSUPDATE" --offline list v5 | grep "incomplete" >/dev/null - update_now "monolithic" + update_now "monolithic" "$client" "$SYSUPDATE" --offline list v5 | grep -v "incomplete" >/dev/null verify_version "$blockdev" "$sector_size" v3 1 verify_version_current "$blockdev" "$sector_size" v5 2 @@ -429,7 +457,7 @@ EOF mkdir "$CONFIGDIR/optional.feature.d" echo -e "[Feature]\nEnabled=true" > "$CONFIGDIR/optional.feature.d/enable.conf" "$SYSUPDATE" --offline list v5 | grep "incomplete" >/dev/null - update_now "$update_type" + update_now "$update_type" "$client" "$SYSUPDATE" --offline list v5 | grep -v "incomplete" >/dev/null verify_version "$blockdev" "$sector_size" v3 1 verify_version_current "$blockdev" "$sector_size" v5 2 @@ -437,7 +465,7 @@ EOF # And now let's disable it and make sure it gets cleaned up rm -r "$CONFIGDIR/optional.feature.d" - check_no_new_update_available + check_no_new_update_available "$client" "$SYSUPDATE" vacuum "$SYSUPDATE" --offline list v5 | grep -v "incomplete" >/dev/null verify_version "$blockdev" "$sector_size" v3 1 @@ -449,17 +477,17 @@ EOF new_version "$sector_size" v6 if [[ -x "$UPDATECTL" ]]; then systemctl start systemd-sysupdated - check_new_update_available + check_new_update_available "$client" "$UPDATECTL" update |& tee "$WORKDIR"/updatectl-update-6 grep "Done" "$WORKDIR"/updatectl-update-6 (! grep "Already up-to-date" "$WORKDIR"/updatectl-update-6) else # If no updatectl, gracefully fall back to systemd-sysupdate - update_now "$update_type" + update_now "$update_type" "$client" fi # User-facing updatectl returns 0 if there's no updates, so use the low-level # utility to make sure we did upgrade - check_no_new_update_available + check_no_new_update_available "$client" verify_version_current "$blockdev" "$sector_size" v6 1 verify_version "$blockdev" "$sector_size" v5 2 @@ -512,7 +540,7 @@ MatchPattern=dir-@v InstancesMax=3 EOF - update_now "$update_type" + update_now "$update_type" "$client" verify_version "$blockdev" "$sector_size" v6 1 verify_version_current "$blockdev" "$sector_size" v7 2 @@ -535,7 +563,7 @@ EOF # (what .transfer files were called before v257) for i in "$CONFIGDIR/"*.conf; do echo mv "$i" "${i%.conf}.transfer"; done new_version "$sector_size" v8 - update_now "$update_type" + update_now "$update_type" "$client" verify_version_current "$blockdev" "$sector_size" v8 1 verify_version "$blockdev" "$sector_size" v7 2 @@ -545,11 +573,11 @@ EOF # Vacuum the partial version, regenerate it on the server, try updating # again and it should succeed. new_version "$sector_size" v9 "corrupt-checksum" - (! update_now "$update_type") + (! update_now "$update_type" "$client") "$SYSUPDATE" --offline list v9 | grep "partial" >/dev/null verify_version_current "$blockdev" "$sector_size" v8 1 # don’t verify the other part of the block device as it’s in an indeterminate state - (! update_now "$update_type" "no-checks") |& tee "$WORKDIR"/update_now-9 + (! update_now "$update_type" "$client" "no-checks") |& tee "$WORKDIR"/update_now-9 cat "$WORKDIR"/update_now-9 grep "is already acquired and partially installed. Vacuum it to try installing again." "$WORKDIR"/update_now-9 "$SYSUPDATE" --offline vacuum |& grep "Removing old partial" >/dev/null @@ -557,7 +585,7 @@ EOF # don’t verify the other part of the block device as it’s in an indeterminate state "$SYSUPDATE" --verify=no list v9 | grep "candidate" >/dev/null new_version "$sector_size" v9 - update_now "$update_type" + update_now "$update_type" "$client" verify_version "$blockdev" "$sector_size" v8 1 verify_version_current "$blockdev" "$sector_size" v9 2 @@ -566,6 +594,7 @@ EOF rm "$BACKING_FILE" done done +done # Regression test for https://github.com/systemd/systemd/issues/41501 — check # that a ‘default’ component is only listed by sysupdate if it’s fully configured