From eb650ffedfaf6c919b1118e40120115e51354564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 30 Sep 2022 11:48:12 +0200 Subject: [PATCH 1/3] firstboot: add debug logging $ SYSTEMD_LOG_LEVEL=debug build/systemd-firstboot --prompt-root-password Found container virtualization systemd-nspawn. Found /etc/locale.conf, assuming locale information has been configured. Failed to read credential firstboot.keymap, ignoring: No such device or address Prompting for keymap was not requested. Found /etc/localtime, assuming timezone has been configured. Prompting for hostname was not requested. Found /etc/machine-id, assuming machine-id has been configured. Found /etc/passwd and /etc/shadow, assuming root account has been initialized. Creation of /etc/kernel/cmdline was not requested, skipping. --- src/firstboot/firstboot.c | 81 +++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index 74fd722e80..065ee896cd 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -259,8 +259,10 @@ static int prompt_locale(void) { return 0; } - if (!arg_prompt_locale) + if (!arg_prompt_locale) { + log_debug("Prompting for locale was not requested."); return 0; + } r = get_locales(&locales); if (r < 0) @@ -312,8 +314,11 @@ static int process_locale(void) { int r; etc_localeconf = prefix_roota(arg_root, "/etc/locale.conf"); - if (laccess(etc_localeconf, F_OK) >= 0 && !arg_force) + if (laccess(etc_localeconf, F_OK) >= 0 && !arg_force) { + log_debug("Found %s, assuming locale information has been configured.", + etc_localeconf); return 0; + } if (arg_copy_locale && arg_root) { @@ -366,12 +371,14 @@ static int prompt_keymap(void) { return 0; } - if (!arg_prompt_keymap) + if (!arg_prompt_keymap) { + log_debug("Prompting for keymap was not requested."); return 0; + } r = get_keymaps(&kmaps); if (r == -ENOENT) /* no keymaps installed */ - return r; + return log_debug_errno(r, "No keymaps are installed."); if (r < 0) return log_error_errno(r, "Failed to read keymaps: %m"); @@ -387,8 +394,11 @@ static int process_keymap(void) { int r; etc_vconsoleconf = prefix_roota(arg_root, "/etc/vconsole.conf"); - if (laccess(etc_vconsoleconf, F_OK) >= 0 && !arg_force) + if (laccess(etc_vconsoleconf, F_OK) >= 0 && !arg_force) { + log_debug("Found %s, assuming console has been configured.", + etc_vconsoleconf); return 0; + } if (arg_copy_keymap && arg_root) { @@ -445,8 +455,10 @@ static int prompt_timezone(void) { return 0; } - if (!arg_prompt_timezone) + if (!arg_prompt_timezone) { + log_debug("Prompting for timezone was not requested."); return 0; + } r = get_timezones(&zones); if (r < 0) @@ -467,8 +479,11 @@ static int process_timezone(void) { int r; etc_localtime = prefix_roota(arg_root, "/etc/localtime"); - if (laccess(etc_localtime, F_OK) >= 0 && !arg_force) + if (laccess(etc_localtime, F_OK) >= 0 && !arg_force) { + log_debug("Found %s, assuming timezone has been configured.", + etc_localtime); return 0; + } if (arg_copy_timezone && arg_root) { _cleanup_free_ char *p = NULL; @@ -512,8 +527,10 @@ static int prompt_hostname(void) { if (arg_hostname) return 0; - if (!arg_prompt_hostname) + if (!arg_prompt_hostname) { + log_debug("Prompting for hostname was not requested."); return 0; + } print_welcome(); putchar('\n'); @@ -549,8 +566,11 @@ static int process_hostname(void) { int r; etc_hostname = prefix_roota(arg_root, "/etc/hostname"); - if (laccess(etc_hostname, F_OK) >= 0 && !arg_force) + if (laccess(etc_hostname, F_OK) >= 0 && !arg_force) { + log_debug("Found %s, assuming hostname has been configured.", + etc_hostname); return 0; + } r = prompt_hostname(); if (r < 0) @@ -574,11 +594,16 @@ static int process_machine_id(void) { int r; etc_machine_id = prefix_roota(arg_root, "/etc/machine-id"); - if (laccess(etc_machine_id, F_OK) >= 0 && !arg_force) + if (laccess(etc_machine_id, F_OK) >= 0 && !arg_force) { + log_debug("Found %s, assuming machine-id has been configured.", + etc_machine_id); return 0; + } - if (sd_id128_is_null(arg_machine_id)) + if (sd_id128_is_null(arg_machine_id)) { + log_debug("Initialization of machine-id was not requested, skipping."); return 0; + } r = write_string_file(etc_machine_id, SD_ID128_TO_STRING(arg_machine_id), WRITE_STRING_FILE_CREATE | WRITE_STRING_FILE_SYNC | WRITE_STRING_FILE_MKDIR_0755 | @@ -600,8 +625,10 @@ static int prompt_root_password(void) { if (get_credential_user_password("root", &arg_root_password, &arg_root_password_is_hashed) >= 0) return 0; - if (!arg_prompt_root_password) + if (!arg_prompt_root_password) { + log_debug("Prompting for root password was not requested."); return 0; + } print_welcome(); putchar('\n'); @@ -684,8 +711,10 @@ static int prompt_root_shell(void) { return 0; } - if (!arg_prompt_root_shell) + if (!arg_prompt_root_shell) { + log_debug("Prompting for root shell was not requested."); return 0; + } print_welcome(); putchar('\n'); @@ -850,7 +879,7 @@ static int write_root_shadow(const char *shadow_path, const char *hashed_passwor return 0; } -static int process_root_args(void) { +static int process_root_account(void) { _cleanup_close_ int lock = -1; _cleanup_(erase_and_freep) char *_hashed_password = NULL; const char *password, *hashed_password; @@ -860,13 +889,18 @@ static int process_root_args(void) { etc_passwd = prefix_roota(arg_root, "/etc/passwd"); etc_shadow = prefix_roota(arg_root, "/etc/shadow"); - if (laccess(etc_passwd, F_OK) >= 0 && laccess(etc_shadow, F_OK) >= 0 && !arg_force) + if (laccess(etc_passwd, F_OK) >= 0 && laccess(etc_shadow, F_OK) >= 0 && !arg_force) { + log_debug("Found %s and %s, assuming root account has been initialized.", + etc_passwd, etc_shadow); return 0; + } /* Don't create/modify passwd and shadow if not asked */ if (!(arg_root_password || arg_prompt_root_password || arg_copy_root_password || arg_delete_root_password || - arg_root_shell || arg_prompt_root_shell || arg_copy_root_shell)) + arg_root_shell || arg_prompt_root_shell || arg_copy_root_shell)) { + log_debug("Initialization of root account was not requested, skipping."); return 0; + } (void) mkdir_parents(etc_passwd, 0755); @@ -945,11 +979,16 @@ static int process_kernel_cmdline(void) { int r; etc_kernel_cmdline = prefix_roota(arg_root, "/etc/kernel/cmdline"); - if (laccess(etc_kernel_cmdline, F_OK) >= 0 && !arg_force) + if (laccess(etc_kernel_cmdline, F_OK) >= 0 && !arg_force) { + log_debug("Found %s, assuming kernel has been configured.", + etc_kernel_cmdline); return 0; + } - if (!arg_kernel_cmdline) + if (!arg_kernel_cmdline) { + log_debug("Creation of /etc/kernel/cmdline was not requested, skipping."); return 0; + } r = write_string_file(etc_kernel_cmdline, arg_kernel_cmdline, WRITE_STRING_FILE_CREATE | WRITE_STRING_FILE_SYNC | WRITE_STRING_FILE_MKDIR_0755 | @@ -1332,8 +1371,10 @@ static int run(int argc, char *argv[]) { r = proc_cmdline_get_bool("systemd.firstboot", &enabled); if (r < 0) return log_error_errno(r, "Failed to parse systemd.firstboot= kernel command line argument, ignoring: %m"); - if (r > 0 && !enabled) + if (r > 0 && !enabled) { + log_debug("Found systemd.firstboot=no kernel command line argument, terminating."); return 0; /* disabled */ + } } if (arg_image) { @@ -1377,7 +1418,7 @@ static int run(int argc, char *argv[]) { if (r < 0) return r; - r = process_root_args(); + r = process_root_account(); if (r < 0) return r; From 7cd43e34c5a302ff323c013f437092d2ff5ccbbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 30 Sep 2022 12:50:40 +0200 Subject: [PATCH 2/3] manager: fix/change evaluation of ConditionFirstBoot The code to evaluate the kernel command line option was busted because it was doing 'return b == !!r' at a point where 'r > 0'. Thus we'd return "true" in both cases: $ SYSTEMD_PROC_CMDLINE=systemd.condition-first-boot build/systemd-analyze condition 'ConditionFirstBoot=true' test.service: ConditionFirstBoot=true succeeded. Conditions succeeded. $ SYSTEMD_PROC_CMDLINE=systemd.condition-first-boot build/systemd-analyze condition 'ConditionFirstBoot=false' test.service: ConditionFirstBoot=false succeeded. Conditions succeeded. We only use 'ConditionFirstBoot=true' in units, so this wasn't noticed. But I think the logic is broken in general: the condition should evaluate as true only during initial boot. If we rerun the units at later points, we should not consider ConditionFirstBoot to be true. Also, the first boot logic is also used in pid1 itself. AFAICT, for two things: in first boot machine-id is initialized transiently (this allows first-boot operations to be restarted if boot fails), and preset-all is executed. But this logic was different and separate from the logic to evaluate ConditionFirstBoot. The distinction is abolished, and the operations in pid1 now use the same logic as ConditionFirstBoot, which means that the kernel command line option is checked, and condition_test_first_boot() just tests whether pid1 thinks we're in first boot. This makes things easier to grok for the user: there's just one condition for "first boot" and it applies to both pid1 and units. --- man/machine-id.xml | 24 ++++++++++++------- man/systemd.unit.xml | 13 +++++++---- src/core/main.c | 53 ++++++++++++++++++++++++++---------------- src/shared/condition.c | 11 ++------- 4 files changed, 58 insertions(+), 43 deletions(-) diff --git a/man/machine-id.xml b/man/machine-id.xml index 9bd49582fc..b40e26bbab 100644 --- a/man/machine-id.xml +++ b/man/machine-id.xml @@ -119,20 +119,26 @@ First Boot Semantics - /etc/machine-id is used to decide whether a boot is the first one. The rules + /etc/machine-id is used to decide whether a boot is the first one. The rules are as follows: - If /etc/machine-id does not exist, this is a first boot. During - early boot, systemd will write uninitialized\n to this file and overmount - a temporary file which contains the actual machine ID. Later (after first-boot-complete.target - has been reached), the real machine ID will be written to disk. + The kernel command argument systemd.condition-first-boot= may be + used to override the autodetection logic, see + kernel-command-line7. + + + Otherwise, if /etc/machine-id does not exist, this is a first + boot. During early boot, systemd will write uninitialized\n to + this file and overmount a temporary file which contains the actual machine ID. Later (after + first-boot-complete.target has been reached), the real machine ID will be written + to disk. If /etc/machine-id contains the string uninitialized, - a boot is also considered the first boot. The same mechanism as above applies. + a boot is also considered the first boot. The same mechanism as above applies. If /etc/machine-id exists and is empty, a boot is - not considered the first boot. systemd will still bind-mount a file + not considered the first boot. systemd will still bind-mount a file containing the actual machine-id over it and later try to commit it to disk (if /etc/ is writable). @@ -140,8 +146,8 @@ not a first boot. - If by any of the above rules, a first boot is detected, units with ConditionFirstBoot=yes - will be run. + If according to the above rules a first boot is detected, units with + ConditionFirstBoot=yes will be run. diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml index d502b6da18..ebb84e9db8 100644 --- a/man/systemd.unit.xml +++ b/man/systemd.unit.xml @@ -1455,15 +1455,18 @@ ConditionFirstBoot= Takes a boolean argument. This condition may be used to conditionalize units on - whether the system is booting up for the first time. This roughly means that /etc/ - is unpopulated (for details, see "First Boot Semantics" in + whether the system is booting up for the first time. This roughly means that /etc/ + was unpopulated when the system started booting (for details, see "First Boot Semantics" in machine-id5). - This may be used to populate /etc/ on the first boot after factory reset, or - when a new system instance boots up for the first time. + First boot is considered finished (this condition will evaluate as false) after the manager + has finished the startup phase. + + This condition may be used to populate /etc/ on the first boot after + factory reset, or when a new system instance boots up for the first time. For robustness, units with ConditionFirstBoot=yes should order themselves before first-boot-complete.target and pull in this passive target with - Wants=. This ensures that in a case of an aborted first boot, these units will + Wants=. This ensures that in a case of an aborted first boot, these units will be re-run during the next system startup. If the systemd.condition-first-boot= option is specified on the kernel diff --git a/src/core/main.c b/src/core/main.c index 27ed8a89ba..14a4f81452 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -2034,6 +2034,8 @@ static int invoke_main_loop( } static void log_execution_mode(bool *ret_first_boot) { + bool first_boot = false; + assert(ret_first_boot); if (arg_system) { @@ -2050,29 +2052,40 @@ static void log_execution_mode(bool *ret_first_boot) { log_info("Detected architecture %s.", architecture_to_string(uname_architecture())); - if (in_initrd()) { - *ret_first_boot = false; + if (in_initrd()) log_info("Running in initrd."); - } else { + else { int r; _cleanup_free_ char *id_text = NULL; - /* Let's check whether we are in first boot. We use /etc/machine-id as flag file - * for this: If it is missing or contains the value "uninitialized", this is the - * first boot. In any other case, it is not. This allows container managers and - * installers to provision a couple of files already. If the container manager - * wants to provision the machine ID itself it should pass $container_uuid to PID 1. */ + /* Let's check whether we are in first boot. First, check if an override was + * specified on the kernel commandline. If yes, we honour that. */ - r = read_one_line_file("/etc/machine-id", &id_text); - if (r < 0 || streq(id_text, "uninitialized")) { - if (r < 0 && r != -ENOENT) - log_warning_errno(r, "Unexpected error while reading /etc/machine-id, ignoring: %m"); + r = proc_cmdline_get_bool("systemd.condition-first-boot", &first_boot); + if (r < 0) + log_debug_errno(r, "Failed to parse systemd.condition-first-boot= kernel commandline argument, ignoring: %m"); - *ret_first_boot = true; - log_info("Detected first boot."); - } else { - *ret_first_boot = false; - log_debug("Detected initialized system, this is not the first boot."); + if (r > 0) + log_full(first_boot ? LOG_INFO : LOG_DEBUG, + "Kernel commandline argument says we are %s first boot.", + first_boot ? "in" : "not in"); + else { + /* Second, perform autodetection. We use /etc/machine-id as flag file for + * this: If it is missing or contains the value "uninitialized", this is the + * first boot. In other cases, it is not. This allows container managers and + * installers to provision a couple of files in /etc but still permit the + * first-boot initialization to occur. If the container manager wants to + * provision the machine ID it should pass $container_uuid to PID 1. */ + + r = read_one_line_file("/etc/machine-id", &id_text); + if (r < 0 || streq(id_text, "uninitialized")) { + if (r < 0 && r != -ENOENT) + log_warning_errno(r, "Unexpected error while reading /etc/machine-id, ignoring: %m"); + + first_boot = true; + log_info("Detected first boot."); + } else + log_debug("Detected initialized system, this is not the first boot."); } } @@ -2092,9 +2105,9 @@ static void log_execution_mode(bool *ret_first_boot) { arg_action == ACTION_TEST ? " test" : "", getuid(), strna(t), systemd_features); } - - *ret_first_boot = false; } + + *ret_first_boot = first_boot; } static int initialize_runtime( @@ -2134,7 +2147,7 @@ static int initialize_runtime( (void) os_release_status(); (void) hostname_setup(true); /* Force transient machine-id on first boot. */ - machine_id_setup(NULL, first_boot, arg_machine_id, NULL); + machine_id_setup(NULL, /* force_transient= */ first_boot, arg_machine_id, NULL); (void) loopback_setup(); bump_unix_max_dgram_qlen(); bump_file_max_and_nr_open(); diff --git a/src/shared/condition.c b/src/shared/condition.c index ffca2006c0..aa34e1e285 100644 --- a/src/shared/condition.c +++ b/src/shared/condition.c @@ -824,27 +824,20 @@ static int condition_test_needs_update(Condition *c, char **env) { static int condition_test_first_boot(Condition *c, char **env) { int r, q; - bool b; assert(c); assert(c->parameter); assert(c->type == CONDITION_FIRST_BOOT); - r = proc_cmdline_get_bool("systemd.condition-first-boot", &b); - if (r < 0) - log_debug_errno(r, "Failed to parse systemd.condition-first-boot= kernel command line argument, ignoring: %m"); - if (r > 0) - return b == !!r; - r = parse_boolean(c->parameter); if (r < 0) return r; q = access("/run/systemd/first-boot", F_OK); if (q < 0 && errno != ENOENT) - log_debug_errno(errno, "Failed to check if /run/systemd/first-boot exists, ignoring: %m"); + log_debug_errno(errno, "Failed to check if /run/systemd/first-boot exists, assuming no: %m"); - return (q >= 0) == !!r; + return (q >= 0) == r; } static int condition_test_environment(Condition *c, char **env) { From bfa769d185dab1d2e4d63cedca16e6fc96ddad8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= Date: Fri, 30 Sep 2022 15:31:40 +0200 Subject: [PATCH 3/3] man: add cross-links about preset operations --- man/machine-id.xml | 3 ++- man/systemd.preset.xml | 16 ++++++++++------ man/systemd.xml | 9 +++++++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/man/machine-id.xml b/man/machine-id.xml index b40e26bbab..ec1ab64dec 100644 --- a/man/machine-id.xml +++ b/man/machine-id.xml @@ -147,7 +147,8 @@ If according to the above rules a first boot is detected, units with - ConditionFirstBoot=yes will be run. + ConditionFirstBoot=yes will be run and systemd will perform + additional initialization steps, in particular presetting units. diff --git a/man/systemd.preset.xml b/man/systemd.preset.xml index 9e6db28536..ab730d2cc2 100644 --- a/man/systemd.preset.xml +++ b/man/systemd.preset.xml @@ -47,12 +47,16 @@ units, but rather centralize them in a distribution or spin default policy, which can be amended by administrator policy, see below. - If no preset files exist, systemctl - preset will enable all units that are installed by - default. If this is not desired and all units shall rather be - disabled, it is necessary to ship a preset file with a single, - catchall "disable *" line. (See example 1, - below.) + If no preset files exist, preset operations will enable all units that are installed by default. If + this is not desired and all units shall rather be disabled, it is necessary to ship a preset file with a + single, catchall "disable *" line. (See example 1, below.) + + When the machine is booted for the first time, + systemd1 will + enable/disable all units according to preset policy, similarly to systemctl + preset-all. Also see "First Boot Semantics" in + machine-id5. + diff --git a/man/systemd.xml b/man/systemd.xml index f4213e2ee8..1a68301d50 100644 --- a/man/systemd.xml +++ b/man/systemd.xml @@ -188,8 +188,13 @@ systemd.special7 for details about these target units. - systemd only keeps a minimal set of units loaded into memory. Specifically, the only units that are kept - loaded into memory are those for which at least one of the following conditions is true: + On first boot, systemd will enable or disable units according to preset policy. + See systemd.preset5 + and "First Boot Semantics" in + machine-id5. + + systemd only keeps a minimal set of units loaded into memory. Specifically, the only units that are + kept loaded into memory are those for which at least one of the following conditions is true: It is in an active, activating, deactivating or failed state (i.e. in any unit state except for inactive)