mirror of
https://github.com/systemd/systemd.git
synced 2025-01-10 05:18:17 +03:00
Merge pull request #16939 from Rahix/robust-first-boot-machine-id
Make ConditionFirstBoot safe against power failures
This commit is contained in:
commit
0ce8a9d6e5
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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. */
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
||||
|
14
units/first-boot-complete.target
Normal file
14
units/first-boot-complete.target
Normal 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
|
@ -17,6 +17,7 @@ units = [
|
||||
['emergency.target', ''],
|
||||
['exit.target', ''],
|
||||
['final.target', ''],
|
||||
['first-boot-complete.target', ''],
|
||||
['getty.target', '',
|
||||
'multi-user.target.wants/'],
|
||||
['getty-pre.target', ''],
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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]
|
||||
|
Loading…
Reference in New Issue
Block a user