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 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/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..d46aaa8828 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); @@ -340,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; @@ -353,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 e79ad74a76..d1146208c5 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. @@ -70,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); +} 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..ce5775e9b9 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" @@ -43,12 +44,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 +184,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 +302,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(); @@ -2078,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); } @@ -2133,6 +2153,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; @@ -2208,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; 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