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);