<literal>?</literal> characters and separators) stays within this limit. An expanded name that exceeds
the limit is considered invalid and the built-in fallback hostname is used.</para>
+ <para>Because the name only stays stable as long as the pattern and word lists are unchanged, the
+ preferred way to obtain a stable, generated hostname is to provide the pattern through the
+ <varname>firstboot.hostname</varname> credential (see
+ <citerefentry><refentrytitle>systemd.system-credentials</refentrytitle><manvolnum>7</manvolnum></citerefentry>)
+ rather than placing it directly in this file. Concretely, write the pattern to
+ <filename>/etc/credstore/firstboot.hostname</filename> (see the description of
+ <varname>LoadCredential=</varname> in
+ <citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry> for the
+ credential store): when firstboot runs on the live system the credential is resolved once and the
+ concrete result is persisted here, so the word lists may be updated afterwards without changing the
+ hostname. A pattern placed directly in <filename>/etc/hostname</filename> (or provisioned into an offline
+ image) is instead re-evaluated on every boot, which only really makes sense for systems that keep the
+ wordlist and the pattern stable.</para>
+
<xi:include href="version-info.xml" xpointer="v262"/>
<para>You may use
hostname, and only has an effect on first boot, unlike <varname>system.hostname</varname> (see
below). Read by
<citerefentry><refentrytitle>systemd-firstboot</refentrytitle><manvolnum>1</manvolnum></citerefentry>
- and only honoured if no static hostname has been configured before.</para>
+ and only honoured if no static hostname has been configured before. The value may use the wildcard
+ patterns documented in
+ <citerefentry><refentrytitle>hostname</refentrytitle><manvolnum>5</manvolnum></citerefentry> (e.g.
+ <literal>$-$</literal> or <literal>foo-????</literal>). When the credential is applied on the running
+ system (the usual case, by <filename>systemd-firstboot.service</filename> on first boot), the wildcards
+ are resolved against the machine ID and the resulting concrete name is written to
+ <filename>/etc/hostname</filename>. The name is thus <emphasis>persisted</emphasis> once at first boot and
+ does not change on subsequent boots even if the word lists are later updated or the pattern is changed
+ (for example to add more <literal>?</literal>/<literal>$</literal> tokens for additional entropy), which is the recommended
+ way to obtain a stable, per-machine generated hostname. (When
+ <citerefentry><refentrytitle>systemd-firstboot</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+ operates on an offline image via <option>--root=</option>/<option>--image=</option>, the target's
+ machine ID is not yet known, so the pattern is written verbatim and, like a pattern placed directly in
+ <filename>/etc/hostname</filename>, re-derived on every boot rather than persisted.)</para>
<xi:include href="version-info.xml" xpointer="v261"/>
</listitem>
in <filename>/etc/hostname</filename>, if configured, takes precedence over this setting.
Interpreted by the service manager (PID 1). For details see
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>. Also
- see <varname>firstboot.hostname</varname> above.</para>
+ see <varname>firstboot.hostname</varname> above. The value may use the wildcard patterns documented
+ in <citerefentry><refentrytitle>hostname</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+ (<literal>?</literal> and <literal>$</literal>), which are expanded deterministically from the
+ machine ID when the credential is applied. As this is a transient hostname that is re-applied on
+ every boot, a wildcard value is re-derived each boot (and may change if the word lists change) and
+ is never persisted; pass an already-resolved name if a stable hostname is required. Note also that
+ the word-list tokens require the word lists to be present, which is generally not the case in the
+ initrd so during initrd the default hostname is used and in late boot the resolved one becomes
+ available.</para>
<xi:include href="version-info.xml" xpointer="v254"/>
</listitem>
#include "fs-util.h"
#include "glyph-util.h"
#include "help-util.h"
+#include "hostname-setup.h"
#include "hostname-util.h"
#include "image-policy.h"
#include "kbd-util.h"
r = read_credential("firstboot.hostname", (void**) &hn, NULL);
if (r < 0)
log_debug_errno(r, "Failed to read credential firstboot.hostname, ignoring: %m");
- else if (!hostname_is_valid(hn, VALID_HOSTNAME_TRAILING_DOT|VALID_HOSTNAME_QUESTION_MARK))
+ else if (!hostname_is_valid(hn, VALID_HOSTNAME_TRAILING_DOT|VALID_HOSTNAME_QUESTION_MARK|VALID_HOSTNAME_WORD_TOKEN))
log_warning_errno(SYNTHETIC_ERRNO(EINVAL), "Hostname '%s' supplied via credential is not valid, ignoring.", hn);
else {
log_debug("Acquired hostname from credentials.");
if (isempty(arg_hostname))
return 0;
- r = write_string_file_at(pfd, f, arg_hostname,
+ /* On running systems we have a machine ID, so resolve any '?'/'$' wildcards now and persist them.
+ * This "freezes" the name, so later word list updates do not change it. When operating on an offline
+ * image (--root=/--image=) the target's machine ID is not known yet, so write the template verbatim
+ * and let it be resolved on each first boot. */
+ const char *hostname = arg_hostname;
+ _cleanup_free_ char *resolved = NULL;
+ if (!arg_root) {
+ r = hostname_substitute_wildcards(arg_hostname, &resolved);
+ if (r < 0)
+ log_warning_errno(r, "Failed to resolve wildcards in hostname '%s', writing it verbatim: %m", arg_hostname);
+ else if (!hostname_is_valid(resolved, VALID_HOSTNAME_TRAILING_DOT))
+ log_warning("Resolved hostname '%s' is invalid, writing template '%s' verbatim instead.", resolved, arg_hostname);
+ else
+ hostname = resolved;
+ }
+
+ r = write_string_file_at(pfd, f, hostname,
WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_SYNC|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_LABEL);
if (r < 0)
return log_error_errno(r, "Failed to write /etc/hostname: %m");
break;
OPTION_LONG("hostname", "NAME", "Set hostname"):
- if (!hostname_is_valid(opts.arg, VALID_HOSTNAME_TRAILING_DOT|VALID_HOSTNAME_QUESTION_MARK))
+ if (!hostname_is_valid(opts.arg, VALID_HOSTNAME_TRAILING_DOT|VALID_HOSTNAME_QUESTION_MARK|VALID_HOSTNAME_WORD_TOKEN))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Host name %s is not valid.", opts.arg);
if (r == 0) /* not found */
return -ENXIO;
- if (!hostname_is_valid(cred, VALID_HOSTNAME_TRAILING_DOT)) /* check that the hostname we return is valid */
+ if (!hostname_is_valid(cred, VALID_HOSTNAME_TRAILING_DOT|VALID_HOSTNAME_QUESTION_MARK|VALID_HOSTNAME_WORD_TOKEN))
return log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Hostname specified in system.hostname credential is invalid, ignoring: %s", cred);
+ _cleanup_free_ char *substituted = NULL;
+ r = hostname_substitute_wildcards(cred, &substituted);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to substitute wildcards in system.hostname credential, ignoring: %m");
+
+ if (!hostname_is_valid(substituted, VALID_HOSTNAME_TRAILING_DOT)) /* check that the expanded hostname is valid */
+ return log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Hostname from system.hostname credential is invalid after expansion, ignoring: %s", substituted);
+
log_info("Initializing hostname from credential.");
- *ret = TAKE_PTR(cred);
+ *ret = TAKE_PTR(substituted);
return 0;
}
exit 0
fi
+restore_etc_hostname() {
+ if [[ -e /tmp/etc-hostname.bak ]]; then
+ mv /tmp/etc-hostname.bak /etc/hostname
+ elif [[ -e /tmp/etc-hostname.absent ]]; then
+ rm -f /etc/hostname /tmp/etc-hostname.absent
+ fi
+}
+
at_exit() {
if [[ -n "${ROOT:-}" ]]; then
ls -lR "$ROOT"
rm -rf /etc/otherpath
fi
+ [[ -n "${WROOT:-}" ]] && rm -rf "$WROOT"
+ [[ -n "${WCREDS:-}" ]] && rm -rf "$WCREDS"
+ [[ -n "${WWORDS:-}" ]] && rm -rf "$WWORDS"
+ restore_etc_hostname
+
restore_locale
}
systemd-firstboot --root="$ROOT" --hostname "foobar"
grep -q "foobar" "$ROOT/etc/hostname"
+# Hostname wildcard templates (see hostname(5)) must be accepted by both the --hostname option and the
+# firstboot.hostname credential (both go through hostname_is_valid() with the wildcard flags). Since these
+# invocations operate on an offline image (--root=), the template is written verbatim and only resolved
+# later, on the target's first boot (the resolve-and-freeze path is taken only when running on the live
+# system, where the machine ID is final).
+WROOT=test-root-hostname-wildcard
+
+# '$' word token via --hostname, stored as-is
+rm -rf "$WROOT"; mkdir -p "$WROOT"
+systemd-firstboot --root="$WROOT" --hostname='$-$'
+assert_eq "$(cat "$WROOT/etc/hostname")" '$-$'
+
+# '?' hex token via --hostname, stored as-is
+rm -rf "$WROOT"; mkdir -p "$WROOT"
+systemd-firstboot --root="$WROOT" --hostname='foo-????'
+assert_eq "$(cat "$WROOT/etc/hostname")" 'foo-????'
+
+# the firstboot.hostname credential is accepted and written verbatim too
+rm -rf "$WROOT"; mkdir -p "$WROOT"
+WCREDS="$(mktemp -d)"
+echo -n '$-$-????' >"$WCREDS/firstboot.hostname"
+CREDENTIALS_DIRECTORY="$WCREDS" systemd-firstboot --root="$WROOT"
+assert_eq "$(cat "$WROOT/etc/hostname")" '$-$-????'
+rm -rf "$WCREDS"
+
+# an invalid hostname (disallowed character) is refused and nothing is written
+rm -rf "$WROOT"; mkdir -p "$WROOT"
+(! systemd-firstboot --root="$WROOT" --hostname='foo_bar')
+[[ ! -e "$WROOT/etc/hostname" ]]
+
+rm -rf "$WROOT"
+
+# When run on the live system (no --root=) the machine ID is final, so the wildcards are resolved
+# immediately and the concrete result is persisted, "freezing" the generated name (see hostname(5)).
+if [[ -f /etc/hostname ]]; then
+ cp /etc/hostname /tmp/etc-hostname.bak
+else
+ touch /tmp/etc-hostname.absent
+fi
+WWORDS="$(mktemp -d)"
+printf 'wildly\nquietly\n' >"$WWORDS/1"
+printf 'happy\nsad\n' >"$WWORDS/2"
+SYSTEMD_HOSTNAME_WORDLIST_PATH="$WWORDS" systemd-firstboot --force --hostname='$-$-????'
+H="$(cat /etc/hostname)"
+IFS='-' read -r w1 w2 suffix <<<"$H"
+grep -Fx -- "$w1" "$WWORDS/1" >/dev/null
+grep -Fx -- "$w2" "$WWORDS/2" >/dev/null
+[[ "$suffix" =~ ^[0-9a-f]{4}$ ]]
+rm -rf "$WWORDS"
+restore_etc_hostname
+
systemd-firstboot --root="$ROOT" --machine-id=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
grep -q "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "$ROOT/etc/machine-id"