diff --git a/TODO.md b/TODO.md index 588ff720d4..bcfe489151 100644 --- a/TODO.md +++ b/TODO.md @@ -186,6 +186,17 @@ SPDX-License-Identifier: LGPL-2.1-or-later use as additional search condition. Use case: images that combined a sysext 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 + - add --offline=no mode where we talk to socket based services rather than forking off + - if a user doesn't pick a locale during boot into installer, don't ask again after install, because we suppressed credential propagation + +- repart: add MatchLabel= which matches against partition label, so that we + truly can install different images in parallel + - add "systemctl wait" or so, which does what "systemd-run --wait" does, but for all units. It should be both a way to pin units into memory as well as a wait to retrieve their exit data. @@ -1200,14 +1211,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later - introduce a new group to own TPM devices -- introduce a small "systemd-installer" tool or so, that glues - systemd-repart-as-installer and bootctl-install into one. Would just - interactively ask user for target disk (with completion and so on), and then do - two varlink calls to the the two tools with the right parameters. To support - "offline" operation, optionally invoke the two tools directly as child - processes with varlink communication over socketpair(). This all should be - useful as blueprint for graphical installers which should do the same. - - introduce an option (or replacement) for "systemctl show" that outputs all properties as JSON, similar to busctl's new JSON output. In contrast to that it should skip the variant type string though. @@ -2491,10 +2494,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later that we can sanely copy ESP contents, /usr/ images, and then set up btrfs raid for the root fs to extend/mirror the existing install. This would be very similar to the concept of live-install-through-btrfs-migration. - - add --installer or so, that will interactively ask for a - target disk, maybe ask for confirmation, and install something on disk. Then, - hook that into installer.target or so, so that it can be used to - install/replicate installs - should probably enable btrfs' "temp_fsid" feature for all file systems it creates, as we have no interest in RAID for repart, and it should make sure that we can mount them trivially everywhere. diff --git a/man/rules/meson.build b/man/rules/meson.build index 719838064c..c42a8d47f8 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1218,6 +1218,10 @@ manpages = [ 'systemd-sysext-sysroot.service', 'systemd-sysext.service'], 'ENABLE_SYSEXT'], + ['systemd-sysinstall', + '8', + ['systemd-sysinstall.service'], + 'ENABLE_SYSINSTALL'], ['systemd-system-update-generator', '8', [], ''], ['systemd-system.conf', '5', diff --git a/man/systemd-sysinstall.xml b/man/systemd-sysinstall.xml new file mode 100644 index 0000000000..228e7e2bff --- /dev/null +++ b/man/systemd-sysinstall.xml @@ -0,0 +1,292 @@ + + + + + + + + systemd-sysinstall + systemd + + + + systemd-sysinstall + 8 + + + + systemd-sysinstall + systemd-sysinstall.service + Simple OS installer + + + + + systemd-sysinstall + OPTIONS + BLOCKDEVICE + + + systemd-sysinstall.service + + + + Description + + systemd-sysinstall is a simple terminal and command line based operating system + installer tool. Its primary use-case is to act as an automatically started interactive interface when + booting from an installer medium (e.g. a USB stick), in order to install an OS onto a target + disk. However, it may also be invoked directly from a shell. It executes the following steps: + + + It prompts the user for the target disk to install the OS on. (Unless the block device + is already specified on the command line.) + + 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 + want to erase the disk before installation, or if the OS shall be added to the existing partitions on + the disk (the latter only if enough free/unpartitioned disk space is available). + + It prompts the user whether to register the newly installed OS with the firmware boot option menu. + + It requests confirmation from the user, after showing a summary of the planned OS installation. + + It invokes + systemd-creds1's + encrypt command in order to generate encrypted (TPM locked, if available) system + credential files for a few, very basic system settings of the currently booted system (locale, keymap, + timezone), which it will install on the target disk, parameterizing the invoked kernel. (Or in other + words, it prepares that some settings already in effect on the installer system are propagated securely + onto the new installation.) + + It invokes + systemd-repart8 with + a definitions directory of /usr/lib/repart.sysinstall.d/ (only if populated – if + not will use the default of /usr/lib/repart.d/). This is supposed to set up the + basic OS partition structure on the target disk and copies in basic OS partitions (most importantly the + /usr/ hierarchy). + + It invokes + bootctl1's + link command to install an OS kernel image onto the target disk's ESP/XBOOTLDR, + together with the credential files prepared earlier. + + It invokes + bootctl1's + install command to install the + systemd-boot7 boot + loader onto the target disk's ESP. + + After confirmation, it reboots the system. + + + Note that the prompts/confirmation may be disabled via the command line, enabling fully automatic, + non-interactive installation. See below. + + Note this tool does not interactively query the user for a user to create or a root password to be + set on the target system, under the assumption these questions are better prompted from within the newly + installed system's first boot process, for example via the + systemd-firstboot1 or + systemd-homed-firstboot.service components. Note that if required such settings + may be propagated explicitly via the switch below. + + + + Options + + The following options are understood: + + + + + + + Overrides the directory where systemd-repart shall read its + partition definitions from, in place of the default of + /usr/lib/repart.sysinstall.d/. + + + + + + + + Takes a boolean argument. Controls whether to show the brief welcome text normally + displayed at the beginning of the installation. Defaults to true. + + + + + + + + Takes a boolean argument. Controls whether to show the colored bars at the top and + bottom of the terminal interface. Defaults to true. + + + + + + + + Takes a boolean argument. Controls whether to erase the current contents of the + target disk. If this switch is not used the user is prompted. + + + + + + + + Takes a boolean argument. Controls whether to interactively query the user for + confirmation before initiating the OS installation. Defaults to true. + + + + + + + + Takes a boolean argument. Controls whether to reboot the system after completing the + installation. Defaults to false. + + + + + + + + Takes a boolean argument. Controls whether to register the installed boot loader in + the firmware's boot options database. If not specified the user will be prompted. + + + + + + + + Takes a boolean argument. Controls whether to show a summary of the choices made + before asking for confirmation to proceed with the OS installation. Defaults to true. + + + + + + + + Takes a path to a unified kernel image (UKI). Explicitly selects the kernel image to + install on the target disk. If unspecified the currently booted kernel image is installed on the + target disk. + + + + + + + + Accepts an additional system credential to encrypt (with a key generated on the local + TPM, if available, and the null key otherwise) and place next to the installed kernel image in the + ESP. This may be used to parameterize the installed kernel with arbitrary system credentials. Do not + use this switch for sensitive data (such as passwords), use + instead, see below. May be used multiple times to configure multiple credentials. + + Note that three system credentials are propagated in similar fashion to the target system: + the locale, keymap and timezone. This may be controlled by the relevant + , and + options below. + + See + systemd.system-credentials7 + for a list of well-known system credentials that may be propagated this way. (Note that you may pass + arbitrary additional credentials this way, that can be consumed by any service of your + choice, via the usual system credentials logic.) + + + + + + + + Similar to but reads the credential value from a + file on disk or an AF_UNIX socket in the file system. This is generally + preferable for sensitive data, such as passwords. + + + + + + + + + + These options take boolean parameters. They control whether the indicated system + settings shall be propagated from the currently running system into the new target OS + installation. These options default to true. + + Typically, these three settings are the minimal settings that need to be configured during early + boot of an installer medium in order to make the installer tool accessible to the user. The + systemd-firstboot1 + tool may be used to query the user interactively when the OS install medium is booted for these + properties. By propagating these settings to the target installation via system credentials they do + not need to be queried again on first boot of the new installation. + + + + + + + + Takes a boolean argument. Controls whether to disable kernel and service manager log + output to the console the installer is invoked on temporarily while running, in order to avoid + interleaved output. Defaults to false. + + + + + + + + + + + Exit status + + On success, 0 is returned, and a non-zero failure code otherwise. + + + + Example + + + Invoke the tool for a fully automatic non-interactive OS installation + + systemd-sysinstall \ + /dev/disk/by-id/nvme-Micron_MTFDKBA1T0TFH_214532D0CDA5 \ + --erase=yes \ + --confirm=no \ + --variables=yes \ + --load-credential=ssh.authorized_keys.root:my-ssh-key + + + This installs the OS on the selected disk, erasing any previous contents, without confirmation, + registers it in the firmware, and drops in the SSH key for the root user, read from the + my-ssh-key file in the current directory. + + + + + See Also + + systemd1 + systemd-creds1 + systemd-repart8 + bootctl1 + systemd-firstboot1 + systemd-boot7 + systemd.system-credentials7 + + + + diff --git a/meson.build b/meson.build index 325b954a78..d6fbd7c2b7 100644 --- a/meson.build +++ b/meson.build @@ -1582,6 +1582,7 @@ foreach tuple : [ ['rfkill'], ['smack'], ['sysext'], + ['sysinstall'], ['sysusers'], ['timedated'], ['timesyncd'], @@ -2144,6 +2145,7 @@ subdir('src/storagetm') subdir('src/sulogin-shell') subdir('src/sysctl') subdir('src/sysext') +subdir('src/sysinstall') subdir('src/system-update-generator') subdir('src/systemctl') subdir('src/sysupdate') @@ -2928,6 +2930,7 @@ foreach tuple : [ ['resolve'], ['rfkill'], ['sysext'], + ['sysinstall'], ['systemd-analyze', conf.get('ENABLE_ANALYZE') == 1], ['sysupdate'], ['sysupdated'], diff --git a/meson_options.txt b/meson_options.txt index d61afac519..1917268d2c 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -109,6 +109,8 @@ option('sysupdate', type : 'feature', deprecated : { 'true' : 'enabled', 'false' option('sysupdated', type: 'combo', value : 'auto', choices : ['auto', 'enabled', 'disabled'], description : 'install the systemd-sysupdated service') +option('sysinstall', type : 'boolean', + description : 'install the systemd-sysinstall tool') option('coredump', type : 'boolean', description : 'install the coredump handler') diff --git a/shell-completion/bash/meson.build b/shell-completion/bash/meson.build index b0e56608e8..cddf742059 100644 --- a/shell-completion/bash/meson.build +++ b/shell-completion/bash/meson.build @@ -54,6 +54,7 @@ foreach item : [ ['systemd-resolve', 'ENABLE_RESOLVE'], ['systemd-run', ''], ['systemd-sysext', 'ENABLE_SYSEXT'], + ['systemd-sysinstall', 'ENABLE_SYSINSTALL'], ['systemd-vmspawn', 'ENABLE_VMSPAWN'], ['systemd-vpick', ''], ['timedatectl', 'ENABLE_TIMEDATED'], diff --git a/shell-completion/bash/systemd-sysinstall b/shell-completion/bash/systemd-sysinstall new file mode 100644 index 0000000000..600e2aa939 --- /dev/null +++ b/shell-completion/bash/systemd-sysinstall @@ -0,0 +1,90 @@ +# shellcheck shell=bash +# systemd-sysinstall(8) completion -*- shell-script -*- +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# systemd is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with systemd; If not, see . + +__contains_word() { + local w word=$1; shift + for w in "$@"; do + [[ $w = "$word" ]] && return + done +} + +__get_block_devices() { + systemd-repart --list-devices 2>/dev/null +} + +_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 + --chrome + --erase + --confirm + --summary + --reboot + --variables + --mute-console + --copy-locale + --copy-keymap + --copy-timezone + --definitions + --kernel + --set-credential + --load-credential' + ) + + _init_completion || return + + if __contains_word "$prev" ${OPTS[ARG]}; then + case $prev in + --welcome|--chrome|--confirm|--summary|--reboot|--mute-console|--copy-locale|--copy-keymap|--copy-timezone) + comps='yes no' + ;; + --erase|--variables) + comps='yes no auto' + ;; + --definitions) + comps=$(compgen -A directory -- "$cur") + compopt -o filenames + ;; + --kernel|--load-credential) + comps=$(compgen -A file -- "$cur") + compopt -o filenames + ;; + --set-credential) + comps='' + ;; + esac + COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) + return 0 + fi + + if [[ "$cur" = -* ]]; then + COMPREPLY=( $(compgen -W '${OPTS[*]}' -- "$cur") ) + return 0 + fi + + comps=$(__get_block_devices) + COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) + compopt -o filenames + return 0 +} + +complete -F _systemd_sysinstall systemd-sysinstall diff --git a/shell-completion/zsh/_systemd-sysinstall b/shell-completion/zsh/_systemd-sysinstall new file mode 100644 index 0000000000..039e4bf2a7 --- /dev/null +++ b/shell-completion/zsh/_systemd-sysinstall @@ -0,0 +1,29 @@ +#compdef systemd-sysinstall +# SPDX-License-Identifier: LGPL-2.1-or-later + +(( $+functions[_systemd-sysinstall_devices] )) || +_systemd-sysinstall_devices() { + local -a _devices + _devices=( ${(f)"$(systemd-repart --list-devices 2>/dev/null)"} ) + _wanted devices expl 'block device' compadd -a _devices +} + +_arguments \ + '(- *)'{-h,--help}'[Show help text]' \ + '(- *)--version[Show package version]' \ + '--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)' \ + '--confirm=[Query for confirmation before installation]:boolean:(yes no)' \ + '--summary=[Show summary before installation]:boolean:(yes no)' \ + '--reboot=[Reboot system after installation]:boolean:(yes no)' \ + '--variables=[Register installation in firmware variables]:boolean:(yes no auto)' \ + '--mute-console=[Mute kernel/PID 1 console output during installation]:boolean:(yes no)' \ + '--copy-locale=[Copy current locale to target system]:boolean:(yes no)' \ + '--copy-keymap=[Copy current keymap to target system]:boolean:(yes no)' \ + '--copy-timezone=[Copy current timezone to target system]:boolean:(yes no)' \ + '--definitions=[Find partition definitions in directory]:directory:_directories' \ + '--kernel=[Kernel image to install]:kernel image:_files' \ + '--set-credential=[Install a credential with a literal value]: : _message "ID:VALUE"' \ + '--load-credential=[Load credential from a file or AF_UNIX socket]: : _message "ID:PATH"' \ + '*::block device:_systemd-sysinstall_devices' diff --git a/shell-completion/zsh/meson.build b/shell-completion/zsh/meson.build index 6cc8a2d57f..f10ba7be61 100644 --- a/shell-completion/zsh/meson.build +++ b/shell-completion/zsh/meson.build @@ -43,6 +43,7 @@ foreach item : [ ['_systemd-nspawn', ''], ['_systemd-path', ''], ['_systemd-run', ''], + ['_systemd-sysinstall', 'ENABLE_SYSINSTALL'], ['_systemd-tmpfiles', 'ENABLE_TMPFILES'], ['_timedatectl', 'ENABLE_TIMEDATED'], ['_udevadm', ''], diff --git a/src/basic/time-util.c b/src/basic/time-util.c index 78c33c7553..eb74de32c2 100644 --- a/src/basic/time-util.c +++ b/src/basic/time-util.c @@ -1697,6 +1697,16 @@ int get_timezone(char **ret) { return strdup_to(ret, e); } +int get_timezone_prefer_env(char **ret) { + assert(ret); + + const char *e = getenv("TZ"); + if (e && e[0] == ':' && timezone_is_valid(e + 1, LOG_DEBUG)) + return strdup_to(ret, e + 1); + + return get_timezone(ret); +} + const char* etc_localtime(void) { static const char *cached = NULL; diff --git a/src/basic/time-util.h b/src/basic/time-util.h index 9a66a90859..fdaf11edcb 100644 --- a/src/basic/time-util.h +++ b/src/basic/time-util.h @@ -178,6 +178,7 @@ bool clock_supported(clockid_t clock); usec_t usec_shift_clock(usec_t x, clockid_t from, clockid_t to); int get_timezone(char **ret); +int get_timezone_prefer_env(char **ret); const char* etc_localtime(void); int mktime_or_timegm_usec(struct tm *tm, bool utc, usec_t *ret); diff --git a/src/bootctl/bootctl-link.c b/src/bootctl/bootctl-link.c index 6358189e7d..b8372752b5 100644 --- a/src/bootctl/bootctl-link.c +++ b/src/bootctl/bootctl-link.c @@ -25,6 +25,7 @@ #include "hashmap.h" #include "id128-util.h" #include "io-util.h" +#include "iovec-util.h" #include "json-util.h" #include "kernel-image.h" #include "log.h" @@ -44,6 +45,7 @@ typedef struct ExtraFile { /* The source and the temporary file we copy it into */ int source_fd, temp_fd; char *filename, *temp_filename; + struct iovec data; /* Alternative to 'source_fd': literal data */ } ExtraFile; #define EXTRA_FILE_NULL \ @@ -115,6 +117,7 @@ static void extra_file_done(ExtraFile *x) { x->temp_fd = safe_close(x->temp_fd); x->filename = mfree(x->filename); x->temp_filename = mfree(x->temp_filename); + iovec_done(&x->data); } static void profile_done(Profile *p) { @@ -366,7 +369,8 @@ static int link_context_pick_entry_token(LinkContext *c) { } static int begin_copy_file( - int source_fd, + int source_fd, /* Either the source fd is specified, or the 'data' below, not both */ + const struct iovec *data, const char *filename, int target_dir_fd, int *ret_tmpfile_fd, @@ -374,7 +378,6 @@ static int begin_copy_file( int r; - assert(source_fd >= 0); assert(filename); assert(target_dir_fd >= 0); assert(ret_tmpfile_fd); @@ -398,11 +401,18 @@ static int begin_copy_file( CLEANUP_TMPFILE_AT(target_dir_fd, t); - r = copy_bytes(source_fd, write_fd, UINT64_MAX, COPY_REFLINK|COPY_SEEK0_SOURCE); - if (r < 0) - return log_error_errno(r, "Failed to copy data into '%s': %m", filename); + if (source_fd >= 0) { + r = copy_bytes(source_fd, write_fd, UINT64_MAX, COPY_REFLINK|COPY_SEEK0_SOURCE); + if (r < 0) + return log_error_errno(r, "Failed to copy data into '%s': %m", filename); + + (void) copy_times(source_fd, write_fd, /* flags= */ 0); + } else if (iovec_is_set(data)) { + r = loop_write(write_fd, data->iov_base, data->iov_len); + if (r < 0) + return log_error_errno(r, "Failed to write data into '%s': %m", filename); + } - (void) copy_times(source_fd, write_fd, /* flags= */ 0); (void) fchmod(write_fd, 0644); *ret_tmpfile_fd = TAKE_FD(write_fd); @@ -824,6 +834,7 @@ static int run_link_now(LinkContext *c) { r = begin_copy_file( c->kernel_fd, + /* data= */ NULL, c->kernel_filename, c->entry_token_dir_fd, &c->kernel_temp_fd, @@ -834,6 +845,7 @@ static int run_link_now(LinkContext *c) { FOREACH_ARRAY(x, c->extra, c->n_extra) { r = begin_copy_file( x->source_fd, + &x->data, x->filename, c->entry_token_dir_fd, &x->temp_fd, @@ -1043,7 +1055,8 @@ static int dispatch_extras(const char *name, sd_json_variant *v, sd_json_dispatc static const sd_json_dispatch_field dispatch_table[] = { { "filename", SD_JSON_VARIANT_STRING, json_dispatch_loader_entry_resource_filename, offsetof(ExtraParameters, extra_file.filename), SD_JSON_MANDATORY }, - { "fileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(ExtraParameters, fd_index), SD_JSON_MANDATORY }, + { "fileDescriptor", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(ExtraParameters, fd_index), 0 }, + { "data", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(ExtraParameters, extra_file.data), 0 }, {}, }; @@ -1051,17 +1064,21 @@ static int dispatch_extras(const char *name, sd_json_variant *v, sd_json_dispatc if (r < 0) return r; - xp.extra_file.source_fd = sd_varlink_peek_dup_fd(c->link, xp.fd_index); - if (xp.extra_file.source_fd < 0) - return log_debug_errno(xp.extra_file.source_fd, "Failed to acquire extra fd from Varlink: %m"); - - r = fd_verify_safe_flags(xp.extra_file.source_fd); - if (r < 0) + if (iovec_is_set(&xp.extra_file.data) == (xp.fd_index != UINT_MAX)) return sd_varlink_error_invalid_parameter_name(c->link, name); + if (xp.fd_index != UINT_MAX) { + xp.extra_file.source_fd = sd_varlink_peek_dup_fd(c->link, xp.fd_index); + if (xp.extra_file.source_fd < 0) + return log_debug_errno(xp.extra_file.source_fd, "Failed to acquire extra fd from Varlink: %m"); - r = fd_verify_regular(xp.extra_file.source_fd); - if (r < 0) - return log_debug_errno(r, "Failed to validate that the extra file is a regular file descriptor: %m"); + r = fd_verify_safe_flags(xp.extra_file.source_fd); + if (r < 0) + return sd_varlink_error_invalid_parameter_name(c->link, name); + + r = fd_verify_regular(xp.extra_file.source_fd); + if (r < 0) + return log_debug_errno(r, "Failed to validate that the extra file is a regular file descriptor: %m"); + } if (!GREEDY_REALLOC(c->context.extra, c->context.n_extra+1)) return log_oom(); diff --git a/src/shared/varlink-io.systemd.BootControl.c b/src/shared/varlink-io.systemd.BootControl.c index 920b9479db..62306f5d79 100644 --- a/src/shared/varlink-io.systemd.BootControl.c +++ b/src/shared/varlink-io.systemd.BootControl.c @@ -153,8 +153,10 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE( BootEntryExtraFile, SD_VARLINK_FIELD_COMMENT("The name of the extra file"), SD_VARLINK_DEFINE_FIELD(filename, SD_VARLINK_STRING, 0), - SD_VARLINK_FIELD_COMMENT("Index into array of file descriptors, pointing to a file descriptor referencing the extra file."), - SD_VARLINK_DEFINE_FIELD(fileDescriptor, SD_VARLINK_INT, 0)); + SD_VARLINK_FIELD_COMMENT("Index into array of file descriptors, pointing to a file descriptor referencing the extra file to copy in. Either this or the 'data' field below must be set – not both, not neither."), + SD_VARLINK_DEFINE_FIELD(fileDescriptor, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Literal data to place in the extra file."), + SD_VARLINK_DEFINE_FIELD(data, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_METHOD( Link, diff --git a/src/sysinstall/meson.build b/src/sysinstall/meson.build new file mode 100644 index 0000000000..1d8be60365 --- /dev/null +++ b/src/sysinstall/meson.build @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + executable_template + { + 'name' : 'systemd-sysinstall', + 'public' : true, + 'conditions' : ['ENABLE_SYSINSTALL'], + 'sources' : files('sysinstall.c'), + }, +] diff --git a/src/sysinstall/sysinstall.c b/src/sysinstall/sysinstall.c new file mode 100644 index 0000000000..d8f5cbee3c --- /dev/null +++ b/src/sysinstall/sysinstall.c @@ -0,0 +1,1425 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +#include "sd-varlink.h" + +#include "alloc-util.h" +#include "ansi-color.h" +#include "blockdev-list.h" +#include "build.h" +#include "build-path.h" +#include "chase.h" +#include "conf-files.h" +#include "constants.h" +#include "efi-loader.h" +#include "efivars.h" +#include "env-file.h" +#include "escape.h" +#include "fd-util.h" +#include "find-esp.h" +#include "format-table.h" +#include "format-util.h" +#include "fs-util.h" +#include "glyph-util.h" +#include "help-util.h" +#include "image-policy.h" +#include "json-util.h" +#include "locale-setup.h" +#include "log.h" +#include "loop-util.h" +#include "machine-credential.h" +#include "main-func.h" +#include "mount-util.h" +#include "options.h" +#include "os-util.h" +#include "parse-argument.h" +#include "parse-util.h" +#include "path-util.h" +#include "prompt-util.h" +#include "strv.h" +#include "terminal-util.h" +#include "varlink-util.h" + +static char *arg_node = NULL; +static bool arg_welcome = true; +static int arg_erase = -1; /* tri-state */ +static bool arg_confirm = true; +static bool arg_summary = true; +static char **arg_definitions = NULL; +static char *arg_kernel_image = NULL; +static bool arg_reboot = false; +static int arg_touch_variables = -1; /* tri-state */ +static MachineCredentialContext arg_credentials = {}; +static bool arg_copy_locale = true; +static bool arg_copy_keymap = true; +static bool arg_copy_timezone = true; +static bool arg_chrome = true; +static bool arg_mute_console = false; + +STATIC_DESTRUCTOR_REGISTER(arg_node, freep); +STATIC_DESTRUCTOR_REGISTER(arg_definitions, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_kernel_image, freep); +STATIC_DESTRUCTOR_REGISTER(arg_credentials, machine_credential_context_done); + +static int help(void) { + int r; + + _cleanup_(table_unrefp) Table *options = NULL; + r = option_parser_get_help_table(&options); + if (r < 0) + return r; + + help_cmdline("[OPTIONS...] [DEVICE]"); + help_abstract("Installs the OS to another block device."); + help_section("Options:"); + + r = table_print_or_warn(options); + if (r < 0) + return r; + + help_man_page_reference("systemd-sysinstall", "8"); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + int r; + + assert(argc >= 0); + assert(argv); + + OptionParser opts = { argc, argv }; + + FOREACH_OPTION_OR_RETURN(c, &opts) + switch (c) { + + OPTION_COMMON_HELP: + return help(); + + OPTION_COMMON_VERSION: + return version(); + + OPTION_LONG("welcome", "no", "Disable the welcome text"): + r = parse_boolean_argument("--welcome=", opts.arg, &arg_welcome); + if (r < 0) + return r; + + break; + + OPTION_LONG("erase", "BOOL", "Whether to erase the target disk"): + r = parse_tristate_argument_with_auto("--erase=", opts.arg, &arg_erase); + if (r < 0) + return r; + break; + + OPTION_LONG("confirm", "no", "Disable query for confirmation"): + r = parse_boolean_argument("--confirm=", opts.arg, &arg_confirm); + if (r < 0) + return r; + break; + + OPTION_LONG("summary", "no", "Disable summary before beginning operation"): + r = parse_boolean_argument("--summary=", opts.arg, &arg_summary); + if (r < 0) + return r; + break; + + OPTION_LONG("definitions", "DIR", "Find partition definitions in specified directory"): { + _cleanup_free_ char *path = NULL; + r = parse_path_argument(opts.arg, /* suppress_root= */ false, &path); + if (r < 0) + return r; + if (strv_consume(&arg_definitions, TAKE_PTR(path)) < 0) + return log_oom(); + break; + } + + OPTION_LONG("reboot", "BOOL", "Whether to reboot after installation is complete"): + r = parse_boolean_argument("--reboot=", opts.arg, &arg_reboot); + if (r < 0) + return r; + break; + + OPTION_LONG("variables", "BOOL", "Whether to modify EFI variables"): + r = parse_tristate_argument_with_auto("--variables=", opts.arg, &arg_touch_variables); + if (r < 0) + return r; + break; + + OPTION_LONG("kernel", "IMAGE", "Explicitly pick kernel image to install"): + r = parse_path_argument(opts.arg, /* suppress_root= */ false, &arg_kernel_image); + if (r < 0) + return r; + break; + + OPTION_LONG("set-credential", "ID:VALUE", "Install a credential with literal value to target system"): + r = machine_credential_set(&arg_credentials, opts.arg); + if (r < 0) + return r; + break; + + OPTION_LONG("load-credential", "ID:PATH", "Load a credential to install to new system from file or AF_UNIX stream socket"): + r = machine_credential_load(&arg_credentials, opts.arg); + if (r < 0) + return r; + + break; + + OPTION_LONG("copy-locale", "no", "Don't copy current locale to target system"): + r = parse_boolean_argument("--copy-locale=", opts.arg, &arg_copy_locale); + if (r < 0) + return r; + break; + + OPTION_LONG("copy-keymap", "no", "Don't copy current keymap to target system"): + r = parse_boolean_argument("--copy-keymap=", opts.arg, &arg_copy_keymap); + if (r < 0) + return r; + break; + + OPTION_LONG("copy-timezone", "no", "Don't copy current timezone to target system"): + r = parse_boolean_argument("--copy-timezone=", opts.arg, &arg_copy_timezone); + if (r < 0) + return r; + break; + + OPTION_LONG("chrome", "no", "Whether to show a color bar at top and bottom of terminal"): + r = parse_boolean_argument("--chrome=", opts.arg, &arg_chrome); + if (r < 0) + return r; + + break; + + OPTION_LONG("mute-console", "BOOL", "Whether to disallow kernel/PID 1 writes to the console while running"): + r = parse_boolean_argument("--mute-console=", opts.arg, &arg_mute_console); + if (r < 0) + return r; + break; + } + + char **args = option_parser_get_args(&opts); + + if (strv_length(args) > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments."); + if (!strv_isempty(args)) { + arg_node = strdup(args[0]); + if (!arg_node) + return log_oom(); + } + + return 1; +} + +static int print_welcome(sd_varlink **mute_console_link) { + _cleanup_free_ char *pretty_name = NULL, *os_name = NULL, *ansi_color = NULL; + const char *pn, *ac; + int r; + + assert(mute_console_link); + + if (!*mute_console_link && arg_mute_console) + (void) mute_console(mute_console_link); + + if (!arg_welcome) + return 0; + + r = parse_os_release( + /* root= */ NULL, + "PRETTY_NAME", &pretty_name, + "NAME", &os_name, + "ANSI_COLOR", &ansi_color); + if (r < 0) + log_full_errno(r == -ENOENT ? LOG_DEBUG : LOG_WARNING, r, + "Failed to read os-release file, ignoring: %m"); + + pn = os_release_pretty_name(pretty_name, os_name); + ac = isempty(ansi_color) ? "0" : ansi_color; + + if (colors_enabled()) + printf(ANSI_HIGHLIGHT "Welcome to the " ANSI_NORMAL "\x1B[%sm%s" ANSI_HIGHLIGHT " Installer!" ANSI_NORMAL "\n", ac, pn); + else + printf("Welcome to the %s Installer!\n", pn); + + putchar('\n'); + + return 0; +} + +static int connect_to_repart(sd_varlink **link) { + int r; + + assert(link); + + if (*link) { + /* Reset the time-out to default here, since we are reusing the connection, but might enqueue + * a different operation */ + r = sd_varlink_set_relative_timeout(*link, 0); + if (r < 0) + return r; + + return 0; + } + + _cleanup_close_ int fd = -EBADF; + _cleanup_free_ char *repart = NULL; + fd = pin_callout_binary("systemd-repart", &repart); + if (fd < 0) + return log_error_errno(fd, "Failed to find systemd-repart binary: %m"); + + r = sd_varlink_connect_exec(link, repart, /* argv= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to connect to systemd-repart: %m"); + + return 1; +} + +static int acquire_device_list( + sd_varlink **link, + char ***ret_menu, + char ***ret_accepted) { + int r; + + r = connect_to_repart(link); + if (r < 0) + return r; + + _cleanup_strv_free_ char **menu = NULL, **accepted = NULL; + + sd_json_variant *reply = NULL; + const char *error_id = NULL; + r = sd_varlink_collectbo( + *link, + "io.systemd.Repart.ListCandidateDevices", + &reply, + &error_id, + SD_JSON_BUILD_PAIR_BOOLEAN("ignoreRoot", true)); + if (r < 0) + return log_error_errno(r, "Failed to issue io.systemd.Repart.ListCandidateDevices() varlink call: %m"); + if (streq_ptr(error_id, "io.systemd.Repart.NoCandidateDevices")) + log_debug("No candidate devices found."); + else if (error_id) { + r = sd_varlink_error_to_errno(error_id, reply); /* If this is a system errno style error, output it with %m */ + if (r != -EBADR) + return log_error_errno(r, "Failed to issue io.systemd.Repart.ListCandidateDevices() varlink call: %m"); + + return log_error_errno(r, "Failed to issue io.systemd.Repart.ListCandidateDevices() varlink call: %s", error_id); + } else { + sd_json_variant *i; + JSON_VARIANT_ARRAY_FOREACH(i, reply) { + _cleanup_(block_device_done) BlockDevice bd = BLOCK_DEVICE_NULL; + + static const sd_json_dispatch_field dispatch_table[] = { + { "node", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(BlockDevice, node), SD_JSON_MANDATORY }, + { "symlinks", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(BlockDevice, symlinks), 0 }, + {} + }; + + r = sd_json_dispatch(i, dispatch_table, SD_JSON_LOG|SD_JSON_ALLOW_EXTENSIONS, &bd); + if (r < 0) + return r; + + if (strv_extend(&accepted, bd.node) < 0) + return log_oom(); + if (strv_extend_strv(&accepted, bd.symlinks, /* filter_duplicates= */ true) < 0) + return log_oom(); + + /* Prefer the by-id and by-loop-ref because they typically contain the strings most + * directly understood by the user */ + const char *n = strv_find_prefix(bd.symlinks, "/dev/disk/by-id/"); + if (!n) + n = strv_find_prefix(bd.symlinks, "/dev/disk/by-loop-ref/"); + if (!n) + n = bd.node; + + if (strv_extend(&menu, n) < 0) + return log_oom(); + } + } + + *ret_menu = TAKE_PTR(menu); + *ret_accepted = TAKE_PTR(accepted); + return 0; +} + +static int device_is_valid(const char *node, void *userdata) { + + if (!path_is_valid(node) || !path_is_absolute(node)) { + log_error("Not a valid absolute file system path, refusing: %s", node); + return false; + } + + struct stat st; + if (stat(node, &st) < 0) { + log_error_errno(errno, "Failed to check if '%s' is a valid block device node: %m", node); + return false; + } + if (!S_ISBLK(st.st_mode)) { + log_error("Path '%s' does not refer to a valid block device node, refusing.", node); + return false; + } + + return true; +} + +static int refresh_devices(char ***ret_menu, char ***ret_accepted, void *userdata) { + sd_varlink **repart_link = ASSERT_PTR(userdata); + + (void) acquire_device_list(repart_link, ret_menu, ret_accepted); + return 0; +} + +static int prompt_block_device(sd_varlink **repart_link, char **ret_node) { + int r; + + putchar('\n'); + + _cleanup_strv_free_ char **menu = NULL, **accepted = NULL; + (void) acquire_device_list(repart_link, &menu, &accepted); + + r = prompt_loop("Please enter target disk device", + GLYPH_COMPUTER_DISK, + menu, + accepted, + /* ellipsize_percentage= */ 20, + /* n_columns= */ 1, + /* column_width= */ 80, + device_is_valid, + refresh_devices, + /* userdata= */ repart_link, + PROMPT_SHOW_MENU|PROMPT_SHOW_MENU_NOW|PROMPT_MAY_SKIP|PROMPT_HIDE_SKIP_HINT|PROMPT_HIDE_MENU_HINT, + ret_node); + if (r < 0) + return r; + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation cancelled."); + + return 0; +} + +static int read_space_metrics( + sd_json_variant *v, + uint64_t *min_size, + uint64_t *current_size, + uint64_t *need_free) { + + int r; + + struct { + uint64_t min_size; + uint64_t current_size; + uint64_t need_free; + } p = { + .min_size = UINT64_MAX, + .current_size = UINT64_MAX, + .need_free = UINT64_MAX, + }; + + static const sd_json_dispatch_field dispatch_table[] = { + { "minimalSizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, min_size), 0 }, + { "currentSizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, current_size), 0 }, + { "needFreeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, need_free), 0 }, + {} + }; + + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); + if (r < 0) + return r; + + if (min_size) + *min_size = p.min_size; + if (current_size) + *current_size = p.current_size; + if (need_free) + *need_free = p.need_free; + + return 0; +} + +static int invoke_repart( + sd_varlink **link, + const char *node, + bool erase, + bool dry_run, + uint64_t *min_size, /* initialized both on success and error */ + uint64_t *current_size, /* ditto */ + uint64_t *need_free) { /* ditto */ + + int r; + + assert(link); + + /* Note, if dry_run is true, then ENOSPC, E2BIG, EHWPOISON will not be logged about beyond LOG_DEBUG, + * but all other errors will be */ + + r = connect_to_repart(link); + if (r < 0) { + read_space_metrics(/* v= */ NULL, min_size, current_size, need_free); + return r; + } + + if (!dry_run) { + /* Seeding the partitions might be very slow, disable timeout */ + r = sd_varlink_set_relative_timeout(*link, UINT64_MAX); + if (r < 0) + return log_error_errno(r, "Failed to disable IPC timeout: %m"); + } + + sd_json_variant *reply = NULL; + const char *error_id = NULL; + r = sd_varlink_callbo( + *link, + "io.systemd.Repart.Run", + &reply, + &error_id, + SD_JSON_BUILD_PAIR_STRING("node", node), + SD_JSON_BUILD_PAIR_STRING("empty", erase ? "force" : "allow"), + SD_JSON_BUILD_PAIR_BOOLEAN("dryRun", dry_run), + SD_JSON_BUILD_PAIR_CONDITION(!!arg_definitions, "definitions", SD_JSON_BUILD_STRV(arg_definitions)), + SD_JSON_BUILD_PAIR_BOOLEAN("deferPartitionsEmpty", true), + SD_JSON_BUILD_PAIR_BOOLEAN("deferPartitionsFactoryReset", true)); + if (r < 0) { + read_space_metrics(/* v= */ NULL, min_size, current_size, need_free); + return log_error_errno(r, "Failed to issue io.systemd.Repart.Run() varlink call: %m"); + } + if (error_id) { + if (streq(error_id, "io.systemd.Repart.InsufficientFreeSpace")) { + (void) read_space_metrics(reply, min_size, current_size, need_free); + return log_full_errno( + dry_run ? LOG_DEBUG : LOG_ERR, + SYNTHETIC_ERRNO(ENOSPC), + "Not enough free space on disk, cannot install."); + } + if (streq(error_id, "io.systemd.Repart.DiskTooSmall")) { + (void) read_space_metrics(reply, min_size, current_size, need_free); + return log_full_errno( + dry_run ? LOG_DEBUG : LOG_ERR, + SYNTHETIC_ERRNO(E2BIG), + "Disk too small for installation, cannot install."); + } + + /* For all other errors reset the metrics */ + read_space_metrics(/* v= */ NULL, min_size, current_size, need_free); + + if (streq(error_id, "io.systemd.Repart.ConflictingDiskLabelPresent")) + return log_full_errno( + dry_run ? LOG_DEBUG : LOG_ERR, + SYNTHETIC_ERRNO(EHWPOISON), + "A conflicting disk label is already present on the target disk, cannot install unless disk is erased."); + + r = sd_varlink_error_to_errno(error_id, reply); /* If this is a system errno style error, output it with %m */ + if (r != -EBADR) + return log_error_errno(r, "Failed to issue io.systemd.Repart.Run() varlink call: %m"); + + return log_error_errno(r, "Failed to issue io.systemd.Repart.Run() varlink call: %s", error_id); + } + + (void) read_space_metrics(reply, min_size, current_size, need_free); + + return 0; +} + +static int prompt_erase( + bool can_add, + int *ret_erase) { + int r; + + assert(ret_erase); + + putchar('\n'); + + char **l = can_add ? STRV_MAKE("keep", "erase") : STRV_MAKE("erase"); + + _cleanup_free_ char *reply = NULL; + r = prompt_loop(can_add ? + "Please type 'keep' to install the OS in addition to what the disk already contains, or 'erase' to erase all data on the disk" : + "Please type 'erase' to confirm that all data on the disk shall be erased", + GLYPH_BROOM, + /* menu= */ l, + /* accepted= */ l, + /* ellipsize_percentage= */ 20, + /* n_columns= */ 2, + /* column_width= */ 40, + /* is_valid= */ NULL, + /* refresh= */ NULL, + /* userdata= */ NULL, + PROMPT_SHOW_MENU|PROMPT_MAY_SKIP|PROMPT_HIDE_MENU_HINT|PROMPT_HIDE_SKIP_HINT, + &reply); + if (r < 0) + return r; + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation cancelled."); + + if (streq(reply, "erase")) + *ret_erase = true; + else if (streq(reply, "keep")) + *ret_erase = false; + else + assert_not_reached(); + + return 0; +} + +static int prompt_touch_variables(void) { + int r; + + if (arg_touch_variables >= 0) + return 0; + + putchar('\n'); + + char **l = STRV_MAKE("yes", "no"); + + _cleanup_free_ char *reply = NULL; + r = prompt_loop("Type 'yes' to register OS installation in firmware variables of the local system, 'no' otherwise", + GLYPH_ROCKET, + /* menu= */ l, + /* accepted= */ l, + /* ellipsize_percentage= */ 20, + /* n_columns= */ 2, + /* column_width= */ 40, + /* is_valid= */ NULL, + /* refresh= */ NULL, + /* userdata= */ NULL, + PROMPT_SHOW_MENU|PROMPT_MAY_SKIP|PROMPT_HIDE_MENU_HINT|PROMPT_HIDE_SKIP_HINT, + &reply); + if (r < 0) + return r; + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation cancelled."); + + r = parse_boolean(reply); + if (r < 0) + return log_error_errno(r, "Failed to parse reply: %s", reply); + + arg_touch_variables = r; + + return 0; +} + +static int prompt_confirm(void) { + int r; + + if (!arg_confirm) + return 0; + + putchar('\n'); + + char **l = STRV_MAKE("yes", "no"); + + _cleanup_free_ char *reply = NULL; + r = prompt_loop(arg_summary ? "Please type 'yes' to confirm the choices above and begin the installation" : + "Please type 'yes' to begin the installation", + GLYPH_WARNING_SIGN, + /* menu= */ l, + /* accepted= */ l, + /* ellipsize_percentage= */ 20, + /* n_columns= */ 2, + /* column_width= */ 40, + /* is_valid= */ NULL, + /* refresh= */ NULL, + /* userdata= */ NULL, + PROMPT_SHOW_MENU|PROMPT_MAY_SKIP|PROMPT_HIDE_MENU_HINT|PROMPT_HIDE_SKIP_HINT, + &reply); + if (r < 0) + return r; + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation cancelled."); + + if (!streq(reply, "yes")) + return log_error_errno(SYNTHETIC_ERRNO(ECANCELED), "Installation not confirmed, cancelling."); + + return 0; +} + +static int validate_run(sd_varlink **repart_link, const char *node) { + int r; + + assert(repart_link); + assert(node); + + /* First loop: either with explicitly configured --erase= value, or false. A second loop only if not configured explicitly. */ + bool try_erase = arg_erase > 0, conflicting_disk_label = false; + for (;;) { + uint64_t min_size = UINT64_MAX, current_size = UINT64_MAX, need_free = UINT64_MAX; + r = invoke_repart( + repart_link, + node, + try_erase, + /* dry_run= */ true, + &min_size, + ¤t_size, + &need_free); + if (r == -ENOSPC) { + /* The disk is large enough, but there's not enough unallocated space. Hence proceed, but require erasing */ + if (try_erase || arg_erase >= 0) + return log_error_errno(r, "The selected disk is big enough for the installation but does not have enough free space."); + + log_notice("The selected disk is big enough for the installation but does not have enough free space. Installation will require erasing."); + if (need_free != UINT64_MAX) + log_info("Required free space is %s.", FORMAT_BYTES(need_free)); + + try_erase = true; + } else if (r == -E2BIG) { + /* Won't fit, whatever we do */ + log_error_errno(r, "The selected disk is not large enough for an OS installation."); + if (current_size != UINT64_MAX) + log_info("The size of the selected disk is %s, but a minimal size of %s is required.", + FORMAT_BYTES(current_size), + FORMAT_BYTES(min_size)); + return r; + } else if (r == -EHWPOISON) { + if (try_erase || arg_erase >= 0) + return log_error_errno(r, "The selected disk contains a conflicting disk label, refusing."); + + log_debug("Disk contains a conflicting disk label, checking if we could install the OS after erasing it."); + try_erase = true; + conflicting_disk_label = true; + continue; + } else if (r < 0) + /* invoke_repart() already logged about all other errors */ + return r; + else + /* Nice, we can add the OS to the disk, without erasing anything. */ + log_info("The selected disk has enough free space for an installation of the OS."); + + if (conflicting_disk_label) + log_warning("A conflicting disk label has been found, and must be erased for installation."); + + if (arg_erase < 0) { + r = prompt_erase(/* can_add= */ !try_erase, &arg_erase); + if (r < 0) + return r; + } + + return 0; + } +} + +static int show_summary(void) { + int r; + + if (!arg_summary) + return 0; + + printf("\n" + "%sSummary:%s\n", ansi_underline(), ansi_normal()); + + _cleanup_(table_unrefp) Table *table = table_new_vertical(); + if (!table) + return log_oom(); + + r = table_add_many( + table, + TABLE_FIELD, "Selected Disk", + TABLE_STRING, arg_node, + TABLE_FIELD, "Erase Disk", + TABLE_BOOLEAN, arg_erase, + TABLE_SET_COLOR, arg_erase ? ansi_highlight_red() : NULL, + TABLE_FIELD, "Register in Firmware", + TABLE_BOOLEAN, arg_touch_variables); + if (r < 0) + return table_log_add_error(r); + + static const char * const map[] = { + "firstboot.keymap", "Keyboard Map", + "firstboot.locale", "Locale", + "firstboot.locale-messages", "Locale (Messages)", + "firstboot.timezone", "Timezone", + NULL + }; + + STRV_FOREACH_PAIR(id, text, map) { + MachineCredential *c = machine_credential_find(&arg_credentials, *id); + if (!c) + continue; + + _cleanup_free_ char *escaped = cescape_length(c->data, c->size); + if (!escaped) + return log_oom(); + + r = table_add_many( + table, + TABLE_FIELD, *text, + TABLE_STRING, escaped); + if (r < 0) + return table_log_add_error(r); + } + + unsigned n_extra_credentials = 0; + FOREACH_ARRAY(cred, arg_credentials.credentials, arg_credentials.n_credentials) { + bool covered = false; + + STRV_FOREACH_PAIR(id, text, map) + if (streq(*id, cred->id)) { + covered = true; + break; + } + + if (!covered) + n_extra_credentials++; + } + + if (n_extra_credentials > 0) { + r = table_add_many( + table, + TABLE_FIELD, "Extra Credentials", + TABLE_UINT, n_extra_credentials); + if (r < 0) + return table_log_add_error(r); + } + + r = table_print(table); + if (r < 0) + return r; + + return 0; +} + +static int find_current_kernel( + char **ret_filename, + int *ret_fd) { + + int r; + + sd_id128_t uuid; + r = efi_stub_get_device_part_uuid(&uuid); + if (r == -ENOENT) + return log_error_errno(r, "Cannot find current kernel, no stub partition UUID passed via EFI variables."); + if (r < 0) + return log_error_errno(r, "Unable to determine stub partition UUID: %m"); + + _cleanup_free_ char *image = NULL; + r = efi_get_variable_path(EFI_LOADER_VARIABLE_STR("StubImageIdentifier"), &image); + if (r == -ENOENT) + return log_error_errno(r, "Cannot find current kernel, no stub EFI binary path passed."); + if (r < 0) + return log_error_errno(r, "Unable to determine stub EFI binary path: %m"); + + /* Note: we search for the *host* ESP here (i.e. the one the current EFI paths relate to), not the + * one of the target image */ + + _cleanup_free_ char *partition_path = NULL; + _cleanup_close_ int partition_fd = -EBADF; + sd_id128_t partition_uuid; + r = find_esp_and_warn_full( + /* root= */ NULL, + /* path= */ NULL, + /* unprivileged_mode= */ false, + &partition_path, + &partition_fd, + /* ret_part= */ NULL, + /* ret_pstart= */ NULL, + /* ret_psize= */ NULL, + &partition_uuid, + /* ret_devid= */ NULL); + if (r < 0 && r != -ENOKEY) + return r; + if (r < 0 || !sd_id128_equal(uuid, partition_uuid)) { + partition_path = mfree(partition_path); + partition_fd = safe_close(partition_fd); + + r = find_xbootldr_and_warn_full( + /* root= */ NULL, + /* path= */ NULL, + /* unprivileged_mode= */ false, + &partition_path, + &partition_fd, + &partition_uuid, + /* ret_devid= */ NULL); + if (r < 0 && r != -ENOKEY) + return r; + + if (r < 0 || !sd_id128_equal(uuid, partition_uuid)) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Unable to find UKI on ESP/XBOOTLDR partitions."); + } + + _cleanup_free_ char *resolved = NULL; + _cleanup_close_ int fd = chase_and_openat( + /* root_fd= */ partition_fd, + /* dir_fd= */ partition_fd, + image, + CHASE_PROHIBIT_SYMLINKS|CHASE_MUST_BE_REGULAR, + O_RDONLY|O_CLOEXEC, + &resolved); + if (fd < 0) + return log_error_errno(fd, "Failed to find EFI binary '%s' on partition '%s': %m", image, partition_path); + + _cleanup_free_ char *fn = NULL; + r = path_extract_filename(resolved, &fn); + if (r < 0) + return log_error_errno(r, "Failed to extract UKI file name from '%s': %m", resolved); + + if (ret_filename) + *ret_filename = TAKE_PTR(fn); + if (ret_fd) + *ret_fd = TAKE_FD(fd); + + return 0; +} + +static int connect_to_bootctl(sd_varlink **link) { + int r; + + assert(link); + + if (*link) + return 0; + + _cleanup_close_ int fd = -EBADF; + _cleanup_free_ char *bootctl = NULL; + fd = pin_callout_binary("bootctl", &bootctl); + if (fd < 0) + return log_error_errno(fd, "Failed to find bootctl binary: %m"); + + r = sd_varlink_connect_exec(link, bootctl, /* argv= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to connect to bootctl: %m"); + + r = sd_varlink_set_allow_fd_passing_output(*link, true); + if (r < 0) + return log_error_errno(r, "Failed to enable fd passing to bootctl: %m"); + + return 1; +} + +static int invoke_bootctl_install( + sd_varlink **link, + const char *root_dir, + int root_fd) { + int r; + + assert(link); + assert(root_dir); + assert(root_fd >= 0); + + r = connect_to_bootctl(link); + if (r < 0) + return r; + + int fd_idx = sd_varlink_push_dup_fd(*link, root_fd); + if (fd_idx < 0) + return log_error_errno(fd_idx, "Failed to submit root fd onto Varlink connection: %m"); + + const char *error_id = NULL; + r = varlink_callbo_and_log( + *link, + "io.systemd.BootControl.Install", + /* reply= */ NULL, + &error_id, + SD_JSON_BUILD_PAIR_STRING("operation", "new"), + SD_JSON_BUILD_PAIR_INTEGER("rootFileDescriptor", fd_idx), + SD_JSON_BUILD_PAIR_STRING("rootDirectory", root_dir), + SD_JSON_BUILD_PAIR_BOOLEAN("touchVariables", arg_touch_variables)); + if (r < 0) + return r; + + return 0; +} + +static int invoke_bootctl_link( + sd_varlink **link, + const char *root_dir, + int root_fd, + char **encrypted_credentials) { + int r; + + assert(link); + assert(root_dir); + assert(root_fd >= 0); + + r = connect_to_bootctl(link); + if (r < 0) + return r; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; + STRV_FOREACH_PAIR(name, value, encrypted_credentials) { + _cleanup_free_ char *j = strjoin(*name, ".cred"); + if (!j) + return log_oom(); + + r = sd_json_variant_append_arraybo( + &array, + SD_JSON_BUILD_PAIR_STRING("filename", j), + SD_JSON_BUILD_PAIR_BASE64("data", *value, strlen(*value))); + if (r < 0) + return log_error_errno(r, "Failed to append credential to message: %m"); + } + + int root_fd_idx = sd_varlink_push_dup_fd(*link, root_fd); + if (root_fd_idx < 0) + return log_error_errno(root_fd_idx, "Failed to submit root fd onto Varlink connection: %m"); + + _cleanup_free_ char *kernel_filename = NULL; + _cleanup_close_ int kernel_fd = -EBADF; + if (arg_kernel_image) { + r = path_extract_filename(arg_kernel_image, &kernel_filename); + if (r < 0) + return log_error_errno(r, "Failed to extract filename from kernel path '%s': %m", arg_kernel_image); + if (r == O_DIRECTORY) + return log_error_errno(SYNTHETIC_ERRNO(EISDIR), "Kernel path '%s' refers to directory, must be regular file, refusing.", arg_kernel_image); + + kernel_fd = xopenat_full(XAT_FDROOT, arg_kernel_image, O_RDONLY|O_CLOEXEC, XO_REGULAR, MODE_INVALID); + if (kernel_fd < 0) + return log_error_errno(kernel_fd, "Failed to open kernel image '%s': %m", arg_kernel_image); + + } else { + r = find_current_kernel(&kernel_filename, &kernel_fd); + if (r < 0) + return r; + } + + int kernel_fd_idx = sd_varlink_push_dup_fd(*link, kernel_fd); + if (kernel_fd_idx < 0) + return log_error_errno(kernel_fd_idx, "Failed to submit kernel fd onto Varlink connection: %m"); + + const char *error_id = NULL; + r = varlink_callbo_and_log( + *link, + "io.systemd.BootControl.Link", + /* reply= */ NULL, + &error_id, + SD_JSON_BUILD_PAIR_INTEGER("rootFileDescriptor", root_fd_idx), + SD_JSON_BUILD_PAIR_STRING("rootDirectory", root_dir), + JSON_BUILD_PAIR_STRING_NON_EMPTY("kernelFilename", kernel_filename), + SD_JSON_BUILD_PAIR_INTEGER("kernelFileDescriptor", kernel_fd_idx), + SD_JSON_BUILD_PAIR_CONDITION(!!array, "extraFiles", SD_JSON_BUILD_VARIANT(array))); + if (r < 0) + return r; + + return 0; +} + +static int maybe_reboot(void) { + int r; + + if (!arg_reboot) + return 0; + + log_notice("%s%sSystem will reboot now.", + emoji_enabled() ? glyph(GLYPH_CIRCLE_ARROW) : "", emoji_enabled() ? " " : ""); + + if (!any_key_to_proceed()) + return 0; + + log_notice("%s%sInitiating reboot.", + emoji_enabled() ? glyph(GLYPH_CIRCLE_ARROW) : "", emoji_enabled() ? " " : ""); + + _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL; + r = sd_varlink_connect_address(&link, "/run/systemd/io.systemd.Shutdown"); + if (r < 0) + return log_error_errno(r, "Failed to connect to systemd-logind: %m"); + + sd_json_variant *reply = NULL; + const char *error_id = NULL; + r = varlink_callbo_and_log( + link, + "io.systemd.Shutdown.Reboot", + &reply, + &error_id); + if (r < 0) + return r; + + return 0; +} + +static int read_credential_locale(void) { + int r; + + if (!arg_copy_locale) + return 0; + + if (machine_credential_find(&arg_credentials, "firstboot.locale") || + machine_credential_find(&arg_credentials, "firstboot.locale-messages")) + return 0; + + /* For the main locale we check the two env vars, and if neither is there, we use LC_NUMERIC, since + * it seems to be one of the most fundamental ones, and is not LC_MESSAGES for which we have a + * separate setting after all */ + const char *l = getenv("LC_ALL") ?: getenv("LANG") ?: setlocale(LC_NUMERIC, NULL); + if (l) { + r = machine_credential_add(&arg_credentials, "firstboot.locale", l, /* size= */ SIZE_MAX); + if (r < 0) + return log_oom(); + } + + const char *m = setlocale(LC_MESSAGES, NULL); + if (m && !streq_ptr(m, l)) { + r = machine_credential_add(&arg_credentials, "firstboot.locale-messages", m, /* size= */ SIZE_MAX); + if (r < 0) + return log_oom(); + } + + return 0; +} + +static int read_credential_keymap(void) { + int r; + + if (!arg_copy_keymap) + return 0; + + if (machine_credential_find(&arg_credentials, "firstboot.keymap")) + return 0; + + _cleanup_free_ char *keymap = NULL; + r = parse_env_file( + /* f= */ NULL, + etc_vconsole_conf(), + "KEYMAP", &keymap); + if (r < 0 && r != -ENOENT) + return log_error_errno(r, "Failed to parse '%s': %m", etc_vconsole_conf()); + + if (!isempty(keymap)) { + r = machine_credential_add(&arg_credentials, "firstboot.keymap", keymap, /* size= */ SIZE_MAX); + if (r < 0) + return log_oom(); + } + + return 0; +} + +static int read_credential_timezone(void) { + int r; + + if (!arg_copy_timezone) + return 0; + + if (machine_credential_find(&arg_credentials, "firstboot.timezone")) + return 0; + + _cleanup_free_ char *tz = NULL; + r = get_timezone_prefer_env(&tz); + if (r < 0) + log_warning_errno(r, "Failed to read timezone, skipping timezone propagation: %m"); + else { + r = machine_credential_add(&arg_credentials, "firstboot.timezone", tz, /* size= */ SIZE_MAX); + if (r < 0) + return log_oom(); + } + + return 0; +} + +static int read_credentials(void) { + int r; + + r = read_credential_locale(); + if (r < 0) + return r; + + r = read_credential_keymap(); + if (r < 0) + return r; + + r = read_credential_timezone(); + if (r < 0) + return r; + + return 0; +} + +static int connect_to_creds(sd_varlink **link) { + int r; + + assert(link); + + if (*link) + return 0; + + _cleanup_close_ int fd = -EBADF; + _cleanup_free_ char *creds = NULL; + fd = pin_callout_binary("systemd-creds", &creds); + if (fd < 0) + return log_error_errno(fd, "Failed to find systemd-creds binary: %m"); + + r = sd_varlink_connect_exec(link, creds, /* argv= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to connect to systemd-creds: %m"); + + return 1; +} + +static int encrypt_one_credential(sd_varlink **link, const MachineCredential *input, char ***encrypted) { + int r; + + assert(link); + assert(input); + assert(encrypted); + + log_info("Encrypting credential '%s'...", input->id); + + r = connect_to_creds(link); + if (r < 0) + return r; + + sd_json_variant *reply = NULL; + const char *error_id = NULL; + r = varlink_callbo_and_log( + *link, + "io.systemd.Credentials.Encrypt", + &reply, + &error_id, + SD_JSON_BUILD_PAIR_STRING("name", input->id), + SD_JSON_BUILD_PAIR_BASE64("data", input->data, input->size), + SD_JSON_BUILD_PAIR_STRING("scope", "system"), + /* We pick the 'auto_initrd' key for this, since we want TPM if available, but are fine with NULL if not */ + SD_JSON_BUILD_PAIR_STRING("withKey", "auto_initrd")); + if (r < 0) + return r; + + static const sd_json_dispatch_field dispatch_table[] = { + { "blob", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, 0, 0 }, + {} + }; + + const char *blob = NULL; + r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &blob); + if (r < 0) + return r; + + r = strv_extend_many(encrypted, input->id, blob); + if (r < 0) + return r; + + return 0; +} + +static int encrypt_credentials(sd_varlink **link, char ***encrypted) { + int r; + + assert(link); + assert(encrypted); + + FOREACH_ARRAY(cred, arg_credentials.credentials, arg_credentials.n_credentials) { + r = encrypt_one_credential(link, cred, encrypted); + if (r < 0) + return r; + } + + return 0; +} + +static const ImagePolicy image_policy = { + .n_policies = 4, + .policies = { + /* We mount / and /usr/ so that we can get access to /etc/machine-id and /etc/kernel/ */ + { PARTITION_ROOT, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_USR, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_ESP, PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_XBOOTLDR, PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + }, + .default_flags = PARTITION_POLICY_IGNORE, +}; + +static int settle_definitions(void) { + int r; + + if (arg_definitions) + return 0; + + /* If /usr/lib/repart.sysinstall.d/ is populated, use it, otherwise use the regular definition + * files */ + + _cleanup_strv_free_ char **files = NULL; + r = conf_files_list_strv( + &files, + ".conf", + /* root= */ NULL, + CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN|CONF_FILES_DONT_PREFIX_ROOT, + (const char**) CONF_PATHS_STRV("repart.sysinstall.d")); + if (r < 0) + return log_error_errno(r, "Failed to enumerate *.conf files: %m"); + + if (!strv_isempty(files)) { + arg_definitions = strv_copy(CONF_PATHS_STRV("repart.sysinstall.d")); + if (!arg_definitions) + return log_oom(); + } + + return 0; +} + +static int run(int argc, char *argv[]) { + int r; + + setlocale(LC_ALL, ""); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + log_setup(); + + r = settle_definitions(); + if (r < 0) + return r; + + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *mute_console_link = NULL; + if (arg_welcome) { + if (arg_mute_console) + (void) mute_console(&mute_console_link); + + (void) terminal_reset_defensive_locked(STDOUT_FILENO, /* flags= */ 0); + + if (arg_chrome) + chrome_show("Operating System Installer", /* bottom= */ NULL); + } + + DEFER_VOID_CALL(chrome_hide); + + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *repart_link = NULL; + if (arg_node) { + r = print_welcome(&mute_console_link); + if (r < 0) + return r; + + r = validate_run(&repart_link, arg_node); + if (r < 0) + return r; + } else { + /* Determine the minimum disk size */ + uint64_t min_size = UINT64_MAX; + r = invoke_repart( + &repart_link, + /* node= */ NULL, + /* erase= */ true, + /* dry_run= */ true, + &min_size, + /* current_size= */ NULL, + /* need_free= */ NULL); + if (r < 0) + return r; + + r = print_welcome(&mute_console_link); + if (r < 0) + return r; + + log_info("Required minimal installation disk size is %s.", FORMAT_BYTES(min_size)); + + for (;;) { + _cleanup_free_ char *node = NULL; + r = prompt_block_device(&repart_link, &node); + if (r < 0) + return r; + + r = validate_run(&repart_link, node); + if (IN_SET(r, -ENOSPC, -E2BIG, -EHWPOISON)) /* Device is no fit, pick other */ + continue; + if (r < 0) + return r; + + arg_node = TAKE_PTR(node); + break; + } + } + + r = prompt_touch_variables(); + if (r < 0) + return r; + + r = read_credentials(); + if (r < 0) + return r; + + /* Verify we have everything we need */ + assert(arg_node); + assert(arg_erase >= 0); + assert(arg_touch_variables >= 0); + + r = show_summary(); + if (r < 0) + return r; + + r = prompt_confirm(); + if (r < 0) + return r; + + putchar('\n'); + + log_notice("%s%sEncrypting credentials...", + emoji_enabled() ? glyph(GLYPH_LOCK_AND_KEY) : "", emoji_enabled() ? " " : ""); + + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *creds_link = NULL; + _cleanup_strv_free_ char **encrypted_credentials = NULL; + r = encrypt_credentials(&creds_link, &encrypted_credentials); + if (r < 0) + return r; + + log_notice("%s%sInstalling partitions...", + emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : ""); + + /* Do the main part of the installation */ + r = invoke_repart( + &repart_link, + arg_node, + arg_erase, + /* dry_run= */ false, + /* min_size= */ NULL, + /* current_size= */ NULL, + /* need_free= */ NULL); + if (r < 0) + return r; + + log_notice("%s%sMounting partitions...", + emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : ""); + + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(umount_and_freep) char *root_dir = NULL; + _cleanup_close_ int root_fd = -EBADF; + r = mount_image_privately_interactively( + arg_node, + &image_policy, + DISSECT_IMAGE_REQUIRE_ROOT | + DISSECT_IMAGE_RELAX_VAR_CHECK | + DISSECT_IMAGE_ALLOW_USERSPACE_VERITY | + DISSECT_IMAGE_DISCARD_ANY | + DISSECT_IMAGE_GPT_ONLY | + DISSECT_IMAGE_FSCK | + DISSECT_IMAGE_USR_NO_ROOT | + DISSECT_IMAGE_ADD_PARTITION_DEVICES | + DISSECT_IMAGE_PIN_PARTITION_DEVICES, + &root_dir, + &root_fd, + &loop_device); + if (r < 0) + return log_error_errno(r, "Failed to mount new image: %m"); + + log_notice("%s%sInstalling kernel...", + emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : ""); + + _cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *bootctl_link = NULL; + r = invoke_bootctl_link(&bootctl_link, root_dir, root_fd, encrypted_credentials); + if (r < 0) + return r; + + log_notice("%s%sInstalling boot loader...", + emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : ""); + + r = invoke_bootctl_install(&bootctl_link, root_dir, root_fd); + if (r < 0) + return r; + + log_notice("%s%sUnmounting partitions...", + emoji_enabled() ? glyph(GLYPH_COMPUTER_DISK) : "", emoji_enabled() ? " " : ""); + + root_fd = safe_close(root_fd); + r = umount_recursive(root_dir, /* flags= */ 0); + if (r < 0) + log_warning_errno(r, "Failed to unmount target disk, proceeding anyway: %m"); + loop_device = loop_device_unref(loop_device); + sync(); + + log_notice("%s%sInstallation succeeded.", + emoji_enabled() ? glyph(GLYPH_SPARKLES) : "", emoji_enabled() ? " " : ""); + + r = maybe_reboot(); + if (r < 0) + return r; + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh b/test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh new file mode 100755 index 0000000000..d4ca6e0a0d --- /dev/null +++ b/test/units/TEST-87-AUX-UTILS-VM.sysinstall.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +if ! command -v systemd-sysinstall >/dev/null; then + echo "systemd-sysinstall not found, skipping." + exit 0 +fi + +if ! command -v systemd-repart >/dev/null; then + echo "systemd-repart not found, skipping." + exit 0 +fi + +if ! command -v bootctl >/dev/null; then + echo "bootctl not found, skipping." + exit 0 +fi + +if ! command -v ukify >/dev/null; then + echo "ukify not found, skipping." + exit 0 +fi + +if [[ ! -d /usr/lib/systemd/boot/efi ]]; then + echo "sd-boot is not installed, skipping." + exit 0 +fi + +# We need a real environment to fiddle with loop devices. +if systemd-detect-virt -cq; then + echo "Running in a container, skipping." + exit 0 +fi + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +WORKDIR="$(mktemp --directory /tmp/test-sysinstall.XXXXXXXXXX)" +LOOPDEV="" +MOUNTED=0 + +cleanup() { + set +e + if [[ "$MOUNTED" -eq 1 ]]; then + umount -R "$WORKDIR/mnt" + MOUNTED=0 + fi + if [[ -n "$LOOPDEV" ]]; then + systemd-dissect --detach "$LOOPDEV" + LOOPDEV="" + fi + rm -rf "$WORKDIR" +} +trap cleanup EXIT + +# 1) Build a small fake "OS source" tree. systemd-sysinstall picks this up via +# the repart.sysinstall.d definitions: CopyFiles= seeds the new root +# partition with these files. +SOURCE_ROOT="$WORKDIR/sourceroot" +mkdir -p "$SOURCE_ROOT/usr/lib" "$SOURCE_ROOT/etc" + +cat >"$SOURCE_ROOT/usr/lib/os-release" <<'EOF' +ID=testos +NAME="Test OS" +PRETTY_NAME="Test OS for systemd-sysinstall" +VERSION_ID=1 +EOF +ln -s ../usr/lib/os-release "$SOURCE_ROOT/etc/os-release" + +# 2) Build a minimal UKI. bootctl link only requires a valid PE with .osrel and +# the systemd-stub SBAT marker, so the .linux/.initrd contents do not need +# to be a real kernel. +echo "fake-kernel" >"$WORKDIR/vmlinuz" +echo "fake-initrd" >"$WORKDIR/initrd" + +ukify build \ + --linux "$WORKDIR/vmlinuz" \ + --initrd "$WORKDIR/initrd" \ + --os-release "@$SOURCE_ROOT/usr/lib/os-release" \ + --uname "1.2.3-testkernel" \ + --cmdline "quiet" \ + --output "$WORKDIR/testuki.efi" + +# 3) Build a sysinstall partition definition: a single ESP plus a root +# partition seeded from the fake source tree. +DEFS="$WORKDIR/sysinstall.d" +mkdir -p "$DEFS" + +cat >"$DEFS/10-esp.conf" <"$DEFS/20-root.conf" </dev/null + +# The UKI file referenced in the entry must exist on the ESP. +UKI_PATH=$(awk '/^uki / { print $2 }' "$ENTRY") +test -n "$UKI_PATH" +test -f "$ESP$UKI_PATH" + +# bootctl install should have placed sd-boot on the ESP. +find "$ESP/EFI/systemd" -type f -iname 'systemd-boot*.efi' | grep . >/dev/null + +# The credential we passed via --set-credential= must have been encrypted and +# placed next to the UKI, and must be referenced as 'extra' from the entry. +UKI_DIR="$(dirname "$ESP$UKI_PATH")" +TOKEN_DIR="$(basename "$UKI_DIR")" +test -s "$UKI_DIR/marker.cred" +grep -E "^extra /$TOKEN_DIR/marker\.cred$" "$ENTRY" >/dev/null + +# Locale/keymap/timezone propagation is off, so those .cred files must NOT +# exist on the ESP. +test ! -e "$UKI_DIR/firstboot.locale.cred" +test ! -e "$UKI_DIR/firstboot.keymap.cred" +test ! -e "$UKI_DIR/firstboot.timezone.cred" + +# 8) The seeded files from the fake source tree must end up in the new root. +test -f "$MNT/usr/lib/os-release" +grep '^ID=testos$' "$MNT/usr/lib/os-release" >/dev/null diff --git a/units/meson.build b/units/meson.build index 0f7ce75bd8..fca299aa84 100644 --- a/units/meson.build +++ b/units/meson.build @@ -240,6 +240,7 @@ units = [ }, { 'file' : 'sysinit.target' }, { 'file' : 'syslog.socket' }, + { 'file' : 'system-install.target' }, { 'file' : 'system-systemd\\x2dcryptsetup.slice', 'conditions' : ['HAVE_LIBCRYPTSETUP'], @@ -778,6 +779,11 @@ units = [ 'file' : 'systemd-sysext@.service', 'conditions' : ['ENABLE_SYSEXT'], }, + { + 'file' : 'systemd-sysinstall.service', + 'conditions' : ['ENABLE_SYSINSTALL'], + 'symlinks' : ['system-install.target.wants/'], + }, { 'file' : 'systemd-sysupdate-reboot.service.in', 'conditions' : ['ENABLE_SYSUPDATE'], diff --git a/units/system-install.target b/units/system-install.target new file mode 100644 index 0000000000..660110dcea --- /dev/null +++ b/units/system-install.target @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=System Installer +Documentation=man:systemd-sysinstall(8) +Requires=sysinit.target +After=sysinit.target +AllowIsolate=yes diff --git a/units/systemd-sysinstall.service b/units/systemd-sysinstall.service new file mode 100644 index 0000000000..a330db2ef3 --- /dev/null +++ b/units/systemd-sysinstall.service @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=System Install Tool +Documentation=man:systemd-sysinstall(8) +Wants=systemd-logind.service +After=systemd-logind.service + +[Service] +ExecStart=systemd-sysinstall --variables=yes --reboot=yes --mute-console=yes +StandardOutput=tty +StandardInput=tty +StandardError=tty +TTYReset=yes +FailureAction=halt