]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sysupdate: Add varlink CheckNew() method
authorPhilip Withnall <pwithnall@gnome.org>
Fri, 29 May 2026 14:34:56 +0000 (15:34 +0100)
committerPhilip Withnall <pwithnall@gnome.org>
Fri, 26 Jun 2026 12:01:53 +0000 (13:01 +0100)
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.

src/shared/varlink-io.systemd.SysUpdate.c
src/sysupdate/sysupdate.c
src/sysupdate/sysupdate.h
test/units/TEST-72-SYSUPDATE.sh

index 67595fe9ee29685382ba931902b0737403aea98b..e80142f9c510fcb3873e11a5f17c752b905ee678 100644 (file)
@@ -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);
index f1ba327c5db1310f37cf14a9ef8abb6bb559b72c..d3874c377d36a8b3d976c5ac448747a9f91f9556 100644 (file)
@@ -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");
 
index 0d0a8e9fbfab4bb082e9e2e11d91147eec7ab6d1..02a9f7c8f348cce69adbfb890401df00964f55e4 100644 (file)
@@ -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);
index 928f110b69d229cb96cc16228033613c6dccfee4..f8ba6035df70910def494b830d0242003d17663d 100755 (executable)
@@ -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<<EOF
 [Service]
@@ -44,6 +45,15 @@ Environment=SYSTEMD_SYSUPDATE_NO_VERIFY=1
 Environment=SYSTEMD_ESP_PATH=${SYSTEMD_ESP_PATH}
 Environment=SYSTEMD_XBOOTLDR_PATH=${SYSTEMD_XBOOTLDR_PATH}
 EOF
+
+mkdir -p /run/systemd/system/systemd-sysupdate@.service.d
+cat >/run/systemd/system/systemd-sysupdate@.service.d/override.conf<<EOF
+[Service]
+Environment=SYSTEMD_SYSUPDATE_NO_VERIFY=1
+Environment=SYSTEMD_ESP_PATH=${SYSTEMD_ESP_PATH}
+Environment=SYSTEMD_XBOOTLDR_PATH=${SYSTEMD_XBOOTLDR_PATH}
+EOF
+
 systemctl daemon-reload
 
 at_exit() {
@@ -122,16 +132,33 @@ new_version() {
 }
 
 check_no_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 io.systemd.SysUpdate.NoUpdateNeeded >/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