From: Michael Vogt Date: Tue, 30 Jun 2026 16:08:47 +0000 (+0200) Subject: firstboot: add new systemd.firstboot=headless mode X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=896fd702ba79648f7e45bd02c0b8dda688581331;p=thirdparty%2Fsystemd.git firstboot: add new systemd.firstboot=headless mode This adds a new systemd.firstboot=headless mode. It differs from the existing systemd.firstboot=no mode in that it still performs the non-interactive auto-configuration that requires no user input (such as selecting the only installed locale, or applying settings provided via credentials), and only skips the prompts that would otherwise block waiting for user input. In contrast, =no disables that auto-configuration along with the prompts. The option is also honoured by homectls firstboot logic and systemd-cryptenroll, where headless behaves the same as no (because the is no auto-configuration). --- 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);