From: Lennart Poettering Date: Thu, 28 Aug 2025 09:51:11 +0000 (+0200) Subject: sysinstall: new component X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=5fbc7a7b126d37e1179e5651ae72d831d92eb464;p=thirdparty%2Fsystemd.git sysinstall: new component --- diff --git a/man/rules/meson.build b/man/rules/meson.build index 719838064c0..c42a8d47f8e 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 00000000000..228e7e2bff1 --- /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 325b954a78b..d6fbd7c2b7e 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 d61afac519d..1917268d2ce 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 b0e56608e8f..cddf742059d 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 00000000000..600e2aa9395 --- /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 00000000000..039e4bf2a79 --- /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 6cc8a2d57f8..f10ba7be617 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 78c33c7553c..eb74de32c2d 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 9a66a90859d..fdaf11edcbf 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/sysinstall/meson.build b/src/sysinstall/meson.build new file mode 100644 index 00000000000..1d8be603656 --- /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 00000000000..d8f5cbee3c9 --- /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/units/meson.build b/units/meson.build index 0f7ce75bd89..fca299aa846 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 00000000000..660110dcea3 --- /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 00000000000..a330db2ef30 --- /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