diff --git a/man/machine-id.xml b/man/machine-id.xml index cf759f2a0e..7e889ca47a 100644 --- a/man/machine-id.xml +++ b/man/machine-id.xml @@ -82,10 +82,11 @@ For operating system images which are created once and used on multiple machines, for example for containers or in the cloud, - /etc/machine-id should be an empty file in the generic file - system image. An ID will be generated during boot and saved to this file if - possible. Having an empty file in place is useful because it allows a temporary file - to be bind-mounted over the real file, in case the image is used read-only. + /etc/machine-id should be either missing or an empty file in the generic file + system image (the difference between the two options is described under "First Boot Semantics" below). An + ID will be generated during boot and saved to this file if possible. Having an empty file in place is + useful because it allows a temporary file to be bind-mounted over the real file, in case the image is + used read-only. systemd-firstboot1 may be used to initialize /etc/machine-id on mounted (but not @@ -115,6 +116,34 @@ early boot but become writable later on. + + First Boot Semantics + + /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 unitialized\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. + + If /etc/machine-id exists and is empty, a boot is + 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). + + If /etc/machine-id already contains a valid machine-id, this is + not a first boot. + + + If by any of the above rules, a first boot is detected, units with ConditionFirstBoot=yes + will be run. + + Relation to OSF UUIDs diff --git a/man/systemd.special.xml b/man/systemd.special.xml index e43d765c9f..1434ead7d5 100644 --- a/man/systemd.special.xml +++ b/man/systemd.special.xml @@ -32,6 +32,7 @@ emergency.target, exit.target, final.target, + first-boot-complete.target, getty.target, getty-pre.target, graphical.target, @@ -878,6 +879,17 @@ stopped. + + first-boot-complete.target + + This passive target is intended as a synchronization point for units that need to run once + during the first boot. Only after all units ordered before this target have finished, will the + machine-id5 + be committed to disk, marking the first boot as completed. If the boot is aborted at any time + before that, the next boot will re-run any units with ConditionFirstBoot=yes. + + + getty-pre.target diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml index 27035798bc..4b8e515fe2 100644 --- a/man/systemd.unit.xml +++ b/man/systemd.unit.xml @@ -1315,10 +1315,16 @@ ConditionFirstBoot= Takes a boolean argument. This condition may be used to conditionalize units on - whether the system is booting up with an unpopulated /etc/ directory - (specifically: an /etc/ with no /etc/machine-id). 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. + whether the system is booting up for the first time. This roughly means that /etc/ + is unpopulated (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. + + 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 + be re-run during the next system startup. If the systemd.condition-first-boot= option is specified on the kernel command line (taking a boolean), it will override the result of this condition check, taking diff --git a/presets/90-systemd.preset b/presets/90-systemd.preset index dd268ae1b5..9e430904f1 100644 --- a/presets/90-systemd.preset +++ b/presets/90-systemd.preset @@ -37,6 +37,7 @@ disable systemd-networkd-wait-online.service disable systemd-time-wait-sync.service disable systemd-boot-check-no-failures.service disable systemd-network-generator.service +disable proc-sys-fs-binfmt_misc.mount disable syslog.socket diff --git a/src/core/machine-id-setup.c b/src/core/machine-id-setup.c index 6aecd36fe6..c55e0b5321 100644 --- a/src/core/machine-id-setup.c +++ b/src/core/machine-id-setup.c @@ -11,6 +11,7 @@ #include "fd-util.h" #include "fs-util.h" #include "id128-util.h" +#include "io-util.h" #include "log.h" #include "machine-id-setup.h" #include "macro.h" @@ -86,7 +87,7 @@ static int generate_machine_id(const char *root, sd_id128_t *ret) { return 0; } -int machine_id_setup(const char *root, sd_id128_t machine_id, sd_id128_t *ret) { +int machine_id_setup(const char *root, bool force_transient, sd_id128_t machine_id, sd_id128_t *ret) { const char *etc_machine_id, *run_machine_id; _cleanup_close_ int fd = -1; bool writable; @@ -143,13 +144,31 @@ int machine_id_setup(const char *root, sd_id128_t machine_id, sd_id128_t *ret) { if (ftruncate(fd, 0) < 0) return log_error_errno(errno, "Failed to truncate %s: %m", etc_machine_id); - if (id128_write_fd(fd, ID128_PLAIN, machine_id, true) >= 0) - goto finish; + /* If the caller requested a transient machine-id, write the string "uninitialized\n" to + * disk and overmount it with a transient file. + * + * Otherwise write the machine-id directly to disk. */ + if (force_transient) { + r = loop_write(fd, "uninitialized\n", strlen("uninitialized\n"), false); + if (r < 0) + return log_error_errno(r, "Failed to write uninitialized %s: %m", etc_machine_id); + + r = fsync_full(fd); + if (r < 0) + return log_error_errno(r, "Failed to sync %s: %m", etc_machine_id); + } else { + r = id128_write_fd(fd, ID128_PLAIN, machine_id, true); + if (r < 0) + return log_error_errno(r, "Failed to write %s: %m", etc_machine_id); + else + goto finish; + } } fd = safe_close(fd); - /* Hmm, we couldn't write it? So let's write it to /run/machine-id as a replacement */ + /* Hmm, we couldn't or shouldn't write the machine-id to /etc? + * So let's write it to /run/machine-id as a replacement */ run_machine_id = prefix_roota(root, "/run/machine-id"); @@ -167,7 +186,7 @@ int machine_id_setup(const char *root, sd_id128_t machine_id, sd_id128_t *ret) { return r; } - log_info("Installed transient %s file.", etc_machine_id); + log_full(force_transient ? LOG_DEBUG : LOG_INFO, "Installed transient %s file.", etc_machine_id); /* Mark the mount read-only */ r = mount_follow_verbose(LOG_WARNING, NULL, etc_machine_id, NULL, MS_BIND|MS_RDONLY|MS_REMOUNT, NULL); @@ -183,10 +202,22 @@ finish: int machine_id_commit(const char *root) { _cleanup_close_ int fd = -1, initial_mntns_fd = -1; - const char *etc_machine_id; + const char *etc_machine_id, *sync_path; sd_id128_t id; int r; + /* Before doing anything, sync everything to ensure any changes by first-boot units are persisted. + * + * First, explicitly sync the file systems we care about and check if it worked. */ + FOREACH_STRING(sync_path, "/etc/", "/var/") { + r = syncfs_path(AT_FDCWD, sync_path); + if (r < 0) + return log_error_errno(r, "Cannot sync %s: %m", sync_path); + } + + /* Afterwards, sync() the rest too, but we can't check the return value for these. */ + sync(); + /* Replaces a tmpfs bind mount of /etc/machine-id by a proper file, atomically. For this, the umount is removed * in a mount namespace, a new file is created at the right place. Afterwards the mount is also removed in the * original mount namespace, thus revealing the file that was just created. */ diff --git a/src/core/machine-id-setup.h b/src/core/machine-id-setup.h index d6ac62a882..e207ccf9c1 100644 --- a/src/core/machine-id-setup.h +++ b/src/core/machine-id-setup.h @@ -1,5 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #pragma once +#include + int machine_id_commit(const char *root); -int machine_id_setup(const char *root, sd_id128_t requested, sd_id128_t *ret); +int machine_id_setup(const char *root, bool force_transient, sd_id128_t requested, sd_id128_t *ret); diff --git a/src/core/main.c b/src/core/main.c index 6cd596fa79..728e1578b1 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -2001,15 +2001,26 @@ static void log_execution_mode(bool *ret_first_boot) { *ret_first_boot = false; log_info("Running in initial RAM disk."); } else { - /* Let's check whether we are in first boot, i.e. whether /etc is still unpopulated. We use - * /etc/machine-id as flag file, for this: if it exists we assume /etc is populated, if it - * doesn't it's unpopulated. 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. */ + int r; + _cleanup_free_ char *id_text = NULL; - *ret_first_boot = access("/etc/machine-id", F_OK) < 0; - if (*ret_first_boot) - log_info("Running with unpopulated /etc."); + /* 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. */ + + 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"); + + *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."); + } } } else { if (DEBUG_LOGGING) { @@ -2026,6 +2037,7 @@ static void log_execution_mode(bool *ret_first_boot) { static int initialize_runtime( bool skip_setup, + bool first_boot, struct rlimit *saved_rlimit_nofile, struct rlimit *saved_rlimit_memlock, const char **ret_error_message) { @@ -2059,7 +2071,8 @@ static int initialize_runtime( status_welcome(); hostname_setup(); - machine_id_setup(NULL, arg_machine_id, NULL); + /* Force transient machine-id on first boot. */ + machine_id_setup(NULL, first_boot, arg_machine_id, NULL); (void) loopback_setup(); bump_unix_max_dgram_qlen(); bump_file_max_and_nr_open(); @@ -2787,6 +2800,7 @@ int main(int argc, char *argv[]) { log_execution_mode(&first_boot); r = initialize_runtime(skip_setup, + first_boot, &saved_rlimit_nofile, &saved_rlimit_memlock, &error_message); diff --git a/src/libsystemd/sd-id128/id128-util.c b/src/libsystemd/sd-id128/id128-util.c index 335f22b920..ebbfb2d32e 100644 --- a/src/libsystemd/sd-id128/id128-util.c +++ b/src/libsystemd/sd-id128/id128-util.c @@ -10,6 +10,7 @@ #include "id128-util.h" #include "io-util.h" #include "stdio-util.h" +#include "string-util.h" char *id128_to_uuid_string(sd_id128_t id, char s[static ID128_UUID_STRING_MAX]) { unsigned n, k = 0; @@ -97,6 +98,11 @@ int id128_read_fd(int fd, Id128Format f, sd_id128_t *ret) { switch (l) { + case 13: + case 14: + /* Treat an "uninitialized" id file like an empty one */ + return f == ID128_PLAIN_OR_UNINIT && strneq(buffer, "uninitialized\n", l) ? -ENOMEDIUM : -EINVAL; + case 33: /* plain UUID with trailing newline */ if (buffer[32] != '\n') return -EINVAL; @@ -115,7 +121,7 @@ int id128_read_fd(int fd, Id128Format f, sd_id128_t *ret) { _fallthrough_; case 36: /* RFC UUID without trailing newline */ - if (f == ID128_PLAIN) + if (IN_SET(f, ID128_PLAIN, ID128_PLAIN_OR_UNINIT)) return -EINVAL; buffer[36] = 0; diff --git a/src/libsystemd/sd-id128/id128-util.h b/src/libsystemd/sd-id128/id128-util.h index 1901bf119f..1453c00f2f 100644 --- a/src/libsystemd/sd-id128/id128-util.h +++ b/src/libsystemd/sd-id128/id128-util.h @@ -17,6 +17,10 @@ bool id128_is_valid(const char *s) _pure_; typedef enum Id128Format { ID128_ANY, ID128_PLAIN, /* formatted as 32 hex chars as-is */ + ID128_PLAIN_OR_UNINIT, /* formatted as 32 hex chars as-is; allow special "uninitialized" + * value when reading from file (id128_read() and id128_read_fd()). + * + * This format should be used when reading a machine-id file. */ ID128_UUID, /* formatted as 36 character uuid string */ _ID128_FORMAT_MAX, } Id128Format; diff --git a/src/machine-id-setup/machine-id-setup-main.c b/src/machine-id-setup/machine-id-setup-main.c index 872b00c158..2bcd99109e 100644 --- a/src/machine-id-setup/machine-id-setup-main.c +++ b/src/machine-id-setup/machine-id-setup-main.c @@ -128,7 +128,7 @@ static int run(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to read machine ID back: %m"); } else { - r = machine_id_setup(arg_root, SD_ID128_NULL, &id); + r = machine_id_setup(arg_root, false, SD_ID128_NULL, &id); if (r < 0) return r; } diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 82bb167397..5db08cb5b3 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -2726,7 +2726,7 @@ static int setup_machine_id(const char *directory) { etc_machine_id = prefix_roota(directory, "/etc/machine-id"); - r = id128_read(etc_machine_id, ID128_PLAIN, &id); + r = id128_read(etc_machine_id, ID128_PLAIN_OR_UNINIT, &id); if (r < 0) { if (!IN_SET(r, -ENOENT, -ENOMEDIUM)) /* If the file is missing or empty, we don't mind */ return log_error_errno(r, "Failed to read machine ID from container image: %m"); diff --git a/src/partition/repart.c b/src/partition/repart.c index d406c9dbe3..4cf6a5fe3a 100644 --- a/src/partition/repart.c +++ b/src/partition/repart.c @@ -3245,7 +3245,7 @@ static int context_read_seed(Context *context, const char *root) { else if (fd < 0) return log_error_errno(fd, "Failed to determine machine ID of image: %m"); else { - r = id128_read_fd(fd, ID128_PLAIN, &context->seed); + r = id128_read_fd(fd, ID128_PLAIN_OR_UNINIT, &context->seed); if (r == -ENOMEDIUM) log_info("No machine ID set, using randomized partition UUIDs."); else if (r < 0) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 91120d7219..cbd3357219 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -2148,6 +2148,8 @@ int dissected_image_acquire_metadata(DissectedImage *m) { log_debug_errno(r, "Image contains invalid /etc/machine-id: %s", line); } else if (r == 0) log_debug("/etc/machine-id file is empty."); + else if (streq(line, "uninitialized")) + log_debug("/etc/machine-id file is uninitialized (likely aborted first boot)."); else log_debug("/etc/machine-id has unexpected length %i.", r); diff --git a/units/first-boot-complete.target b/units/first-boot-complete.target new file mode 100644 index 0000000000..98b236e9ff --- /dev/null +++ b/units/first-boot-complete.target @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: LGPL-2.1+ +# +# 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=First Boot Complete +Documentation=man:systemd.special(7) +RefuseManualStart=yes +ConditionFirstBoot=yes diff --git a/units/meson.build b/units/meson.build index e94e7f7efd..40ffca5058 100644 --- a/units/meson.build +++ b/units/meson.build @@ -17,6 +17,7 @@ units = [ ['emergency.target', ''], ['exit.target', ''], ['final.target', ''], + ['first-boot-complete.target', ''], ['getty.target', '', 'multi-user.target.wants/'], ['getty-pre.target', ''], diff --git a/units/systemd-firstboot.service b/units/systemd-firstboot.service index 9f5c7101cd..e17fef7b36 100644 --- a/units/systemd-firstboot.service +++ b/units/systemd-firstboot.service @@ -13,7 +13,8 @@ Documentation=man:systemd-firstboot(1) DefaultDependencies=no Conflicts=shutdown.target After=systemd-remount-fs.service -Before=systemd-sysusers.service sysinit.target shutdown.target +Before=systemd-sysusers.service sysinit.target first-boot-complete.target shutdown.target +Wants=first-boot-complete.target ConditionPathIsReadWrite=/etc ConditionFirstBoot=yes diff --git a/units/systemd-machine-id-commit.service b/units/systemd-machine-id-commit.service index e3acb0f326..5616a35701 100644 --- a/units/systemd-machine-id-commit.service +++ b/units/systemd-machine-id-commit.service @@ -12,8 +12,8 @@ Description=Commit a transient machine-id on disk Documentation=man:systemd-machine-id-commit.service(8) DefaultDependencies=no Conflicts=shutdown.target -Before=sysinit.target shutdown.target -After=local-fs.target +Before=shutdown.target +After=local-fs.target first-boot-complete.target ConditionPathIsReadWrite=/etc ConditionPathIsMountPoint=/etc/machine-id diff --git a/units/systemd-random-seed.service.in b/units/systemd-random-seed.service.in index f97f99a966..aa4b3e7357 100644 --- a/units/systemd-random-seed.service.in +++ b/units/systemd-random-seed.service.in @@ -14,7 +14,8 @@ DefaultDependencies=no RequiresMountsFor=@RANDOM_SEED@ Conflicts=shutdown.target After=systemd-remount-fs.service -Before=shutdown.target +Before=first-boot-complete.target shutdown.target +Wants=first-boot-complete.target ConditionVirtualization=!container [Service]