Merge pull request #16939 from Rahix/robust-first-boot-machine-id

Make ConditionFirstBoot safe against power failures
This commit is contained in:
Yu Watanabe 2020-10-20 14:01:41 +09:00 committed by GitHub
commit 0ce8a9d6e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 156 additions and 32 deletions

View File

@ -82,10 +82,11 @@
<para>For operating system images which are created once and used on multiple
machines, for example for containers or in the cloud,
<filename>/etc/machine-id</filename> 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.</para>
<filename>/etc/machine-id</filename> 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.</para>
<para><citerefentry><refentrytitle>systemd-firstboot</refentrytitle><manvolnum>1</manvolnum></citerefentry>
may be used to initialize <filename>/etc/machine-id</filename> on mounted (but not
@ -115,6 +116,34 @@
early boot but become writable later on.</para>
</refsect1>
<refsect1>
<title>First Boot Semantics</title>
<para><filename>/etc/machine-id</filename> is used to decide whether a boot is the first one. The rules
are as follows:</para>
<orderedlist>
<listitem><para>If <filename>/etc/machine-id</filename> does not exist, this is a first boot. During
early boot, <command>systemd</command> will write <literal>unitialized\n</literal> to this file and overmount
a temporary file which contains the actual machine ID. Later (after <filename>first-boot-complete.target</filename>
has been reached), the real machine ID will be written to disk.</para></listitem>
<listitem><para>If <filename>/etc/machine-id</filename> contains the string <literal>uninitialized</literal>,
a boot is also considered the first boot. The same mechanism as above applies.</para></listitem>
<listitem><para>If <filename>/etc/machine-id</filename> exists and is empty, a boot is
<emphasis>not</emphasis> considered the first boot. <command>systemd</command> will still bind-mount a file
containing the actual machine-id over it and later try to commit it to disk (if <filename>/etc/</filename> is
writable).</para></listitem>
<listitem><para>If <filename>/etc/machine-id</filename> already contains a valid machine-id, this is
not a first boot.</para></listitem>
</orderedlist>
<para>If by any of the above rules, a first boot is detected, units with <varname>ConditionFirstBoot=yes</varname>
will be run.</para>
</refsect1>
<refsect1>
<title>Relation to OSF UUIDs</title>

View File

@ -32,6 +32,7 @@
<filename>emergency.target</filename>,
<filename>exit.target</filename>,
<filename>final.target</filename>,
<filename>first-boot-complete.target</filename>,
<filename>getty.target</filename>,
<filename>getty-pre.target</filename>,
<filename>graphical.target</filename>,
@ -878,6 +879,17 @@
stopped.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><filename>first-boot-complete.target</filename></term>
<listitem>
<para>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
<citerefentry><refentrytitle>machine-id</refentrytitle><manvolnum>5</manvolnum></citerefentry>
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 <varname>ConditionFirstBoot=yes</varname>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><filename>getty-pre.target</filename></term>
<listitem>

View File

@ -1315,10 +1315,16 @@
<term><varname>ConditionFirstBoot=</varname></term>
<listitem><para>Takes a boolean argument. This condition may be used to conditionalize units on
whether the system is booting up with an unpopulated <filename>/etc/</filename> directory
(specifically: an <filename>/etc/</filename> with no <filename>/etc/machine-id</filename>). This may
be used to populate <filename>/etc/</filename> on the first boot after factory reset, or when a new
system instance boots up for the first time.</para>
whether the system is booting up for the first time. This roughly means that <filename>/etc/</filename>
is unpopulated (for details, see "First Boot Semantics" in
<citerefentry><refentrytitle>machine-id</refentrytitle><manvolnum>5</manvolnum></citerefentry>).
This may be used to populate <filename>/etc/</filename> on the first boot after factory reset, or
when a new system instance boots up for the first time.</para>
<para>For robustness, units with <varname>ConditionFirstBoot=yes</varname> should order themselves
before <filename>first-boot-complete.target</filename> and pull in this passive target with
<varname>Wants=</varname>. This ensures that in a case of an aborted first boot, these units will
be re-run during the next system startup.</para>
<para>If the <varname>systemd.condition-first-boot=</varname> option is specified on the kernel
command line (taking a boolean), it will override the result of this condition check, taking

View File

@ -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

View File

@ -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. */

View File

@ -1,5 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include <stdbool.h>
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);

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -17,6 +17,7 @@ units = [
['emergency.target', ''],
['exit.target', ''],
['final.target', ''],
['first-boot-complete.target', ''],
['getty.target', '',
'multi-user.target.wants/'],
['getty-pre.target', ''],

View File

@ -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

View File

@ -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

View File

@ -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]