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.
This commit is contained in:
Zbigniew Jędrzejewski-Szmek 2022-09-30 12:50:40 +02:00
parent eb650ffedf
commit 7cd43e34c5
4 changed files with 58 additions and 43 deletions

View file

@ -119,20 +119,26 @@
<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
<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>uninitialized\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>The kernel command argument <varname>systemd.condition-first-boot=</varname> may be
used to override the autodetection logic, see
<citerefentry><refentrytitle>kernel-command-line</refentrytitle><manvolnum>7</manvolnum></citerefentry>.
</para></listitem>
<listitem><para>Otherwise, if <filename>/etc/machine-id</filename> does not exist, this is a first
boot. During early boot, <command>systemd</command> will write <literal>uninitialized\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>
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
<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>
@ -140,8 +146,8 @@
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>
<para>If according to the above rules a first boot is detected, units with
<varname>ConditionFirstBoot=yes</varname> will be run.</para>
</refsect1>
<refsect1>

View file

@ -1455,15 +1455,18 @@
<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 for the first time. This roughly means that <filename>/etc/</filename>
is unpopulated (for details, see "First Boot Semantics" in
whether the system is booting up for the first time. This roughly means that <filename>/etc/</filename>
was unpopulated when the system started booting (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>
First boot is considered finished (this condition will evaluate as false) after the manager
has finished the startup phase.</para>
<para>This condition 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
<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

View file

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

View file

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