From 48a9afb6eca06f8bf7434cd907af02cdabb75ecb Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 2 Sep 2026 12:06:58 +0200 Subject: [PATCH 1/7] blockdev-list: split BLOCKDEV_LIST_IGNORE_LOOP out of BLOCKDEV_LIST_IGNORE_VIRTUAL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit So far BLOCKDEV_LIST_IGNORE_VIRTUAL filtered out everything below /sys/devices/virtual/, which includes loopback devices. Loopback devices are frequently used as stand-ins for real disks though (in tests, and when installing to disk images), hence it's useful to be able to control them independently of the other virtual devices (dm, md, zram, …). Introduce a separate BLOCKDEV_LIST_IGNORE_LOOP flag for loopback devices, and make BLOCKDEV_LIST_IGNORE_VIRTUAL only cover the non-loop virtual devices. The only existing user (systemd-report) is updated to pass both flags, so that its behaviour stays unchanged. --- src/report/report-basic.c | 2 +- src/shared/blockdev-list.c | 17 ++++++++++++++++- src/shared/blockdev-list.h | 21 +++++++++++---------- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/report/report-basic.c b/src/report/report-basic.c index 4b8d9ce024..eb2fe6a498 100644 --- a/src/report/report-basic.c +++ b/src/report/report-basic.c @@ -384,7 +384,7 @@ static int disk_io_generate(const MetricFamily mf[static 2], sd_varlink *link, v size_t n_devices = 0; CLEANUP_ARRAY(devices, n_devices, block_device_array_free); - r = blockdev_list(BLOCKDEV_LIST_IGNORE_VIRTUAL | BLOCKDEV_LIST_IGNORE_PARTITIONS | BLOCKDEV_LIST_RW_STATS, + r = blockdev_list(BLOCKDEV_LIST_IGNORE_VIRTUAL | BLOCKDEV_LIST_IGNORE_LOOP | BLOCKDEV_LIST_IGNORE_PARTITIONS | BLOCKDEV_LIST_RW_STATS, &devices, &n_devices); if (r < 0) return r; diff --git a/src/shared/blockdev-list.c b/src/shared/blockdev-list.c index fe71bd4ccc..e44332dc60 100644 --- a/src/shared/blockdev-list.c +++ b/src/shared/blockdev-list.c @@ -167,7 +167,22 @@ int blockdev_list_one( goto no_match; } - if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_VIRTUAL)) { + bool is_loop = false; + if (flags & (BLOCKDEV_LIST_IGNORE_LOOP|BLOCKDEV_LIST_IGNORE_VIRTUAL)) { + r = device_sysname_startswith(dev, "loop"); + if (r < 0) { + log_device_warning_errno(dev, r, "Failed to check device name of discovered block device '%s', ignoring: %m", node); + goto skipped; + } + + is_loop = r > 0; + if (is_loop && FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_LOOP)) + goto no_match; + } + + /* Loopback devices are virtual devices too, but they are covered by BLOCKDEV_LIST_IGNORE_LOOP above, + * hence exclude them here. */ + if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_VIRTUAL) && !is_loop) { const char *devpath; r = sd_device_get_devpath(dev, &devpath); diff --git a/src/shared/blockdev-list.h b/src/shared/blockdev-list.h index e79ad74a76..c50745abbb 100644 --- a/src/shared/blockdev-list.h +++ b/src/shared/blockdev-list.h @@ -6,16 +6,17 @@ typedef enum BlockDevListFlags { BLOCKDEV_LIST_SHOW_SYMLINKS = 1 << 0, /* Pick up symlinks to block devices too */ BLOCKDEV_LIST_REQUIRE_PARTITION_SCANNING = 1 << 1, /* Only consider block devices with partition scanning */ - BLOCKDEV_LIST_IGNORE_VIRTUAL = 1 << 2, /* Ignore virtual devices (devices under /sys/devices/virtual/) */ - BLOCKDEV_LIST_IGNORE_PARTITIONS = 1 << 3, /* Ignore partitions */ - BLOCKDEV_LIST_IGNORE_ZRAM = 1 << 4, /* Ignore ZRAM */ - BLOCKDEV_LIST_IGNORE_ROOT = 1 << 5, /* Ignore the block device we are currently booted from */ - BLOCKDEV_LIST_IGNORE_EMPTY = 1 << 6, /* Ignore disks of zero size (usually drives without a medium) */ - BLOCKDEV_LIST_IGNORE_READ_ONLY = 1 << 7, /* Ignore read-only block devices */ - BLOCKDEV_LIST_REQUIRE_LUKS = 1 << 8, /* Only consider block devices with LUKS superblocks */ - BLOCKDEV_LIST_METADATA = 1 << 9, /* Fill in model, vendor, subsystem, read_only */ - BLOCKDEV_LIST_DISKSEQ = 1 << 10, /* Fill in the diskseq field */ - BLOCKDEV_LIST_RW_STATS = 1 << 11, /* Fill in read and write measurements */ + BLOCKDEV_LIST_IGNORE_VIRTUAL = 1 << 2, /* Ignore virtual devices (devices under /sys/devices/virtual/), except loopback devices */ + BLOCKDEV_LIST_IGNORE_LOOP = 1 << 3, /* Ignore loopback devices */ + BLOCKDEV_LIST_IGNORE_PARTITIONS = 1 << 4, /* Ignore partitions */ + BLOCKDEV_LIST_IGNORE_ZRAM = 1 << 5, /* Ignore ZRAM */ + BLOCKDEV_LIST_IGNORE_ROOT = 1 << 6, /* Ignore the block device we are currently booted from */ + BLOCKDEV_LIST_IGNORE_EMPTY = 1 << 7, /* Ignore disks of zero size (usually drives without a medium) */ + BLOCKDEV_LIST_IGNORE_READ_ONLY = 1 << 8, /* Ignore read-only block devices */ + BLOCKDEV_LIST_REQUIRE_LUKS = 1 << 9, /* Only consider block devices with LUKS superblocks */ + BLOCKDEV_LIST_METADATA = 1 << 10, /* Fill in model, vendor, subsystem, read_only */ + BLOCKDEV_LIST_DISKSEQ = 1 << 11, /* Fill in the diskseq field */ + BLOCKDEV_LIST_RW_STATS = 1 << 12, /* Fill in read and write measurements */ } BlockDevListFlags; /* The "dynamic" filters – ones the kernel can flip at runtime via sysattrs on a live device. From 647fa4032714a077286dc57941731bedade94fdd Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 2 Sep 2026 12:16:14 +0200 Subject: [PATCH 2/7] blockdev-list: add blockdev_list_full() which allows passing in a pre-determined pair of root_devno/whole_root_devno --- src/shared/blockdev-list.c | 12 +++++++++--- src/shared/blockdev-list.h | 11 ++++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/shared/blockdev-list.c b/src/shared/blockdev-list.c index e44332dc60..d46aaa8828 100644 --- a/src/shared/blockdev-list.c +++ b/src/shared/blockdev-list.c @@ -355,7 +355,13 @@ skipped: return BLOCKDEV_LIST_MATCH_SKIPPED; } -int blockdev_list(BlockDevListFlags flags, BlockDevice **ret_devices, size_t *ret_n_devices) { +int blockdev_list_full( + BlockDevListFlags flags, + dev_t root_devno, + dev_t whole_root_devno, + BlockDevice **ret_devices, + size_t *ret_n_devices) { + _cleanup_(sd_device_enumerator_unrefp) sd_device_enumerator *e = NULL; int r; @@ -368,8 +374,8 @@ int blockdev_list(BlockDevListFlags flags, BlockDevice **ret_devices, size_t *re size_t n = 0; CLEANUP_ARRAY(l, n, block_device_array_free); - dev_t root_devno = 0, whole_root_devno = 0; - (void) blockdev_list_get_root_devnos(flags, &root_devno, &whole_root_devno); + if (root_devno == 0 && whole_root_devno == 0) + (void) blockdev_list_get_root_devnos(flags, &root_devno, &whole_root_devno); if (sd_device_enumerator_new(&e) < 0) return log_oom(); diff --git a/src/shared/blockdev-list.h b/src/shared/blockdev-list.h index c50745abbb..d1146208c5 100644 --- a/src/shared/blockdev-list.h +++ b/src/shared/blockdev-list.h @@ -71,4 +71,13 @@ int blockdev_list_one( dev_t whole_root_devno, BlockDevice *ret); -int blockdev_list(BlockDevListFlags flags, BlockDevice **ret_devices, size_t *ret_n_devices); +int blockdev_list_full( + BlockDevListFlags flags, + dev_t root_devno, + dev_t whole_root_devno, + BlockDevice **ret_devices, + size_t *ret_n_devices); + +static inline int blockdev_list(BlockDevListFlags flags, BlockDevice **ret_devices, size_t *ret_n_devices) { + return blockdev_list_full(flags, 0, 0, ret_devices, ret_n_devices); +} From ce8d684ed843e632731649ba15e6374ace12383e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 31 Aug 2026 15:36:01 +0200 Subject: [PATCH 3/7] sysinstall: optionally, automatically pick target block device after a settle timeout Let's make sysinstall useful for truly unattended installations, and let it it automatically pick a target disk after some settlement time-out. --- docs/ENVIRONMENT.md | 11 + man/systemd-sysinstall.xml | 36 +- shell-completion/bash/systemd-sysinstall | 7 +- shell-completion/zsh/_systemd-sysinstall | 2 + src/repart/repart-list-candidate-devices.c | 2 + src/sysinstall/meson.build | 5 +- src/sysinstall/sysinstall-auto-device.c | 452 +++++++++++++++++++++ src/sysinstall/sysinstall-auto-device.h | 6 + src/sysinstall/sysinstall.c | 31 ++ 9 files changed, 547 insertions(+), 5 deletions(-) create mode 100644 src/sysinstall/sysinstall-auto-device.c create mode 100644 src/sysinstall/sysinstall-auto-device.h diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index da24c169f7..3519d44918 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -735,6 +735,17 @@ SYSTEMD_HOME_DEBUG_SUFFIX=foo \ to override the filesystem type for all partitions with `Type=root`, you can set `SYSTEMD_REPART_OVERRIDE_FSTYPE_ROOT=ext4`. +`systemd-sysinstall`: + +* `$SYSTEMD_SYSINSTALL_PERMIT_AUTO_TARGET_LOOP` – takes a boolean. If true, + loopback block devices are considered as candidates when the installation + target device is picked automatically (i.e. when `--device-auto` is used). + By default, loopback devices are never picked automatically, since they are + hardly ever the disk an unattended installation is supposed to end up on. + This is primarily useful for testing purposes, where loopback devices are + used as stand-ins for real disks. Only honoured if `systemd-sysinstall` is + not invoked with elevated privileges (i.e. via `secure_getenv()`). + `systemd-nspawn`, `systemd-networkd`: * `$SYSTEMD_FIREWALL_BACKEND` – takes a string, either `iptables` or diff --git a/man/systemd-sysinstall.xml b/man/systemd-sysinstall.xml index 228e7e2bff..c1ad278128 100644 --- a/man/systemd-sysinstall.xml +++ b/man/systemd-sysinstall.xml @@ -42,7 +42,8 @@ It prompts the user for the target disk to install the OS on. (Unless the block device - is already specified on the command line.) + is already specified on the command line, or is picked automatically via + .) It validates whether the disk is suitable (i.e. large enough, and with enough free/unpartitioned space) for an OS installation. If it is generally suitable the user is prompted if they @@ -110,6 +111,39 @@ + + + + Automatically pick the target disk instead of prompting for it or taking it from the + command line. The tool watches the block devices of the system (via udev) and waits for the list of + candidate disks to settle: the timeout configured with + starts at invocation and is restarted whenever a block device appears, disappears or changes. Once + it expires without further changes, the single candidate disk present at that moment is selected as + installation target. If no candidate disk is present when the timeout expires, or if at any time + more than one candidate disk is present, the installation fails. Candidate disks are those + systemd-repart8 + would offer in the interactive prompt (i.e. that support partition scanning, are writable, not the + disk the running system is booted from, and contain a medium). If the disk the running system is + booted from cannot be determined – and hence cannot reliably be excluded from the candidate set – + the automatic selection is refused. Cannot be combined with an explicitly + specified block device. This is primarily useful together with , + and , for fully unattended + installations. + + + + + + + + Configures the settle timeout for , i.e. for how long + the list of candidate disks must remain unchanged before a disk is selected. Takes a time span, see + systemd.time7. + Defaults to 30s. + + + + diff --git a/shell-completion/bash/systemd-sysinstall b/shell-completion/bash/systemd-sysinstall index 600e2aa939..631acbdb21 100644 --- a/shell-completion/bash/systemd-sysinstall +++ b/shell-completion/bash/systemd-sysinstall @@ -32,8 +32,9 @@ _systemd_sysinstall() { local comps local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} words cword local -A OPTS=( - [STANDALONE]='-h --help --version' - [ARG]='--welcome + [STANDALONE]='-h --help --version --device-auto' + [ARG]='--device-auto-timeout + --welcome --chrome --erase --confirm @@ -68,7 +69,7 @@ _systemd_sysinstall() { comps=$(compgen -A file -- "$cur") compopt -o filenames ;; - --set-credential) + --set-credential|--device-auto-timeout) comps='' ;; esac diff --git a/shell-completion/zsh/_systemd-sysinstall b/shell-completion/zsh/_systemd-sysinstall index 039e4bf2a7..0f1fa7c2c0 100644 --- a/shell-completion/zsh/_systemd-sysinstall +++ b/shell-completion/zsh/_systemd-sysinstall @@ -11,6 +11,8 @@ _systemd-sysinstall_devices() { _arguments \ '(- *)'{-h,--help}'[Show help text]' \ '(- *)--version[Show package version]' \ + '--device-auto[Automatically pick the target disk once the device list settled]' \ + '--device-auto-timeout=[Settle timeout for --device-auto]:time span' \ '--welcome=[Show welcome text]:boolean:(yes no)' \ '--chrome=[Show colored bars at top and bottom of the terminal]:boolean:(yes no)' \ '--erase=[Erase target disk before installation]:boolean:(yes no auto)' \ diff --git a/src/repart/repart-list-candidate-devices.c b/src/repart/repart-list-candidate-devices.c index 5e5c037ef9..ea93006915 100644 --- a/src/repart/repart-list-candidate-devices.c +++ b/src/repart/repart-list-candidate-devices.c @@ -157,6 +157,8 @@ int vl_method_list_candidate_devices( if (r != 0) return r; + /* The filtering below should probably be kept in sync with the one done in src/sysinstall/sysinstall-auto-device.c! */ + const BlockDevListFlags listflags = BLOCKDEV_LIST_SHOW_SYMLINKS| BLOCKDEV_LIST_REQUIRE_PARTITION_SCANNING| diff --git a/src/sysinstall/meson.build b/src/sysinstall/meson.build index 79618d6933..c04e937ad1 100644 --- a/src/sysinstall/meson.build +++ b/src/sysinstall/meson.build @@ -5,7 +5,10 @@ executables += [ 'name' : 'systemd-sysinstall', 'public' : true, 'conditions' : ['ENABLE_SYSINSTALL'], - 'sources' : files('sysinstall.c'), + 'sources' : files( + 'sysinstall.c', + 'sysinstall-auto-device.c', + ), }, ] diff --git a/src/sysinstall/sysinstall-auto-device.c b/src/sysinstall/sysinstall-auto-device.c new file mode 100644 index 0000000000..e1a46251db --- /dev/null +++ b/src/sysinstall/sysinstall-auto-device.c @@ -0,0 +1,452 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-daemon.h" +#include "sd-device.h" +#include "sd-event.h" + +#include "alloc-util.h" +#include "blockdev-list.h" +#include "blockdev-util.h" +#include "device-util.h" +#include "env-util.h" +#include "event-util.h" +#include "format-util.h" +#include "glyph-util.h" +#include "log.h" +#include "path-util.h" +#include "string-util.h" +#include "sysinstall-auto-device.h" +#include "time-util.h" + +/* Keep in sync with the filter set io.systemd.Repart.ListCandidateDevices applies (see + * src/repart/repart-list-candidate-devices.c), which is what the interactive device prompt is populated + * from, so that the automatic pick never picks a device the prompt wouldn't offer. On top of that we + * ignore devices without a medium (i.e. of zero size) – a drive without a disk in it is not something we + * can install to, and should not be counted as a candidate. BLOCKDEV_LIST_METADATA is only there so the + * log messages can mention vendor/model. Loopback devices are ignored on top of that (unless the + * $SYSTEMD_SYSINSTALL_PERMIT_AUTO_TARGET_LOOP env var says otherwise, see auto_device_list_flags()). */ +#define AUTO_DEVICE_LIST_FLAGS \ + (BLOCKDEV_LIST_REQUIRE_PARTITION_SCANNING | \ + BLOCKDEV_LIST_IGNORE_ZRAM | \ + BLOCKDEV_LIST_IGNORE_READ_ONLY | \ + BLOCKDEV_LIST_IGNORE_ROOT | \ + BLOCKDEV_LIST_IGNORE_EMPTY | \ + BLOCKDEV_LIST_DISKSEQ | \ + BLOCKDEV_LIST_METADATA) + +/* How often to log about the remaining time while waiting for the device list to settle */ +#define COUNTDOWN_INTERVAL_USEC (5 * USEC_PER_SEC) + +typedef struct AutoDeviceContext { + sd_event *event; + sd_device_monitor *monitor; + sd_event_source *settle_timer; + sd_event_source *countdown_timer; + char *candidate; /* Device node of the single candidate seen so far, or NULL if none */ + uint64_t candidate_diskseq; /* Diskseq of the device */ + dev_t root_devno, whole_root_devno; + usec_t settle_timeout; + BlockDevListFlags flags; +} AutoDeviceContext; + +static BlockDevListFlags auto_device_list_flags(void) { + int r; + + /* A loopback device is hardly ever the disk an unattended installation is supposed to end up on, + * but it's what tests use as stand-in for a real disk. Hence ignore loopback devices by default, + * but allow explicitly permitting them via an environment variable. */ + r = secure_getenv_bool("SYSTEMD_SYSINSTALL_PERMIT_AUTO_TARGET_LOOP"); + if (r < 0 && r != -ENXIO) + log_warning_errno(r, "Failed to parse $SYSTEMD_SYSINSTALL_PERMIT_AUTO_TARGET_LOOP, ignoring: %m"); + + return AUTO_DEVICE_LIST_FLAGS | (r > 0 ? 0 : BLOCKDEV_LIST_IGNORE_LOOP); +} + +static void auto_device_context_done(AutoDeviceContext *c) { + assert(c); + + c->settle_timer = sd_event_source_disable_unref(c->settle_timer); + c->countdown_timer = sd_event_source_disable_unref(c->countdown_timer); + c->monitor = sd_device_monitor_unref(c->monitor); + c->event = sd_event_unref(c->event); + c->candidate = mfree(c->candidate); +} + +static int auto_device_on_settle(sd_event_source *s, uint64_t usec, void *userdata) { + AutoDeviceContext *c = ASSERT_PTR(userdata); + + /* The candidate list has been stable for the settle timeout. If we have exactly one candidate at + * this point, it's the one. (Two candidates can never be pending here, since auto_device_add() + * terminates the event loop the moment a second one shows up.) */ + + if (!c->candidate) { + log_error("No suitable block device showed up within %s, cannot continue.", + FORMAT_TIMESPAN(c->settle_timeout, 0)); + return sd_event_exit(c->event, -ENODEV); + } + + return sd_event_exit(c->event, 0); +} + +static int auto_device_on_countdown(sd_event_source *s, uint64_t usec, void *userdata) { + AutoDeviceContext *c = ASSERT_PTR(userdata); + int r; + + assert(s); + assert(c->settle_timer); + + /* Logs how much time is left until the settle timer fires, and reschedules itself, unless the + * settle timer fires before the next message would be due anyway. */ + + usec_t deadline; + r = sd_event_source_get_time(c->settle_timer, &deadline); + if (r < 0) + return log_error_errno(r, "Failed to query settle timer: %m"); + + usec_t n; + r = sd_event_now(c->event, CLOCK_MONOTONIC, &n); + if (r < 0) + return log_error_errno(r, "Failed to query event loop time: %m"); + + usec_t remaining = usec_sub_unsigned(deadline, n); + if (remaining <= COUNTDOWN_INTERVAL_USEC) /* The settle timer fires soon enough anyway */ + return 0; + + log_info("Still waiting for the list of candidate devices to settle, %s left.", + FORMAT_TIMESPAN(remaining, USEC_PER_SEC)); + + r = sd_event_source_set_time_relative(s, COUNTDOWN_INTERVAL_USEC); + if (r < 0) + return log_error_errno(r, "Failed to re-arm countdown timer: %m"); + + r = sd_event_source_set_enabled(s, SD_EVENT_ONESHOT); + if (r < 0) + return log_error_errno(r, "Failed to enable countdown timer: %m"); + + return 0; +} + +static int auto_device_arm_countdown(AutoDeviceContext *c) { + int r; + + assert(c); + + /* Schedules the first countdown message, unless the settle timer fires before that anyway. (The + * subsequent messages are scheduled by auto_device_on_countdown() itself.) */ + + if (c->settle_timeout <= COUNTDOWN_INTERVAL_USEC) { + (void) event_source_disable(c->countdown_timer); + return 0; + } + + r = event_reset_time_relative( + c->event, + &c->countdown_timer, + CLOCK_MONOTONIC, + COUNTDOWN_INTERVAL_USEC, + /* accuracy= */ 0, + auto_device_on_countdown, + c, + SD_EVENT_PRIORITY_NORMAL + 1, + "sysinstall-countdown-timer", + /* force_reset= */ true); + if (r < 0) + return log_error_errno(r, "Failed to arm countdown timer: %m"); + + return 0; +} + +static int auto_device_arm_timer(AutoDeviceContext *c) { + int r; + + assert(c); + + /* (Re)starts the settle clock. Use a priority below the device monitor's, so that uevents that are + * already queued are always dispatched before the timer fires. */ + + bool initial = !c->settle_timer; + + r = event_reset_time_relative( + c->event, + &c->settle_timer, + CLOCK_MONOTONIC, + c->settle_timeout, + /* accuracy= */ 0, + auto_device_on_settle, + c, + SD_EVENT_PRIORITY_NORMAL + 1, + "sysinstall-settle-timer", + /* force_reset= */ true); + if (r < 0) + return log_error_errno(r, "Failed to arm settle timer: %m"); + + /* Log the wait at INFO only when we start waiting; re-arms happen once per change to the candidate + * set, and are already accompanied by a message about that change. */ + log_full(initial ? LOG_INFO : LOG_DEBUG, + "Waiting %s for the list of candidate devices to settle...", + FORMAT_TIMESPAN(c->settle_timeout, 0)); + + return auto_device_arm_countdown(c); +} + +static int auto_device_get_node(AutoDeviceContext *c, char **ret) { + assert(c); + assert(c->candidate); + assert(ret); + + /* Return a by-diskseq path for the disk, if we have it */ + + if (c->candidate_diskseq != UINT64_MAX) { + if (asprintf(ret, "/dev/disk/by-diskseq/%" PRIu64, c->candidate_diskseq) < 0) + return log_oom(); + + return 0; + } + + if (strdup_to(ret, c->candidate) < 0) + return log_oom(); + + return 0; +} + +static int auto_device_add(AutoDeviceContext *c, const BlockDevice *d) { + int r; + + assert(c); + assert(d); + assert(d->node); + + if (c->candidate) { + if (!path_equal(c->candidate, d->node)) { + log_error("Multiple candidate block devices found (%s, %s), refusing to pick one " + "automatically. Please specify the target device explicitly.", + c->candidate, d->node); + return sd_event_exit(c->event, -ENOTUNIQ); + } + + /* Already known, e.g. a "change" uevent for our candidate */ + if (c->candidate_diskseq == d->diskseq) + return 0; /* no change */ + + /* Same node, but a different diskseq: the medium behind the node is not the one we saw + * before (e.g. medium swapped, or a "remove" uevent got lost). We are going to be + * destructive on this device, hence don't silently switch the target, but treat this like a + * new candidate: announce it below and restart the settle clock. */ + log_info("Medium of candidate device %s changed (diskseq %" PRIu64 " %s %" PRIu64 ").", + c->candidate, c->candidate_diskseq, glyph(GLYPH_ARROW_RIGHT), d->diskseq); + } else { + c->candidate = strdup(d->node); + if (!c->candidate) + return log_oom(); + } + + c->candidate_diskseq = d->diskseq; + + _cleanup_free_ char *description = NULL; + if (!isempty(d->vendor) && !strextend_with_separator(&description, " ", d->vendor)) + return log_oom(); + if (!isempty(d->model) && !strextend_with_separator(&description, " ", d->model)) + return log_oom(); + if (d->size != UINT64_MAX && !strextend_with_separator(&description, ", ", FORMAT_BYTES(d->size))) + return log_oom(); + + _cleanup_free_ char *n = NULL; + r = auto_device_get_node(c, &n); + if (r < 0) + return r; + + log_info("Found candidate device %s%s%s%s.", + n, + description ? " (" : "", strempty(description), description ? ")" : ""); + + return 1; /* positive change */ +} + +static int auto_device_remove(AutoDeviceContext *c, sd_device *dev) { + int r; + + assert(c); + assert(dev); + + if (!c->candidate) + return 0; /* no change */ + + const char *node; + r = sd_device_get_devname(dev, &node); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to acquire device node of block device: %m"); + + if (!path_equal(c->candidate, node)) + return 0; /* no change */ + + log_info("Candidate device %s disappeared.", node); + c->candidate = mfree(c->candidate); + c->candidate_diskseq = UINT64_MAX; + + return 1; /* negative change */ +} + +static int auto_device_on_uevent(sd_device_monitor *monitor, sd_device *dev, void *userdata) { + AutoDeviceContext *c = ASSERT_PTR(userdata); + int r; + + assert(dev); + + /* Note that any failure returned from this callback terminates the event loop (and hence the + * automatic pick) with that error, see the sd_event_source_set_exit_on_failure() call in + * sysinstall_auto_pick_block_device(). We must never continue with uevent processing disabled: the + * settle timer would keep ticking and commit to the current candidate, while a second disk showing + * up – i.e. the very thing that must make us refuse – would go unnoticed. */ + + sd_device_action_t action; + r = sd_device_get_action(dev, &action); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to acquire uevent action of block device: %m"); + + if (action == SD_DEVICE_REMOVE) + r = auto_device_remove(c, dev); + else { + /* All non-REMOVE actions mean "device exists afterwards". Re-run the full filter set, the + * tri-state distinguishes "never interesting" from "was/is a candidate, but currently + * fails a dynamic filter (e.g. medium ejected, read-only flipped on)". */ + _cleanup_(block_device_done) BlockDevice d = BLOCK_DEVICE_NULL; + r = blockdev_list_one(dev, c->flags, c->root_devno, c->whole_root_devno, &d); + if (r < 0) + return r; + + switch (r) { + + case BLOCKDEV_LIST_MATCH_NO: + case BLOCKDEV_LIST_MATCH_SKIPPED: + case BLOCKDEV_LIST_MATCH_FILTERED: + r = auto_device_remove(c, dev); + break; + + case BLOCKDEV_LIST_MATCH_YES: + r = auto_device_add(c, &d); + break; + + default: + assert_not_reached(); + } + } + if (r <= 0) + return r; + + /* Restart the timer if auto_device_add()/auto_device_remove() indicated a relevant change by + * returning > 0. */ + return auto_device_arm_timer(c); +} + +int sysinstall_auto_pick_block_device(usec_t settle_timeout, char **ret_node) { + int r; + + assert(ret_node); + + /* Watches the block device tree until it has been quiet for 'settle_timeout' (measured from + * invocation or from the last block device uevent, whichever is later), and then returns the one + * candidate device present. Fails immediately if more than one candidate is ever present, and + * fails after the timeout if none is. */ + + _cleanup_(auto_device_context_done) AutoDeviceContext c = { + .settle_timeout = settle_timeout, + .candidate_diskseq = UINT64_MAX, + .flags = auto_device_list_flags(), + }; + + r = sd_event_new(&c.event); + if (r < 0) + return log_error_errno(r, "Failed to allocate event loop: %m"); + + /* Make SIGINT/SIGTERM terminate the loop cleanly, with an error code we recognize below */ + r = sd_event_add_signal(c.event, /* ret= */ NULL, SIGINT|SD_EVENT_SIGNAL_PROCMASK, + /* callback= */ NULL, INT_TO_PTR(-ECANCELED)); + if (r < 0) + return log_error_errno(r, "Failed to add SIGINT handler: %m"); + r = sd_event_add_signal(c.event, /* ret= */ NULL, SIGTERM|SD_EVENT_SIGNAL_PROCMASK, + /* callback= */ NULL, INT_TO_PTR(-ECANCELED)); + if (r < 0) + return log_error_errno(r, "Failed to add SIGTERM handler: %m"); + + /* Determine the disk the running system is booted from, so that we never pick it as installation + * target. In the interactive flow a failure to determine it merely changes which devices a human is + * offered, but here it decides what may be erased unattended, hence refuse operation if the booted + * disk cannot be determined (e.g. NFS or multi-device root file systems). */ + r = blockdev_get_root(LOG_ERR, &c.root_devno); + if (r < 0) + return r; + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENODEV), + "Unable to determine the disk the running system is booted from, " + "cannot reliably exclude it from the candidate set, refusing."); + + r = block_get_whole_disk(c.root_devno, &c.whole_root_devno); + if (r < 0) + return log_error_errno(r, "Failed to determine whole disk of the root block device: %m"); + + /* Start the monitor *before* the initial enumeration so we don't lose events that fire during the + * enumeration window. Seeing the same device twice is harmless, auto_device_add() is idempotent. */ + r = sd_device_monitor_new(&c.monitor); + if (r < 0) + return log_error_errno(r, "Failed to allocate block device monitor: %m"); + + (void) sd_device_monitor_set_description(c.monitor, "sysinstall-auto-device"); + + r = sd_device_monitor_filter_add_match_subsystem_devtype(c.monitor, "block", /* devtype= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to configure block device monitor filter: %m"); + + r = sd_device_monitor_attach_event(c.monitor, c.event); + if (r < 0) + return log_error_errno(r, "Failed to attach block device monitor to event loop: %m"); + + r = sd_device_monitor_start(c.monitor, auto_device_on_uevent, &c); + if (r < 0) + return log_error_errno(r, "Failed to start block device monitor: %m"); + + /* By default sd-event just disables an event source whose callback fails. That would leave us + * running blind: the settle timer would keep going and eventually commit to whatever candidate we + * had, while further devices – in particular a second candidate – could no longer be observed. + * Hence make any failure in the uevent callback terminate the event loop (and thus the automatic + * pick) instead. */ + r = sd_event_source_set_exit_on_failure(sd_device_monitor_get_event_source(c.monitor), true); + if (r < 0) + return log_error_errno(r, "Failed to enable exit-on-failure for block device monitor: %m"); + + BlockDevice *l = NULL; + size_t n = 0; + CLEANUP_ARRAY(l, n, block_device_array_free); + + r = blockdev_list_full(c.flags, c.root_devno, c.whole_root_devno, &l, &n); + if (r < 0) + return r; + + FOREACH_ARRAY(d, l, n) { + r = auto_device_add(&c, d); + if (r < 0) + return r; + } + + /* The settle clock starts ticking now, regardless of whether we found something already */ + r = auto_device_arm_timer(&c); + if (r < 0) + return r; + + /* Tell the service manager (if any) that we are now watching for devices, so that whoever runs us + * knows it's safe to plug in the target disk now. */ + (void) sd_notify(/* unset_environment= */ false, "READY=1"); + + r = sd_event_loop(c.event); + if (r == -ECANCELED) + return log_error_errno(r, "Installation cancelled."); + if (r < 0) + return r; /* Already logged about */ + + r = auto_device_get_node(&c, ret_node); + if (r < 0) + return r; + + log_info("Selected '%s' as installation target.", *ret_node); + + return 0; +} diff --git a/src/sysinstall/sysinstall-auto-device.h b/src/sysinstall/sysinstall-auto-device.h new file mode 100644 index 0000000000..4481ab5e03 --- /dev/null +++ b/src/sysinstall/sysinstall-auto-device.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "forward.h" + +int sysinstall_auto_pick_block_device(usec_t settle_timeout, char **ret_node); diff --git a/src/sysinstall/sysinstall.c b/src/sysinstall/sysinstall.c index f5a4cb9bd4..dbdbd34b76 100644 --- a/src/sysinstall/sysinstall.c +++ b/src/sysinstall/sysinstall.c @@ -43,12 +43,16 @@ #include "prompt-util.h" #include "string-table.h" #include "strv.h" +#include "sysinstall-auto-device.h" #include "terminal-util.h" +#include "time-util.h" #include "varlink-io.systemd.SysInstall.h" #include "varlink-util.h" #include "verbs.h" static char *arg_node = NULL; +static bool arg_device_auto = false; +static usec_t arg_device_auto_timeout = 30 * USEC_PER_SEC; static bool arg_welcome = true; static int arg_erase = -1; /* tri-state */ static bool arg_confirm = true; @@ -179,6 +183,18 @@ static int parse_argv(int argc, char *argv[]) { OPTION_COMMON_VERSION: return version(); + OPTION_LONG("device-auto", NULL, "Automatically pick the target disk, once exactly one candidate is present and the device list settled"): + arg_device_auto = true; + break; + + OPTION_LONG("device-auto-timeout", "SECS", "How long the list of candidate disks must remain unchanged before a disk is picked"): + r = parse_sec(opts.arg, &arg_device_auto_timeout); + if (r < 0) + return log_error_errno(r, "Failed to parse --device-auto-timeout= parameter: %s", opts.arg); + if (arg_device_auto_timeout == USEC_INFINITY) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--device-auto-timeout= cannot be infinite, refusing."); + break; + OPTION_LONG("welcome", "no", "Disable the welcome text"): r = parse_boolean_argument("--welcome=", opts.arg, &arg_welcome); if (r < 0) @@ -285,6 +301,9 @@ static int parse_argv(int argc, char *argv[]) { if (strv_length(args) > 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments."); if (!strv_isempty(args)) { + if (arg_device_auto) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot combine --device-auto with an explicitly specified device, refusing."); + arg_node = strdup(args[0]); if (!arg_node) return log_oom(); @@ -2133,6 +2152,18 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; + r = validate_run(&repart_link, arg_node); + if (r < 0) + return r; + } else if (arg_device_auto) { + r = print_welcome(&mute_console_link); + if (r < 0) + return r; + + r = sysinstall_auto_pick_block_device(arg_device_auto_timeout, &arg_node); + if (r < 0) + return r; + r = validate_run(&repart_link, arg_node); if (r < 0) return r; From 9b7f2c64b1ae0ea41f744dc8b182f185a52a25e6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 31 Aug 2026 16:26:09 +0200 Subject: [PATCH 4/7] sysinstall: send out READY=1 when we start our thing This is useful so that whoever invokes the tool knows that the general parameters checked out, and the tool now proceeds with actually doing the work. --- src/sysinstall/sysinstall.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sysinstall/sysinstall.c b/src/sysinstall/sysinstall.c index dbdbd34b76..96023a9b37 100644 --- a/src/sysinstall/sysinstall.c +++ b/src/sysinstall/sysinstall.c @@ -4,6 +4,7 @@ #include #include +#include "sd-daemon.h" #include "sd-json.h" #include "sd-varlink.h" @@ -2239,6 +2240,10 @@ static int run(int argc, char *argv[]) { putchar('\n'); + /* All questions asked, we are about to begin the installation. Let the service manager know (it's + * fine if we sent READY=1 before already, e.g. from the device auto-pick logic). */ + (void) sd_notify(/* unset_environment= */ false, "READY=1"); + r = sysinstall_context_run(&context); if (r < 0) return r; From 0bb2f138f4d271d80e663b2baa2d1b67c2ff31db Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 31 Aug 2026 16:46:55 +0200 Subject: [PATCH 5/7] sysinstall: correct end marker log message Copypasta... --- src/sysinstall/sysinstall.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sysinstall/sysinstall.c b/src/sysinstall/sysinstall.c index 96023a9b37..ce5775e9b9 100644 --- a/src/sysinstall/sysinstall.c +++ b/src/sysinstall/sysinstall.c @@ -2098,7 +2098,7 @@ static void end_marker(void) { if (!arg_welcome) return; - printf("\n%sExiting first boot settings tool.%s\n\n", ansi_grey(), ansi_normal()); + printf("\n%sExiting system installer tool.%s\n\n", ansi_grey(), ansi_normal()); fflush(stdout); } From 236e7d8a4c4e872d724431a6f42f6f1725305cdd Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 31 Aug 2026 16:26:27 +0200 Subject: [PATCH 6/7] ci: testcase for new auto-pick mode of sysinstall --- test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh | 391 ++++++++++++++++++ 1 file changed, 391 insertions(+) diff --git a/test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh b/test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh index 51be486995..be4dd94488 100755 --- a/test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh +++ b/test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh @@ -46,6 +46,12 @@ CRED_VALUE_BASE64=$(echo -n "$CRED_VALUE" | base64 -w0) cleanup() { set +e + # Stop the installer first: it might still be writing to the devices and files removed below. + if [[ -n "${UNIT:-}" ]]; then + systemctl stop "$UNIT" + systemctl reset-failed "$UNIT" + UNIT="" + fi if [[ "$MOUNTED" -eq 1 ]]; then umount -R "$WORKDIR/mnt" MOUNTED=0 @@ -54,9 +60,92 @@ cleanup() { systemd-dissect --detach "$LOOPDEV" LOOPDEV="" fi + if [[ -n "${TARGETDEV:-}" ]]; then + systemd-dissect --detach "$TARGETDEV" + TARGETDEV="" + fi + if [[ -n "${TARGETDEV2:-}" ]]; then + systemd-dissect --detach "$TARGETDEV2" + TARGETDEV2="" + fi rm -rf "$WORKDIR" } +check_device_auto_environment() { + # Safety check for the --device-auto testcases: systemd-sysinstall must be able to determine the disk + # the running system is booted from (it refuses the automatic pick otherwise), and no candidate disk + # may be around initially, since the testcases rely on the disks they attach themselves being the only + # candidates — otherwise the automatic pick would either fail or – worse – install to some unexpected + # disk. Query the candidate list the same way systemd-sysinstall does (i.e. with the very same filter + # set, except that systemd-sysinstall additionally ignores loopback devices unless + # $SYSTEMD_SYSINSTALL_PERMIT_AUTO_TARGET_LOOP is set, which the testcases do), instead of approximating + # the filters here. + # + # Returns non-zero (and the testcase is to be skipped) only if the booted disk cannot be determined, + # i.e. if the feature is genuinely unusable in this environment. Stray candidate disks on the other + # hand mean the test environment is broken (e.g. a loopback device leaked by an earlier testcase), + # which must not silently turn the --device-auto testcases into no-ops, hence fail hard in that case. + local out + + if ! bootctl -RR >/dev/null; then + echo "Cannot determine the disk the system is booted from." >&2 + return 1 + fi + + if out="$(varlinkctl call --more "exec:$(command -v systemd-repart)" io.systemd.Repart.ListCandidateDevices '{"ignoreRoot":true,"ignoreEmpty":true}' 2>&1)"; then + echo "Unexpected candidate disks present, test environment is broken: $out" >&2 + exit 1 + fi + + if [[ "$out" != *"io.systemd.Repart.NoCandidateDevices"* ]]; then + echo "Failed to enumerate candidate disks: $out" >&2 + exit 1 + fi +} + +start_device_auto_installer() { + # Starts the installer as a notify service with --device-auto and the specified settle timeout (plus + # any further arguments). systemd-sysinstall sends READY=1 once its device monitor is set up, hence + # systemd-run only returns once we can be sure that a disk appearing from now on will be noticed. + # Loopback devices are not considered as candidates by default, hence explicitly permit them, the + # target disks of the testcases are loopback devices. + local settle_timeout="${1:?}" + shift + + UNIT="sysinstall-device-auto-$RANDOM.service" + systemd-run \ + --unit="$UNIT" \ + --service-type=notify \ + --property=RemainAfterExit=yes \ + --setenv=SYSTEMD_SYSINSTALL_PERMIT_AUTO_TARGET_LOOP=1 \ + --quiet \ + -- \ + systemd-sysinstall \ + --welcome=no \ + --chrome=no \ + --confirm=no \ + --summary=no \ + --erase=yes \ + --variables=no \ + --reboot=no \ + --mute-console=no \ + --copy-locale=no \ + --copy-keymap=no \ + --copy-timezone=no \ + --device-auto \ + --device-auto-timeout="$settle_timeout" \ + "$@" +} + +wait_for_device_auto_installer() { + # Waits for the installer started by start_device_auto_installer() to finish, and dumps its output, so + # that it ends up in the test log whatever happened — hence don't abort right away if the wait times + # out, the assertions of the caller will catch that case after the output has been dumped. + timeout "${1:?}" bash -c "while [[ \"\$(systemctl show -P SubState '$UNIT')\" == running ]]; do sleep 1; done" || : + journalctl --sync + journalctl --no-pager -u "$UNIT" +} + create_fake_os_source_tree() { # 1) Build a small fake "OS source" tree. systemd-sysinstall picks this up via # the repart.sysinstall.d definitions: CopyFiles= seeds the new root @@ -222,4 +311,306 @@ testcase_sysinstall_varlink_basic() { validate_image } +testcase_sysinstall_device_auto() { + WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)" + LOOPDEV="" + TARGETDEV="" + UNIT="" + MOUNTED=0 + + echo "WORKDIR=$WORKDIR" + + trap cleanup RETURN + + if ! check_device_auto_environment; then + echo "Environment not suitable for --device-auto tests, skipping." >&2 + return 0 + fi + + create_fake_os_source_tree + + # Put a bare file system on the target image, so that systemd-dissect --attach accepts it as an + # image later on (an all-zero file is not a valid image). Don't put a partition table on it though: + # the resulting loopback device shall have no partition block devices, so that nothing is in the + # way when systemd-repart adds the new ones. systemd-sysinstall erases the file system anyway. + mkfs.ext4 -q -F "$WORKDIR/target.img" + + # Start the installer while no candidate disk exists yet. + start_device_auto_installer 30s \ + --set-credential="marker:$CRED_VALUE" \ + --kernel="$WORKDIR/testuki.efi" \ + --definitions="$DEFS" + + # Only now, 5s later, make the target disk appear: a partition-scanning loopback device backed by + # the target image. It's the sole candidate disk in the VM, hence it must be picked. + sleep 5 + + # The installer must still be waiting at this point, there was nothing to install to so far. + assert_eq "$(systemctl show -P SubState "$UNIT")" "running" + + TARGETDEV="$(systemd-dissect --attach "$WORKDIR/target.img")" + udevadm wait --settle "$TARGETDEV" + + # Wait for the installation to finish, and check that it succeeded. + wait_for_device_auto_installer 180 + assert_eq "$(systemctl show -P SubState "$UNIT")" "exited" + assert_eq "$(systemctl show -P Result "$UNIT")" "success" + + # Release our loopback device again, validate_image attaches the image on its own. + systemd-dissect --detach "$TARGETDEV" + TARGETDEV="" + + validate_image +} + +testcase_sysinstall_device_auto_two_candidates() { + WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)" + LOOPDEV="" + TARGETDEV="" + TARGETDEV2="" + UNIT="" + MOUNTED=0 + + echo "WORKDIR=$WORKDIR" + + trap cleanup RETURN + + if ! check_device_auto_environment; then + echo "Environment not suitable for --device-auto tests, skipping." >&2 + return 0 + fi + + create_fake_os_source_tree + + # Attach two candidate disks, both carrying a bare file system (see testcase_sysinstall_device_auto + # for why), so that we can verify below that neither of them was touched. + mkfs.ext4 -q -F "$WORKDIR/target.img" + truncate -s 512M "$WORKDIR/target2.img" + mkfs.ext4 -q -F "$WORKDIR/target2.img" + + TARGETDEV="$(systemd-dissect --attach "$WORKDIR/target.img")" + TARGETDEV2="$(systemd-dissect --attach "$WORKDIR/target2.img")" + udevadm wait --settle "$TARGETDEV" "$TARGETDEV2" + + # With two candidate disks present the automatic pick must refuse right away, i.e. well before the + # settle timeout expires, and without installing anywhere. + local start="$SECONDS" rc=0 out + out="$(SYSTEMD_SYSINSTALL_PERMIT_AUTO_TARGET_LOOP=1 timeout 60 systemd-sysinstall \ + --welcome=no \ + --chrome=no \ + --confirm=no \ + --summary=no \ + --erase=yes \ + --variables=no \ + --reboot=no \ + --mute-console=no \ + --copy-locale=no \ + --copy-keymap=no \ + --copy-timezone=no \ + --set-credential="marker:$CRED_VALUE" \ + --kernel="$WORKDIR/testuki.efi" \ + --definitions="$DEFS" \ + --device-auto \ + --device-auto-timeout=30s 2>&1)" || rc=$? + echo "$out" + assert_neq "$rc" 0 + assert_neq "$rc" 124 + assert_in "Multiple candidate block devices found" "$out" + assert_le "$((SECONDS - start))" 10 + + # Neither disk may have been touched: the bare file systems must have survived unmodified, in + # particular no partition table may have been written. + assert_eq "$(blkid -p -o value -s TYPE "$TARGETDEV")" "ext4" + assert_eq "$(blkid -p -o value -s TYPE "$TARGETDEV2")" "ext4" +} + +testcase_sysinstall_device_auto_late_second_candidate() { + WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)" + LOOPDEV="" + TARGETDEV="" + TARGETDEV2="" + UNIT="" + MOUNTED=0 + + echo "WORKDIR=$WORKDIR" + + trap cleanup RETURN + + if ! check_device_auto_environment; then + echo "Environment not suitable for --device-auto tests, skipping." >&2 + return 0 + fi + + # Same as testcase_sysinstall_device_auto_two_candidates, but the second disk shows up only while the + # installer is already waiting for the settle timeout, i.e. the refusal must come via the uevent path + # rather than the initial enumeration. + truncate -s 512M "$WORKDIR/target.img" + mkfs.ext4 -q -F "$WORKDIR/target.img" + truncate -s 512M "$WORKDIR/target2.img" + mkfs.ext4 -q -F "$WORKDIR/target2.img" + + start_device_auto_installer 30s + + TARGETDEV="$(systemd-dissect --attach "$WORKDIR/target.img")" + udevadm wait --settle "$TARGETDEV" + sleep 5 + + # A single candidate must not have been picked yet, the settle timeout is far from expired. + assert_eq "$(systemctl show -P SubState "$UNIT")" "running" + + # Now make the second disk appear. This must make the installer refuse right away. + local start="$SECONDS" out + TARGETDEV2="$(systemd-dissect --attach "$WORKDIR/target2.img")" + udevadm wait --settle "$TARGETDEV2" + + wait_for_device_auto_installer 60 + assert_le "$((SECONDS - start))" 10 + assert_neq "$(systemctl show -P SubState "$UNIT")" "running" + assert_neq "$(systemctl show -P Result "$UNIT")" "success" + out="$(journalctl --no-pager -o cat -u "$UNIT")" + assert_in "Multiple candidate block devices found" "$out" + + # Neither disk may have been touched. + assert_eq "$(blkid -p -o value -s TYPE "$TARGETDEV")" "ext4" + assert_eq "$(blkid -p -o value -s TYPE "$TARGETDEV2")" "ext4" +} + +testcase_sysinstall_device_auto_candidate_disappears() { + WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)" + LOOPDEV="" + TARGETDEV="" + UNIT="" + MOUNTED=0 + + echo "WORKDIR=$WORKDIR" + + trap cleanup RETURN + + if ! check_device_auto_environment; then + echo "Environment not suitable for --device-auto tests, skipping." >&2 + return 0 + fi + + # A candidate that appears and disappears again while the installer is waiting must be forgotten + # about: the installer must not pick the stale device node, but restart the settle clock and + # eventually fail for lack of a candidate. + truncate -s 512M "$WORKDIR/target.img" + mkfs.ext4 -q -F "$WORKDIR/target.img" + + start_device_auto_installer 10s + + TARGETDEV="$(systemd-dissect --attach "$WORKDIR/target.img")" + udevadm wait --settle "$TARGETDEV" + sleep 3 + + assert_eq "$(systemctl show -P SubState "$UNIT")" "running" + + local start="$SECONDS" out + systemd-dissect --detach "$TARGETDEV" + TARGETDEV="" + + # The removal restarts the settle clock, hence the failure may come no earlier than a full settle + # timeout after the disk disappeared. + wait_for_device_auto_installer 60 + assert_ge "$((SECONDS - start))" 10 + assert_neq "$(systemctl show -P SubState "$UNIT")" "running" + assert_neq "$(systemctl show -P Result "$UNIT")" "success" + out="$(journalctl --no-pager -o cat -u "$UNIT")" + assert_in "disappeared" "$out" + assert_in "No suitable block device" "$out" +} + +testcase_sysinstall_device_auto_no_candidate() { + if ! check_device_auto_environment; then + echo "Environment not suitable for --device-auto tests, skipping." >&2 + return 0 + fi + + # With no candidate disk around the automatic pick must fail once the settle timeout expires — not + # before it expires, and not by hanging around forever either. + local start="$SECONDS" rc=0 out + out="$(timeout 60 systemd-sysinstall \ + --welcome=no \ + --chrome=no \ + --confirm=no \ + --summary=no \ + --erase=yes \ + --variables=no \ + --reboot=no \ + --mute-console=no \ + --device-auto \ + --device-auto-timeout=5s 2>&1)" || rc=$? + echo "$out" + assert_neq "$rc" 0 + assert_neq "$rc" 124 + assert_in "No suitable block device" "$out" + assert_ge "$((SECONDS - start))" 5 +} + +testcase_sysinstall_device_auto_loop_ignored() { + WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)" + LOOPDEV="" + TARGETDEV="" + UNIT="" + MOUNTED=0 + + echo "WORKDIR=$WORKDIR" + + trap cleanup RETURN + + if ! check_device_auto_environment; then + echo "Environment not suitable for --device-auto tests, skipping." >&2 + return 0 + fi + + # Unless $SYSTEMD_SYSINSTALL_PERMIT_AUTO_TARGET_LOOP is set, loopback devices must not be considered as + # candidates, i.e. a lone loopback disk must not be picked, and the automatic pick must fail once the + # settle timeout expires, exactly as if no disk was around. + truncate -s 512M "$WORKDIR/target.img" + mkfs.ext4 -q -F "$WORKDIR/target.img" + TARGETDEV="$(systemd-dissect --attach "$WORKDIR/target.img")" + udevadm wait --settle "$TARGETDEV" + + local start="$SECONDS" rc=0 out + out="$(timeout 60 systemd-sysinstall \ + --welcome=no \ + --chrome=no \ + --confirm=no \ + --summary=no \ + --erase=yes \ + --variables=no \ + --reboot=no \ + --mute-console=no \ + --device-auto \ + --device-auto-timeout=5s 2>&1)" || rc=$? + echo "$out" + assert_neq "$rc" 0 + assert_neq "$rc" 124 + assert_in "No suitable block device" "$out" + assert_ge "$((SECONDS - start))" 5 + + # The loopback disk must not have been touched. + assert_eq "$(blkid -p -o value -s TYPE "$TARGETDEV")" "ext4" +} + +testcase_sysinstall_device_auto_argument_errors() { + local rc out + + # --device-auto cannot be combined with an explicitly specified target device... + rc=0 + out="$(timeout 30 systemd-sysinstall --device-auto /dev/null 2>&1)" || rc=$? + echo "$out" + assert_neq "$rc" 0 + assert_neq "$rc" 124 + assert_in "Cannot combine --device-auto with an explicitly specified device" "$out" + + # ...and the settle timeout cannot be infinite. + rc=0 + out="$(timeout 30 systemd-sysinstall --device-auto --device-auto-timeout=infinity 2>&1)" || rc=$? + echo "$out" + assert_neq "$rc" 0 + assert_neq "$rc" 124 + assert_in "--device-auto-timeout= cannot be infinite" "$out" +} + run_testcases From 641da198fd37e82bf48416b776f97cc4f755ddb0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 31 Aug 2026 16:43:51 +0200 Subject: [PATCH 7/7] update TODO --- TODO.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/TODO.md b/TODO.md index 9d723d62df..274cf755b9 100644 --- a/TODO.md +++ b/TODO.md @@ -204,12 +204,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later - portabled driving by system credential -- sysinstall: add fully automatic mode that automatically picks target disk, - non-interactively. Should wait to ensure system is up for a certain amount of - minimal time (alternatively: certain amount of time since the last disk - showed up), to ensure disks have shown up before making the decision. Usecase - for this: redfish style server provisioning. - - nspawn: optionally provide a /dev/tpm0 + /dev/tpmrm0 that is backed by swtpm, much like we do in vmspawn. let's us minimize differences between environments systemd runs in. @@ -338,7 +332,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later partition with a portable service partition in one. - **systemd-sysinstall:** - - make systemd-sysinstall itself a varlink service - read installation definition from json file - polkit support in sysinstall - sysinstall: permit driving installer via credentials