diff --git a/NEWS b/NEWS index d34215377eb..ef3d070df19 100644 --- a/NEWS +++ b/NEWS @@ -16,6 +16,17 @@ CHANGES WITH 262: have been provided. This clears the way for a new "systemd-sysupdate@.service" unit for varlink activation of sysupdate. + Changes in systemd-firstboot: + + * The "systemd.firstboot=" kernel command line option now accepts the + special value "headless" in addition to a boolean. Like "no", it + suppresses all interactive prompts, but unlike "no" it still performs + non-interactive auto-configuration that requires no user input (such + as selecting the sole installed locale, or applying settings provided + via credentials). This is useful for unattended installations that + should be provisioned as far as possible without ever blocking on a + prompt. + CHANGES WITH 261: Announcements of Future Feature Removals and Incompatible Changes: diff --git a/man/kernel-command-line.xml b/man/kernel-command-line.xml index 8253f0fafd4..3a6a5df8200 100644 --- a/man/kernel-command-line.xml +++ b/man/kernel-command-line.xml @@ -645,15 +645,20 @@ systemd.firstboot= - Takes a boolean argument, defaults to on. If off, + Takes a boolean argument or the special value headless, defaults to + on. If off, systemd-firstboot.service8 and systemd-homed-firstboot.service1 will not query the user for basic system settings, even if the system boots up for the first time and - the relevant settings are not initialized yet. Not to be confused with - systemd.condition_first_boot= (see below), which overrides the result of the - ConditionFirstBoot= unit file condition, and thus controls more than just - systemd-firstboot.service behaviour. + the relevant settings are not initialized yet. If set to headless, the same + interactive prompts are suppressed as with no, but non-interactive + auto-configuration that requires no user input is still performed (for example, selecting the only + installed locale, or applying settings provided via credentials). This is useful for unattended + installations that should be provisioned as far as possible without ever blocking on a prompt. Not to + be confused with systemd.condition_first_boot= (see below), which overrides the + result of the ConditionFirstBoot= unit file condition, and thus controls more than + just systemd-firstboot.service behaviour. diff --git a/man/systemd-firstboot.xml b/man/systemd-firstboot.xml index 252ec73fedd..62e7c36dfe4 100644 --- a/man/systemd-firstboot.xml +++ b/man/systemd-firstboot.xml @@ -521,9 +521,12 @@ systemd.firstboot= - Takes a boolean argument, defaults to on. If off, systemd-firstboot.service - will not interactively query the user for basic settings at first boot, even if those settings are not - initialized yet. + Takes a boolean argument or the special value headless, defaults to + on. If off, systemd-firstboot.service will not interactively query the user for + basic settings at first boot, even if those settings are not initialized yet. If set to + headless, interactive prompts are suppressed just like with no, + but non-interactive auto-configuration that requires no user input is still performed (for example, + selecting the sole installed locale, or applying settings passed in via credentials). diff --git a/src/cryptenroll/cryptenroll-interactive.c b/src/cryptenroll/cryptenroll-interactive.c index 6dc584b18b4..c4b8c998bf5 100644 --- a/src/cryptenroll/cryptenroll-interactive.c +++ b/src/cryptenroll/cryptenroll-interactive.c @@ -8,10 +8,10 @@ #include "cryptenroll-interactive.h" #include "cryptenroll-list.h" #include "cryptsetup-util.h" +#include "firstboot-util.h" #include "glyph-util.h" #include "libfido2-util.h" #include "log.h" -#include "proc-cmdline.h" #include "prompt-util.h" #include "string-util.h" #include "strv.h" @@ -176,12 +176,15 @@ int cryptenroll_run_interactive( assert(c->node); /* Honour the systemd.firstboot= kernel command line option, just like systemd-firstboot. */ - bool enabled; - r = proc_cmdline_get_bool("systemd.firstboot", PROC_CMDLINE_TRUE_WHEN_MISSING, &enabled); + FirstBootMode mode; + _cleanup_free_ char *bad = NULL; + r = firstboot_mode_from_cmdline(&mode, &bad); if (r < 0) - log_warning_errno(r, "Failed to parse systemd.firstboot= kernel command line option, ignoring: %m"); - else if (!enabled) { - log_debug("systemd.firstboot=no set, skipping interactive enrollment."); + log_warning_errno(r, "Failed to parse systemd.firstboot= kernel command line option, ignoring%s: %m", + bad ? strjoina(" (invalid value '", bad, "')") : ""); + else if (IN_SET(mode, FIRSTBOOT_NO, FIRSTBOOT_HEADLESS)) { + log_debug("systemd.firstboot=%s set, skipping interactive enrollment.", + firstboot_mode_to_string(mode)); return 0; } diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index 2200235e1d1..e81923beb5b 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -24,6 +24,7 @@ #include "errno-util.h" #include "fd-util.h" #include "fileio.h" +#include "firstboot-util.h" #include "format-table.h" #include "fs-util.h" #include "glyph-util.h" @@ -47,7 +48,6 @@ #include "password-quality-util.h" #include "path-util.h" #include "plymouth-util.h" -#include "proc-cmdline.h" #include "prompt-util.h" #include "runtime-scope.h" #include "smack-util.h" @@ -79,6 +79,7 @@ static bool arg_prompt_timezone = false; static bool arg_prompt_hostname = false; static bool arg_prompt_root_password = false; static bool arg_prompt_root_shell = false; +static bool arg_headless = false; static bool arg_copy_locale = false; static bool arg_copy_keymap = false; static bool arg_copy_timezone = false; @@ -244,6 +245,16 @@ static int locale_is_ok(const char *name, void *userdata) { return r != 0 ? locale_is_installed(name) > 0 : locale_is_valid(name); } +static bool headless_skips_prompt_for(const char *what) { + assert(what); + + if (!arg_headless) + return false; + + log_debug("Running headless, not prompting for %s.", what); + return true; +} + static int prompt_locale(int rfd, sd_varlink **mute_console_link) { _cleanup_strv_free_ char **locales = NULL; bool acquired_from_creds = false; @@ -296,6 +307,9 @@ static int prompt_locale(int rfd, sd_varlink **mute_console_link) { /* Not setting arg_locale_message here, since it defaults to LANG anyway */ } } else { + if (headless_skips_prompt_for("locale")) + return 0; + print_welcome(rfd, mute_console_link); _cleanup_free_ char *prefill = NULL; @@ -456,6 +470,9 @@ static int prompt_keymap(int rfd, sd_varlink **mute_console_link) { return 0; } + if (headless_skips_prompt_for("keymap")) + return 0; + r = get_keymaps(&kmaps); if (r == -ENOENT) /* no keymaps installed */ return log_debug_errno(r, "No keymaps are installed."); @@ -578,6 +595,9 @@ static int prompt_timezone(int rfd, sd_varlink **mute_console_link) { return 0; } + if (headless_skips_prompt_for("timezone")) + return 0; + r = get_timezones(&zones); if (r < 0) return log_error_errno(r, "Cannot query timezone list: %m"); @@ -691,6 +711,9 @@ static int prompt_hostname(int rfd, sd_varlink **mute_console_link) { return 0; } + if (headless_skips_prompt_for("hostname")) + return 0; + print_welcome(rfd, mute_console_link); r = prompt_loop("Please enter the new hostname", @@ -879,6 +902,9 @@ static int prompt_root_password(int rfd, sd_varlink **mute_console_link) { return 0; } + if (headless_skips_prompt_for("root password")) + return 0; + print_welcome(rfd, mute_console_link); msg1 = "Please enter the new root password (empty to skip):"; @@ -980,6 +1006,9 @@ static int prompt_root_shell(int rfd, sd_varlink **mute_console_link) { return 0; } + if (headless_skips_prompt_for("root shell")) + return 0; + print_welcome(rfd, mute_console_link); return prompt_loop( @@ -1688,13 +1717,18 @@ static int run(int argc, char *argv[]) { * command line option, because we are called to provision the host with basic settings (as * opposed to some other file system tree/image) */ - bool enabled; - r = proc_cmdline_get_bool("systemd.firstboot", /* flags= */ 0, &enabled); + FirstBootMode mode; + _cleanup_free_ char *bad = NULL; + r = firstboot_mode_from_cmdline(&mode, &bad); if (r < 0) - return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m"); - if (r > 0 && !enabled) { + return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument%s: %m", + bad ? strjoina(" (invalid value '", bad, "')") : ""); + if (mode == FIRSTBOOT_NO) { log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts."); arg_prompt_locale = arg_prompt_keymap = arg_prompt_keymap_auto = arg_prompt_timezone = arg_prompt_hostname = arg_prompt_root_password = arg_prompt_root_shell = false; + } else if (mode == FIRSTBOOT_HEADLESS) { + log_debug("Found systemd.firstboot=headless kernel command line argument, skipping interactive prompts but keeping non-interactive auto-configuration."); + arg_headless = true; } } diff --git a/src/home/homectl.c b/src/home/homectl.c index c0dab67989d..0da671bca65 100644 --- a/src/home/homectl.c +++ b/src/home/homectl.c @@ -25,6 +25,7 @@ #include "extract-word.h" #include "fd-util.h" #include "fileio.h" +#include "firstboot-util.h" #include "format-table.h" #include "format-util.h" #include "fs-util.h" @@ -52,7 +53,6 @@ #include "pkcs11-util.h" #include "plymouth-util.h" #include "polkit-agent.h" -#include "proc-cmdline.h" #include "process-util.h" #include "prompt-util.h" #include "recurse-dir.h" @@ -3120,12 +3120,15 @@ static int verb_firstboot(int argc, char *argv[], uintptr_t _data, void *userdat /* Let's honour the systemd.firstboot kernel command line option, just like the systemd-firstboot * tool. */ - bool enabled; - r = proc_cmdline_get_bool("systemd.firstboot", /* flags= */ 0, &enabled); + FirstBootMode mode; + _cleanup_free_ char *bad = NULL; + r = firstboot_mode_from_cmdline(&mode, &bad); if (r < 0) - return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m"); - if (r > 0 && !enabled) { - log_debug("Found systemd.firstboot=no kernel command line argument, turning off all prompts."); + return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument%s: %m", + bad ? strjoina(" (invalid value '", bad, "')") : ""); + if (IN_SET(mode, FIRSTBOOT_NO, FIRSTBOOT_HEADLESS)) { + log_debug("Found systemd.firstboot=%s kernel command line argument, turning off all prompts.", + firstboot_mode_to_string(mode)); arg_prompt_new_user = false; } diff --git a/src/shared/firstboot-util.c b/src/shared/firstboot-util.c new file mode 100644 index 00000000000..c848a4ab054 --- /dev/null +++ b/src/shared/firstboot-util.c @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "firstboot-util.h" +#include "proc-cmdline.h" +#include "string-table.h" + +static const char* const firstboot_mode_table[_FIRSTBOOT_MODE_MAX] = { + [FIRSTBOOT_NO] = "no", + [FIRSTBOOT_INTERACTIVE] = "interactive", + [FIRSTBOOT_HEADLESS] = "headless", +}; + +assert_cc(FIRSTBOOT_NO == 0); + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(firstboot_mode, FirstBootMode, FIRSTBOOT_INTERACTIVE); + +int firstboot_mode_from_cmdline(FirstBootMode *ret, char **reterr_value) { + _cleanup_free_ char *value = NULL; + int r; + + assert(ret); + + r = proc_cmdline_get_key("systemd.firstboot", PROC_CMDLINE_VALUE_OPTIONAL, &value); + if (r < 0) + return r; + if (r == 0) { /* not specified at all */ + *ret = FIRSTBOOT_INTERACTIVE; + return 0; + } + if (!value) { /* key without parameter, i.e. bare "systemd.firstboot" */ + *ret = FIRSTBOOT_INTERACTIVE; + return 1; + } + + FirstBootMode m = firstboot_mode_from_string(value); + if (m < 0) { + if (reterr_value) + *reterr_value = TAKE_PTR(value); + return m; + } + + *ret = m; + return 1; +} diff --git a/src/shared/firstboot-util.h b/src/shared/firstboot-util.h new file mode 100644 index 00000000000..e87da6f18c5 --- /dev/null +++ b/src/shared/firstboot-util.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "shared-forward.h" + +/* Parsed value of the systemd.firstboot= kernel command line option, honoured by systemd-firstboot, + * homectl's firstboot logic and systemd-cryptenroll. */ +typedef enum FirstBootMode { + FIRSTBOOT_NO, /* "no": don't prompt, don't auto-configure */ + FIRSTBOOT_INTERACTIVE, /* "interactive" or unset: prompt as needed */ + FIRSTBOOT_HEADLESS, /* "headless": auto-configure, but never prompt */ + _FIRSTBOOT_MODE_MAX, + _FIRSTBOOT_MODE_INVALID = -EINVAL, +} FirstBootMode; + +DECLARE_STRING_TABLE_LOOKUP(firstboot_mode, FirstBootMode); + +int firstboot_mode_from_cmdline(FirstBootMode *ret, char **reterr_value); diff --git a/src/shared/meson.build b/src/shared/meson.build index 7c6505e0151..f9367ac2265 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -85,6 +85,7 @@ shared_sources = files( 'fido2-util.c', 'find-esp.c', 'firewall-util.c', + 'firstboot-util.c', 'fork-notify.c', 'format-table.c', 'fsprg-openssl.c', diff --git a/src/test/meson.build b/src/test/meson.build index 9d5a79c78e5..c90235e0789 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -108,6 +108,7 @@ simple_tests += files( 'test-fiemap.c', 'test-fileio.c', 'test-firewall-util.c', + 'test-firstboot-util.c', 'test-format-table.c', 'test-format-util.c', 'test-fs-util.c', diff --git a/src/test/test-firstboot-util.c b/src/test/test-firstboot-util.c new file mode 100644 index 00000000000..bb3c30321f2 --- /dev/null +++ b/src/test/test-firstboot-util.c @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "firstboot-util.h" +#include "tests.h" + +TEST(firstboot_mode_from_string) { + assert_se(firstboot_mode_from_string("yes") == FIRSTBOOT_INTERACTIVE); + assert_se(firstboot_mode_from_string("1") == FIRSTBOOT_INTERACTIVE); + assert_se(firstboot_mode_from_string("on") == FIRSTBOOT_INTERACTIVE); + assert_se(firstboot_mode_from_string("true") == FIRSTBOOT_INTERACTIVE); + assert_se(firstboot_mode_from_string("interactive") == FIRSTBOOT_INTERACTIVE); + + assert_se(firstboot_mode_from_string("no") == FIRSTBOOT_NO); + assert_se(firstboot_mode_from_string("0") == FIRSTBOOT_NO); + assert_se(firstboot_mode_from_string("off") == FIRSTBOOT_NO); + assert_se(firstboot_mode_from_string("false") == FIRSTBOOT_NO); + + assert_se(firstboot_mode_from_string("headless") == FIRSTBOOT_HEADLESS); + + assert_se(firstboot_mode_from_string("") == _FIRSTBOOT_MODE_INVALID); + assert_se(firstboot_mode_from_string(NULL) == _FIRSTBOOT_MODE_INVALID); + assert_se(firstboot_mode_from_string("Headless") == _FIRSTBOOT_MODE_INVALID); + assert_se(firstboot_mode_from_string("maybe") == _FIRSTBOOT_MODE_INVALID); +} + +DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/test/test-tables.c b/src/test/test-tables.c index a70e5b342d5..b85ea33acf5 100644 --- a/src/test/test-tables.c +++ b/src/test/test-tables.c @@ -12,6 +12,7 @@ #include "device-private.h" #include "discover-image.h" #include "execute.h" +#include "firstboot-util.h" #include "gpt.h" #include "import-util.h" #include "install.h" @@ -62,6 +63,7 @@ int main(int argc, char **argv) { test_table(ExecOutput, exec_output, EXEC_OUTPUT); test_table(ExecPreserveMode, exec_preserve_mode, EXEC_PRESERVE_MODE); test_table(ExecUtmpMode, exec_utmp_mode, EXEC_UTMP_MODE); + test_table(FirstBootMode, firstboot_mode, FIRSTBOOT_MODE); test_table(ImageType, image_type, IMAGE_TYPE); test_table(ImportVerify, import_verify, IMPORT_VERIFY); test_table(JobMode, job_mode, JOB_MODE);