hostnamed,pid1,firstboot: introduce "machine tags" concept (#42223)

Fixes: #38591
This commit is contained in:
Luca Boccassi
2026-05-21 20:57:34 +01:00
committed by GitHub
21 changed files with 815 additions and 57 deletions
-14
View File
@@ -159,8 +159,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- sysupdate: add concept for enabling/disabling specific components explicitly,
just like features.
- machine-info: add a TAGS concept that can be used to categorize a machine
- udev: add a MACHINE_TAGS field, that augments /etc/machine-info configured
tags.
@@ -180,13 +178,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- pcrextend: we should measure something when we enter developer mode, by some
definition of developer mode.
- /etc/machine-info should have a concept of a "role" that we can put a machine
into, which can be consumed by sysupdate and similar. A role should be
something we can set once (i.e. the initial setting should be protected by
polkit and be somewhat loosely access control, and later settings should use a
different/tougher polkit authorization, so that people can implement a
no-way-back mechanism)
- firstboot: optionally accept credentials at firstboot without authentication
- firstboot/sysinstall: add simple interface for prompting users to enable
@@ -788,11 +779,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- sysext: make systemd-{sys,conf}ext-sysroot.service work in the split `/var`
configuration.
- introduce a concept of /etc/machine-info "TAGS=" field that allows tagging
machines with zero, one or more roles, states or other forms of
categorization. Then, add a way of using this in sysupdate to automatically
enable certain transfers, one for each role.
- sd-varlink: add fully async modes of the protocol upgrade stuff
- repart: maybe remove iso9660/eltorito superblock from disk when booting via
+26
View File
@@ -165,6 +165,32 @@
<xi:include href="version-info.xml" xpointer="v249"/>
</listitem>
</varlistentry>
<varlistentry>
<term><command>tags</command> [<replaceable>TAG</replaceable>…]</term>
<listitem><para>If no argument is given, print the machine tags currently configured, as a
colon-separated list. If one or more <replaceable>TAG</replaceable> arguments are provided then the
command replaces the configured tags with the specified ones. Each argument may itself be a
colon-separated list of tags, so that the tags may be specified either as multiple arguments or as a
single colon-separated argument, or any combination thereof. Duplicate tags are removed and the
resulting list is sorted before being stored. To remove all tags, invoke the command with a single
empty string argument.</para>
<para>Machine tags are short labels that may be used to classify and group machines for management
purposes, for example to identify the role a machine plays in a deployment, the fleet or
organizational unit it belongs to, or any other administrator-defined attribute. Each individual tag
must be 1…255 characters long and consist only of ASCII alphanumeric characters,
<literal>-</literal> and <literal>.</literal>. The tags are stored in the <varname>TAGS=</varname>
field of <filename>/etc/machine-info</filename>; see
<citerefentry><refentrytitle>machine-info</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
details. They may also be matched against with the
<varname>ConditionMachineTag=</varname>/<varname>AssertMachineTag=</varname> unit settings, see
<citerefentry><refentrytitle>systemd.unit</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para>
<xi:include href="version-info.xml" xpointer="v261"/>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
+23 -1
View File
@@ -139,6 +139,27 @@
<xi:include href="version-info.xml" xpointer="v216"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>TAGS=</varname></term>
<listitem><para>A colon-separated list of tags attached to this machine. Tags are short labels that
may be used to classify and group machines for management purposes, for example to identify the role
a machine plays in a deployment (<literal>webserver</literal>, <literal>database</literal>), the
fleet or organizational unit it belongs to, or any other administrator-defined attribute. Example:
<literal>TAGS=webserver:frontend:berlin</literal>.</para>
<para>Each individual tag must be 1…255 characters long and may consist only of the ASCII
alphanumeric characters, <literal>-</literal> and <literal>.</literal>.</para>
<para>The configured tags may be matched against with the
<varname>ConditionMachineTag=</varname> and <varname>AssertMachineTag=</varname> unit settings, see
<citerefentry><refentrytitle>systemd.unit</refentrytitle><manvolnum>5</manvolnum></citerefentry>
for details. They may be queried and changed with the <command>tags</command> command of
<citerefentry><refentrytitle>hostnamectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>HARDWARE_VENDOR=</varname></term>
@@ -195,7 +216,8 @@
<programlisting>PRETTY_HOSTNAME="Lennart's Tablet"
ICON_NAME=computer-tablet
CHASSIS=tablet
DEPLOYMENT=production</programlisting>
DEPLOYMENT=production
TAGS=demo:berlin</programlisting>
</refsect1>
<refsect1>
+25 -4
View File
@@ -56,6 +56,7 @@ node /org/freedesktop/hostname1 {
in b interactive);
SetLocation(in s location,
in b interactive);
SetTags(in as tags);
GetProductUUID(in b interactive,
out ay uuid);
GetHardwareSerial(out s serial);
@@ -73,6 +74,7 @@ node /org/freedesktop/hostname1 {
readonly s Chassis = '...';
readonly s Deployment = '...';
readonly s Location = '...';
readonly as Tags = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s KernelName = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -142,6 +144,8 @@ node /org/freedesktop/hostname1 {
<variablelist class="dbus-method" generated="True" extra-ref="SetLocation()"/>
<variablelist class="dbus-method" generated="True" extra-ref="SetTags()"/>
<variablelist class="dbus-method" generated="True" extra-ref="GetProductUUID()"/>
<variablelist class="dbus-method" generated="True" extra-ref="GetHardwareSerial()"/>
@@ -168,6 +172,8 @@ node /org/freedesktop/hostname1 {
<variablelist class="dbus-property" generated="True" extra-ref="Location"/>
<variablelist class="dbus-property" generated="True" extra-ref="Tags"/>
<variablelist class="dbus-property" generated="True" extra-ref="KernelName"/>
<variablelist class="dbus-property" generated="True" extra-ref="KernelRelease"/>
@@ -268,6 +274,15 @@ node /org/freedesktop/hostname1 {
configure the chassis type if it could not be auto-detected. Set this property to the empty string to
reenable the automatic detection of the chassis type from firmware information.</para>
<para>The <varname>Tags</varname> property exposes a list of <emphasis>machine tags</emphasis>: short
labels that may be used to classify and group machines for management purposes, for example to identify
the role a machine plays in a deployment, the fleet or organizational unit it belongs to, or any other
administrator-defined attribute. Each individual tag is 1…255 characters long and consists only of the
ASCII alphanumeric characters, <literal>-</literal> and <literal>.</literal>. The tags are stored as a
colon-separated list in the <varname>TAGS=</varname> field of <filename>/etc/machine-info</filename>, see
<citerefentry><refentrytitle>machine-info</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
details. If no tags are configured this property is the empty list.</para>
<para>Note that <filename>systemd-hostnamed</filename> starts only on request and terminates after a
short idle period. This effectively means that <function>PropertyChanged</function> messages are not sent
out for changes made directly on the files (as in: administrator edits the files with vi). This is
@@ -365,8 +380,13 @@ node /org/freedesktop/hostname1 {
deployment environment), and <varname>Location</varname> (physical system location), respectively.
</para>
<para><function>SetTags()</function> sets the machine tags exposed by the <varname>Tags</varname>
property. It takes a list of strings, each of which must be a valid machine tag (1…255 ASCII
alphanumeric characters, <literal>-</literal> and <literal>.</literal>). Passing an empty list removes
the <varname>TAGS=</varname> field from <filename>/etc/machine-info</filename>.</para>
<para><varname>PrettyHostname</varname>, <varname>IconName</varname>, <varname>Chassis</varname>,
<varname>Deployment</varname>, and <varname>Location</varname> are stored in
<varname>Deployment</varname>, <varname>Location</varname>, and <varname>Tags</varname> are stored in
<filename>/etc/machine-info</filename>. See
<citerefentry><refentrytitle>machine-info</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
the semantics of those settings.</para>
@@ -400,8 +420,8 @@ node /org/freedesktop/hostname1 {
<interfacename>org.freedesktop.hostname1.set-hostname</interfacename>. For
<function>SetStaticHostname()</function> and <function>SetPrettyHostname()</function> it is
<interfacename>org.freedesktop.hostname1.set-static-hostname</interfacename>. For
<function>SetIconName()</function>, <function>SetChassis()</function>, <function>SetDeployment()</function>
and <function>SetLocation()</function> it is
<function>SetIconName()</function>, <function>SetChassis()</function>, <function>SetDeployment()</function>,
<function>SetLocation()</function> and <function>SetTags()</function> it is
<interfacename>org.freedesktop.hostname1.set-machine-info</interfacename>.</para>
</refsect2>
</refsect1>
@@ -505,7 +525,8 @@ node /org/freedesktop/hostname1 {
<varname>OperatingSystemImageVersion</varname>, <varname>HardwareSKU</varname>, and
<varname>HardwareVersion</varname> were added in version 258.</para>
<para><varname>OperatingSystemFancyName</varname> was added in version 260.</para>
<para><function>GetMachineInfo()</function> was added in version 261.</para>
<para><function>GetMachineInfo()</function>, <varname>Tags</varname> and
<function>SetTags()</function> were added in version 261.</para>
</refsect2>
</refsect1>
+35
View File
@@ -67,6 +67,8 @@
<listitem><para>The system hostname</para></listitem>
<listitem><para>The machine tags</para></listitem>
<listitem><para>The kernel command line used when installing kernel images</para></listitem>
<listitem><para>The root user's password and shell</para></listitem>
@@ -198,6 +200,27 @@
<xi:include href="version-info.xml" xpointer="v216"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--machine-tags=<replaceable>TAG</replaceable><optional>:<replaceable>TAG</replaceable>…</optional></option></term>
<listitem><para>Set the machine tags to the specified colon-separated list. Machine tags are short
labels that may be used to classify and group machines for management purposes, for example to
identify the role a machine plays in a deployment, the fleet or organizational unit it belongs to, or
any other administrator-defined attribute. Each individual tag must be 1…255 characters long and
consist only of ASCII alphanumeric characters, <literal>-</literal> and <literal>.</literal>. This
controls the <varname>TAGS=</varname> field of
<citerefentry><refentrytitle>machine-info</refentrytitle><manvolnum>5</manvolnum></citerefentry>. The
tags may later be queried and changed with the <command>tags</command> command of
<citerefentry><refentrytitle>hostnamectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>, and
matched against with the <varname>ConditionMachineTag=</varname>/<varname>AssertMachineTag=</varname>
unit settings, see
<citerefentry><refentrytitle>systemd.unit</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para>
<para>Unlike most other settings, machine tags are not prompted for interactively.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--root-password=<replaceable>PASSWORD</replaceable></option></term>
<term><option>--root-password-file=<replaceable>PATH</replaceable></option></term>
@@ -458,6 +481,18 @@
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>firstboot.machine-tags</varname></term>
<listitem><para>This credential specifies the machine tags to set during first boot, as a
colon-separated list, equivalent to the <option>--machine-tags=</option> switch described above. The
tags are written into the <varname>TAGS=</varname> field of <filename>/etc/machine-info</filename>
(if that file is not already present), and only have an effect on first boot. If the list contains
invalid tags it is ignored in its entirety.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
<para>Note that by default the <filename>systemd-firstboot.service</filename> unit file is set up to
+15
View File
@@ -91,6 +91,21 @@
</listitem>
</varlistentry>
<varlistentry>
<term><varname>firstboot.machine-tags</varname></term>
<listitem>
<para>The machine tags to set, as a colon-separated list (e.g.
<literal>webserver:frontend:berlin</literal>). Read by
<citerefentry><refentrytitle>systemd-firstboot</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
and only honoured if <filename>/etc/machine-info</filename> has not been configured before. Written
into the <varname>TAGS=</varname> field of that file, see
<citerefentry><refentrytitle>machine-info</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
details.</para>
<xi:include href="version-info.xml" xpointer="v261"/>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>login.issue</varname></term>
<listitem>
+20
View File
@@ -2058,6 +2058,25 @@
</listitem>
</varlistentry>
<varlistentry>
<term><varname>ConditionMachineTag=</varname></term>
<listitem><para><varname>ConditionMachineTag=</varname> may be used to match against the tags
assigned to the local machine. Machine tags are short labels that classify and group machines for
management purposes; they are configured in the <varname>TAGS=</varname> field of
<citerefentry><refentrytitle>machine-info</refentrytitle><manvolnum>5</manvolnum></citerefentry> and
may be queried and changed with the <command>tags</command> command of
<citerefentry><refentrytitle>hostnamectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>. The
argument is a single tag pattern, which is compared against each of the configured tags using
shell-style globbing (<literal>*</literal>, <literal>?</literal>, <literal>[]</literal>). The
condition is satisfied if at least one of the configured tags matches the pattern. The test may be
negated by prepending an exclamation mark, in which case it is satisfied if none of the configured
tags matches.</para>
<xi:include href="version-info.xml" xpointer="v261"/>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>ConditionMemoryPressure=</varname></term>
<term><varname>ConditionCPUPressure=</varname></term>
@@ -2126,6 +2145,7 @@
<term><varname>AssertCPUs=</varname></term>
<term><varname>AssertCPUFeature=</varname></term>
<term><varname>AssertOSRelease=</varname></term>
<term><varname>AssertMachineTag=</varname></term>
<term><varname>AssertMemoryPressure=</varname></term>
<term><varname>AssertCPUPressure=</varname></term>
<term><varname>AssertIOPressure=</varname></term>
+1 -1
View File
@@ -67,7 +67,7 @@ _hostnamectl() {
local -A VERBS=(
[STANDALONE]='status'
[ICONS]='icon-name'
[NAME]='hostname deployment location'
[NAME]='hostname deployment location tags'
[CHASSIS]='chassis'
)
+6
View File
@@ -47,6 +47,11 @@ _hostnamectl_location() {
fi
}
(( $+functions[_hostnamectl_tags] )) ||
_hostnamectl_tags() {
_message "machine tag"
}
(( $+functions[_hostnamectl_commands] )) ||
_hostnamectl_commands() {
local -a _hostnamectl_cmds
@@ -57,6 +62,7 @@ _hostnamectl_commands() {
"chassis:Get/set chassis type for host"
"deployment:Get/set deployment environment for host"
"location:Get/set location for host"
"tags:Get/set machine tags for host"
)
if (( CURRENT == 1 )); then
_describe -t commands 'hostnamectl commands' _hostnamectl_cmds || compadd "$@"
+69
View File
@@ -253,3 +253,72 @@ int machine_spec_valid(const char *s) {
return true;
}
bool machine_tag_is_valid(const char *s) {
size_t n = strlen_ptr(s);
if (n <= 0 || n >= 256)
return false;
return in_charset(s, ALPHANUMERICAL "-.");
}
bool machine_tag_list_is_valid(char **l) {
size_t n = 0;
STRV_FOREACH(i, l) {
n++;
if (n > MACHINE_TAGS_MAX)
return false;
if (!machine_tag_is_valid(*i))
return false;
}
return true;
}
int machine_tags_from_string(const char *s, bool graceful, char ***ret) {
int r;
assert(ret);
/* Parses the colon-separated TAGS= machine-info field into a sorted, deduplicated strv. Each tag is
* validated: if 'graceful' is true invalid tags are silently dropped, otherwise an invalid tag makes
* us fail with -EINVAL. The result is NULL if no (valid) tags remain. */
if (isempty(s)) {
*ret = NULL;
return 0;
}
_cleanup_strv_free_ char **l = strv_split(s, ":");
if (!l)
return -ENOMEM;
strv_sort_uniq(l);
if (!graceful) {
if (!machine_tag_list_is_valid(l))
return -EINVAL;
*ret = strv_isempty(l) ? NULL : TAKE_PTR(l);
return 0;
}
size_t n = 0;
_cleanup_strv_free_ char **cleaned = NULL;
STRV_FOREACH(i, l) {
if (!machine_tag_is_valid(*i))
continue;
n++;
if (n > MACHINE_TAGS_MAX)
return -E2BIG;
r = strv_extend(&cleaned, *i);
if (r < 0)
return r;
}
*ret = TAKE_PTR(cleaned);
return 0;
}
+6
View File
@@ -47,3 +47,9 @@ int get_pretty_hostname(char **ret);
int machine_spec_valid(const char *s);
int split_user_at_host(const char *s, char **ret_user, char **ret_host);
#define MACHINE_TAGS_MAX 1024U
bool machine_tag_is_valid(const char *s);
bool machine_tag_list_is_valid(char **l);
int machine_tags_from_string(const char *s, bool graceful, char ***ret);
+2
View File
@@ -398,6 +398,7 @@ Unit.ConditionUser, config_parse_unit_condition_string
Unit.ConditionGroup, config_parse_unit_condition_string, CONDITION_GROUP, offsetof(Unit, conditions)
Unit.ConditionControlGroupController, config_parse_unit_condition_string, CONDITION_CONTROL_GROUP_CONTROLLER, offsetof(Unit, conditions)
Unit.ConditionOSRelease, config_parse_unit_condition_string, CONDITION_OS_RELEASE, offsetof(Unit, conditions)
Unit.ConditionMachineTag, config_parse_unit_condition_string, CONDITION_MACHINE_TAG, offsetof(Unit, conditions)
Unit.ConditionMemoryPressure, config_parse_unit_condition_string, CONDITION_MEMORY_PRESSURE, offsetof(Unit, conditions)
Unit.ConditionCPUPressure, config_parse_unit_condition_string, CONDITION_CPU_PRESSURE, offsetof(Unit, conditions)
Unit.ConditionIOPressure, config_parse_unit_condition_string, CONDITION_IO_PRESSURE, offsetof(Unit, conditions)
@@ -434,6 +435,7 @@ Unit.AssertUser, config_parse_unit_condition_string
Unit.AssertGroup, config_parse_unit_condition_string, CONDITION_GROUP, offsetof(Unit, asserts)
Unit.AssertControlGroupController, config_parse_unit_condition_string, CONDITION_CONTROL_GROUP_CONTROLLER, offsetof(Unit, asserts)
Unit.AssertOSRelease, config_parse_unit_condition_string, CONDITION_OS_RELEASE, offsetof(Unit, asserts)
Unit.AssertMachineTag, config_parse_unit_condition_string, CONDITION_MACHINE_TAG, offsetof(Unit, asserts)
Unit.AssertMemoryPressure, config_parse_unit_condition_string, CONDITION_MEMORY_PRESSURE, offsetof(Unit, asserts)
Unit.AssertCPUPressure, config_parse_unit_condition_string, CONDITION_CPU_PRESSURE, offsetof(Unit, asserts)
Unit.AssertIOPressure, config_parse_unit_condition_string, CONDITION_IO_PRESSURE, offsetof(Unit, asserts)
+81
View File
@@ -67,6 +67,7 @@ static char *arg_keymap = NULL;
static char *arg_timezone = NULL;
static char *arg_hostname = NULL;
static sd_id128_t arg_machine_id = {};
static char **arg_machine_tags = NULL;
static char *arg_root_password = NULL;
static char *arg_root_shell = NULL;
static char *arg_kernel_cmdline = NULL;
@@ -98,6 +99,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_locale_messages, freep);
STATIC_DESTRUCTOR_REGISTER(arg_keymap, freep);
STATIC_DESTRUCTOR_REGISTER(arg_timezone, freep);
STATIC_DESTRUCTOR_REGISTER(arg_hostname, freep);
STATIC_DESTRUCTOR_REGISTER(arg_machine_tags, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_root_password, erase_and_freep);
STATIC_DESTRUCTOR_REGISTER(arg_root_shell, freep);
STATIC_DESTRUCTOR_REGISTER(arg_kernel_cmdline, freep);
@@ -778,6 +780,71 @@ static int process_machine_id(int rfd) {
return 0;
}
static int process_machine_tags(int rfd) {
int r;
assert(rfd >= 0);
_cleanup_free_ char *f = NULL;
_cleanup_close_ int pfd = chase_and_open_parent_at(
/* root_fd= */ rfd,
/* dir_fd= */ rfd,
"/etc/machine-info",
CHASE_MKDIR_0755|CHASE_WARN|CHASE_NOFOLLOW,
&f);
if (pfd < 0)
return log_error_errno(pfd, "Failed to chase /etc/machine-info parent: %m");
r = should_configure(pfd, f);
if (r == 0)
log_debug("Found /etc/machine-info, assuming machine tags have been configured.");
if (r <= 0)
return r;
if (!arg_machine_tags) {
_cleanup_free_ char *tags = NULL;
r = read_credential("firstboot.machine-tags", (void**) &tags, /* ret_size= */ NULL);
if (r < 0)
log_debug_errno(r, "Failed to read credential firstboot.machine-tags, ignoring: %m");
else {
_cleanup_strv_free_ char **l = NULL;
r = machine_tags_from_string(tags, /* graceful= */ false, &l);
if (r < 0)
log_warning_errno(r, "Failed to parse machine tags '%s', ignoring credential: %m", tags);
else {
strv_free_and_replace(arg_machine_tags, l);
log_debug("Acquired machine tags list from credentials.");
}
}
}
/* NB: We do not prompt for machine tags, at least not for now */
if (!arg_machine_tags) {
log_debug("Initialization of machine tags was not requested, skipping.");
return 0;
}
_cleanup_free_ char *j = strv_join(arg_machine_tags, ":");
if (!j)
return log_oom();
_cleanup_free_ char *c = strjoin("TAGS=\"", j, "\"\n");
if (!c)
return log_oom();
r = write_string_file_at(
pfd,
"machine-info",
c,
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/machine-info: %m");
log_info("/etc/machine-info written.");
return 0;
}
static int prompt_root_password(int rfd, sd_varlink **mute_console_link) {
const char *msg1, *msg2;
int r;
@@ -1363,6 +1430,16 @@ static int parse_argv(int argc, char *argv[]) {
return log_error_errno(r, "Failed to parse machine id %s.", opts.arg);
break;
OPTION_LONG("machine-tags", "TAG[:…]", "Set machine tags"): {
_cleanup_strv_free_ char **tags = NULL;
r = machine_tags_from_string(opts.arg, /* graceful= */ false, &tags);
if (r < 0)
return log_error_errno(r, "Failed to parse machine tags '%s': %m", opts.arg);
strv_free_and_replace(arg_machine_tags, tags);
break;
}
OPTION_LONG("root-password", "PASSWORD", "Set root password from plaintext password"):
r = free_and_strdup_warn(&arg_root_password, opts.arg);
if (r < 0)
@@ -1693,6 +1770,10 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return r;
r = process_machine_tags(rfd);
if (r < 0)
return r;
return 0;
}
+88 -15
View File
@@ -9,6 +9,7 @@
#include "sd-json.h"
#include "alloc-util.h"
#include "ansi-color.h"
#include "build.h"
#include "bus-common-errors.h"
#include "bus-error.h"
@@ -18,6 +19,7 @@
#include "bus-util.h"
#include "errno-util.h"
#include "format-table.h"
#include "help-util.h"
#include "hostname-setup.h"
#include "hostname-util.h"
#include "log.h"
@@ -26,9 +28,9 @@
#include "os-util.h"
#include "parse-argument.h"
#include "polkit-agent.h"
#include "pretty-print.h"
#include "runtime-scope.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
#include "verbs.h"
@@ -49,6 +51,7 @@ typedef struct StatusInfo {
const char *chassis_asset_tag;
const char *deployment;
const char *location;
char **tags;
const char *kernel_name;
const char *kernel_release;
const char *os_pretty_name;
@@ -196,6 +199,18 @@ static int print_status_info(StatusInfo *i) {
return table_log_add_error(r);
}
if (!strv_isempty(i->tags)) {
_cleanup_free_ char *j = strv_join(i->tags, ":");
if (!j)
return log_oom();
r = table_add_many(table,
TABLE_FIELD, "Tags",
TABLE_STRING, j);
if (r < 0)
return table_log_add_error(r);
}
if (!sd_id128_is_null(i->machine_id)) {
r = table_add_many(table,
TABLE_FIELD, "Machine ID",
@@ -411,8 +426,14 @@ static int get_one_name(sd_bus *bus, const char* attr, char **ret) {
return 0;
}
static void status_info_done(StatusInfo *info) {
assert(info);
info->tags = strv_free(info->tags);
}
static int show_all_names(sd_bus *bus) {
StatusInfo info = {
_cleanup_(status_info_done) StatusInfo info = {
.vsock_cid = VMADDR_CID_ANY,
.os_support_end = USEC_INFINITY,
.firmware_date = USEC_INFINITY,
@@ -427,6 +448,7 @@ static int show_all_names(sd_bus *bus) {
{ "ChassisAssetTag", "s", NULL, offsetof(StatusInfo, chassis_asset_tag)},
{ "Deployment", "s", NULL, offsetof(StatusInfo, deployment) },
{ "Location", "s", NULL, offsetof(StatusInfo, location) },
{ "Tags", "as", NULL, offsetof(StatusInfo, tags) },
{ "KernelName", "s", NULL, offsetof(StatusInfo, kernel_name) },
{ "KernelRelease", "s", NULL, offsetof(StatusInfo, kernel_release) },
{ "OperatingSystemPrettyName", "s", NULL, offsetof(StatusInfo, os_pretty_name) },
@@ -719,14 +741,67 @@ static int verb_get_or_set_location(int argc, char *argv[], uintptr_t _data, voi
set_simple_string(userdata, "location", "SetLocation", argv[1]);
}
static int help(void) {
_cleanup_free_ char *link = NULL;
_cleanup_(table_unrefp) Table *options = NULL, *verbs = NULL;
VERB(verb_get_or_set_tags, "tags", "[TAG …]", VERB_ANY, VERB_ANY, 0, "Get/set machine tags for host");
static int verb_get_or_set_tags(int argc, char *argv[], uintptr_t _data, void *userdata) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
sd_bus *bus = ASSERT_PTR(userdata);
int r;
r = terminal_urlify_man("hostnamectl", "1", &link);
if (argc == 1) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_free_ char *j = NULL;
r = bus_get_property(bus, bus_hostname, "Tags", &error, &reply, "as");
if (r < 0) {
if (!sd_bus_error_has_name(&error, SD_BUS_ERROR_UNKNOWN_PROPERTY))
return log_error_errno(r, "Could not get property: %s", bus_error_message(&error, r));
/* Old hostnamed didn't know the tags concept, hence such a machine has no tags. */
} else {
_cleanup_strv_free_ char **l = NULL;
r = sd_bus_message_read_strv(reply, &l);
if (r < 0)
return bus_log_parse_error(r);
j = strv_join(l, ":");
if (!j)
return log_oom();
}
printf("%s\n", strempty(j));
return 0;
}
_cleanup_strv_free_ char **l = NULL;
for (int i = 1; i < argc; i++) {
r = strv_split_and_extend(&l, argv[i], ":", /* filter_duplicates= */ true);
if (r < 0)
return log_oom();
}
strv_sort(l);
(void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
r = bus_message_new_method_call(bus, &m, bus_hostname, "SetTags");
if (r < 0)
return log_oom();
return bus_log_create_error(r);
r = sd_bus_message_append_strv(m, l);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_call(bus, m, /* usec= */ 0, &error, /* ret_reply= */ NULL);
if (r < 0)
return log_error_errno(r, "Could not set tags: %s", bus_error_message(&error, r));
return 0;
}
static int help(void) {
_cleanup_(table_unrefp) Table *options = NULL, *verbs = NULL;
int r;
r = option_parser_get_help_table(&options);
if (r < 0)
@@ -738,22 +813,20 @@ static int help(void) {
(void) table_sync_column_widths(0, options, verbs);
printf("%s [OPTIONS...] COMMAND ...\n"
"\n%sQuery or change system hostname.%s\n"
"\nCommands:\n",
program_invocation_short_name,
ansi_highlight(),
ansi_normal());
help_cmdline("[OPTIONS...] COMMAND ...");
help_abstract("Query or change system hostname.");
help_section("Commands");
r = table_print_or_warn(verbs);
if (r < 0)
return r;
printf("\nOptions:\n");
help_section("Options");
r = table_print_or_warn(options);
if (r < 0)
return r;
printf("\nSee the %s for details.\n", link);
help_man_page_reference("hostnamectl", "1");
return 0;
}
+114 -22
View File
@@ -43,7 +43,6 @@
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
#include "utf8.h"
#include "varlink-io.systemd.Hostname.h"
#include "varlink-io.systemd.service.h"
#include "varlink-util.h"
@@ -58,12 +57,15 @@ typedef enum {
PROP_STATIC_HOSTNAME,
PROP_STATIC_HOSTNAME_SUBSTITUTED_WILDCARDS,
/* Read from /etc/machine-info */
/* Read from /etc/machine-info (with fallbacks) */
PROP_PRETTY_HOSTNAME,
_PROP_MACHINE_INFO_SETTABLE_FIRST = PROP_PRETTY_HOSTNAME,
PROP_ICON_NAME,
PROP_CHASSIS,
PROP_DEPLOYMENT,
PROP_LOCATION,
PROP_TAGS,
_PROP_MACHINE_INFO_SETTABLE_LAST = PROP_TAGS,
PROP_HARDWARE_VENDOR,
PROP_HARDWARE_MODEL,
PROP_HARDWARE_SKU,
@@ -77,6 +79,7 @@ typedef enum {
PROP_OS_SUPPORT_END,
PROP_OS_IMAGE_ID,
PROP_OS_IMAGE_VERSION,
_PROP_MAX,
_PROP_INVALID = -EINVAL,
} HostProperty;
@@ -173,6 +176,7 @@ static void context_read_machine_info(Context *c) {
PROP_CHASSIS,
PROP_DEPLOYMENT,
PROP_LOCATION,
PROP_TAGS,
PROP_HARDWARE_VENDOR,
PROP_HARDWARE_MODEL,
PROP_HARDWARE_SKU,
@@ -184,6 +188,7 @@ static void context_read_machine_info(Context *c) {
"CHASSIS", &c->data[PROP_CHASSIS],
"DEPLOYMENT", &c->data[PROP_DEPLOYMENT],
"LOCATION", &c->data[PROP_LOCATION],
"TAGS", &c->data[PROP_TAGS],
"HARDWARE_VENDOR", &c->data[PROP_HARDWARE_VENDOR],
"HARDWARE_MODEL", &c->data[PROP_HARDWARE_MODEL],
"HARDWARE_SKU", &c->data[PROP_HARDWARE_SKU],
@@ -321,18 +326,6 @@ static int context_acquire_device_tree(Context *c) {
return 1;
}
static bool string_is_safe_for_dbus(const char *s) {
assert(s);
/* Do some superficial validation: do not allow CCs and make sure D-Bus won't kick us off the bus
* because we send invalid UTF-8 data */
if (string_has_cc(s, /* ok= */ NULL))
return false;
return utf8_is_valid(s);
}
static int get_dmi_property(Context *c, const char *key, char **ret) {
const char *s;
int r;
@@ -348,7 +341,7 @@ static int get_dmi_property(Context *c, const char *key, char **ret) {
if (r < 0)
return r;
if (!string_is_safe_for_dbus(s))
if (!string_is_safe(s, STRING_ALLOW_EMPTY|STRING_ALLOW_BACKSLASHES|STRING_ALLOW_QUOTES|STRING_ALLOW_GLOBS))
return -ENXIO;
return strdup_to(ret, s);
@@ -447,7 +440,7 @@ static int get_sysattr(sd_device *device, const char *key, char **ret) {
if (r < 0)
return log_device_debug_errno(device, r, "Failed to read '%s' attribute: %m", key);
if (!string_is_safe_for_dbus(s))
if (!string_is_safe(s, STRING_ALLOW_EMPTY|STRING_ALLOW_BACKSLASHES|STRING_ALLOW_QUOTES|STRING_ALLOW_GLOBS))
return log_device_debug_errno(device, SYNTHETIC_ERRNO(ENXIO),
"'%s' attribute is not safe for exposing through DBus: %s",
key, s);
@@ -852,11 +845,12 @@ static int context_write_data_static_hostname(Context *c) {
static int context_write_data_machine_info(Context *c) {
_cleanup_(unset_statp) struct stat *s = NULL;
static const char * const name[_PROP_MAX] = {
[PROP_PRETTY_HOSTNAME] = "PRETTY_HOSTNAME",
[PROP_ICON_NAME] = "ICON_NAME",
[PROP_CHASSIS] = "CHASSIS",
[PROP_DEPLOYMENT] = "DEPLOYMENT",
[PROP_LOCATION] = "LOCATION",
[PROP_PRETTY_HOSTNAME] = "PRETTY_HOSTNAME",
[PROP_ICON_NAME] = "ICON_NAME",
[PROP_CHASSIS] = "CHASSIS",
[PROP_DEPLOYMENT] = "DEPLOYMENT",
[PROP_LOCATION] = "LOCATION",
[PROP_TAGS] = "TAGS",
};
_cleanup_strv_free_ char **l = NULL;
int r;
@@ -871,7 +865,7 @@ static int context_write_data_machine_info(Context *c) {
if (r < 0 && r != -ENOENT)
return r;
for (HostProperty p = PROP_PRETTY_HOSTNAME; p <= PROP_LOCATION; p++) {
for (HostProperty p = _PROP_MACHINE_INFO_SETTABLE_FIRST; p <= _PROP_MACHINE_INFO_SETTABLE_LAST; p++) {
assert(name[p]);
r = strv_env_assign(&l, name[p], empty_to_null(c->data[p]));
@@ -1175,6 +1169,29 @@ static int property_get_machine_info_field(
return sd_bus_message_append(reply, "s", *(char**) userdata);
}
static int property_get_tags(
sd_bus *bus,
const char *path,
const char *interface,
const char *property,
sd_bus_message *reply,
void *userdata,
sd_bus_error *error) {
Context *c = ASSERT_PTR(userdata);
int r;
context_read_machine_info(c);
/* Silently drop any invalid tags that might have been written into the file by hand */
_cleanup_strv_free_ char **l = NULL;
r = machine_tags_from_string(c->data[PROP_TAGS], /* graceful= */ true, &l);
if (r < 0)
log_warning_errno(r, "Failed to parse machine tags '%s', ignoring: %m", strnull(c->data[PROP_TAGS]));
return sd_bus_message_append_strv(reply, l);
}
static int property_get_os_release_field(
sd_bus *bus,
const char *path,
@@ -1573,6 +1590,74 @@ static int method_set_location(sd_bus_message *m, void *userdata, sd_bus_error *
return set_machine_info(userdata, m, PROP_LOCATION, method_set_location, error);
}
static int method_set_tags(sd_bus_message *m, void *userdata, sd_bus_error *error) {
Context *c = ASSERT_PTR(userdata);
int r;
assert(m);
_cleanup_strv_free_ char **tags = NULL;
r = sd_bus_message_read_strv(m, &tags);
if (r < 0)
return r;
strv_sort_uniq(tags);
if (strv_length(tags) > MACHINE_TAGS_MAX)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Too many machine tags specified.");
_cleanup_free_ char *j = strv_join(tags, ":");
if (!j)
return log_oom();
if (!machine_tag_list_is_valid(tags))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid tags '%s'", j);
context_read_machine_info(c);
if (streq_ptr(empty_to_null(j), empty_to_null(c->data[PROP_TAGS])))
return sd_bus_reply_method_return(m, NULL);
r = bus_verify_polkit_async_full(
m,
"org.freedesktop.hostname1.set-machine-info",
/* details= */ NULL,
/* good_user= */ UID_INVALID,
/* flags= */ 0,
&c->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* No authorization for now, but the async polkit stuff will call us again when it has it */
if (strv_isempty(tags))
c->data[PROP_TAGS] = mfree(c->data[PROP_TAGS]);
else
free_and_replace(c->data[PROP_TAGS], j);
r = context_write_data_machine_info(c);
if (r < 0) {
log_error_errno(r, "Failed to write machine info: %m");
if (ERRNO_IS_PRIVILEGE(r))
return sd_bus_error_set(error, BUS_ERROR_FILE_IS_PROTECTED, "Not allowed to update /etc/machine-info.");
if (r == -EROFS)
return sd_bus_error_set(error, BUS_ERROR_READ_ONLY_FILESYSTEM, "/etc/machine-info is in a read-only filesystem.");
return sd_bus_error_set_errnof(error, r, "Failed to write machine info: %m");
}
log_info("Changed tags to '%s'", strempty(c->data[PROP_TAGS]));
(void) sd_bus_emit_properties_changed(
sd_bus_message_get_bus(m),
"/org/freedesktop/hostname1",
"org.freedesktop.hostname1",
"Tags",
NULL);
return sd_bus_reply_method_return(m, NULL);
}
static int method_get_product_uuid(sd_bus_message *m, void *userdata, sd_bus_error *error) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
Context *c = ASSERT_PTR(userdata);
@@ -1657,6 +1742,7 @@ static int method_get_machine_info(sd_bus_message *m, void *userdata, sd_bus_err
{ "CHASSIS", PROP_CHASSIS },
{ "DEPLOYMENT", PROP_DEPLOYMENT },
{ "LOCATION", PROP_LOCATION },
{ "TAGS", PROP_TAGS },
{ "HARDWARE_VENDOR", PROP_HARDWARE_VENDOR },
{ "HARDWARE_MODEL", PROP_HARDWARE_MODEL },
{ "HARDWARE_SKU", PROP_HARDWARE_SKU },
@@ -1865,6 +1951,7 @@ static const sd_bus_vtable hostname_vtable[] = {
SD_BUS_PROPERTY("Chassis", "s", property_get_chassis, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("Deployment", "s", property_get_machine_info_field, offsetof(Context, data[PROP_DEPLOYMENT]), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("Location", "s", property_get_machine_info_field, offsetof(Context, data[PROP_LOCATION]), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("Tags", "as", property_get_tags, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("KernelName", "s", property_get_uname_field, offsetof(struct utsname, sysname), SD_BUS_VTABLE_ABSOLUTE_OFFSET|SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("KernelRelease", "s", property_get_uname_field, offsetof(struct utsname, release), SD_BUS_VTABLE_ABSOLUTE_OFFSET|SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("KernelVersion", "s", property_get_uname_field, offsetof(struct utsname, version), SD_BUS_VTABLE_ABSOLUTE_OFFSET|SD_BUS_VTABLE_PROPERTY_CONST),
@@ -1922,6 +2009,11 @@ static const sd_bus_vtable hostname_vtable[] = {
SD_BUS_NO_RESULT,
method_set_location,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("SetTags",
SD_BUS_ARGS("as", tags),
SD_BUS_NO_RESULT,
method_set_tags,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("GetProductUUID",
SD_BUS_ARGS("b", interactive),
SD_BUS_RESULT("ay", uuid),
+35
View File
@@ -35,6 +35,7 @@
#include "glob-util.h"
#include "hmac.h"
#include "hostname-setup.h"
#include "hostname-util.h"
#include "id128-util.h"
#include "ima-util.h"
#include "initrd-util.h"
@@ -316,6 +317,37 @@ static int condition_test_osrelease(Condition *c, char **env) {
return true;
}
static int condition_test_machine_tag(Condition *c, char **env) {
int r;
assert(c);
assert(c->parameter);
assert(c->type == CONDITION_MACHINE_TAG);
_cleanup_free_ char *tags = NULL;
r = parse_env_file(
/* f= */ NULL, etc_machine_info(),
"TAGS", &tags);
if (r < 0 && r != -ENOENT) {
log_debug_errno(r, "Failed to read /etc/machine-info, ignoring: %m");
return false;
}
/* Silently ignore invalid tags, matching the Tags D-Bus property */
_cleanup_strv_free_ char **l = NULL;
r = machine_tags_from_string(tags, /* graceful= */ true, &l);
if (r < 0) {
log_debug_errno(r, "Failed to parse machine tags '%s', ignoring: %m", tags);
return false;
}
STRV_FOREACH(i, l)
if (fnmatch(c->parameter, *i, /* flags= */ 0) == 0)
return true;
return false;
}
static int condition_test_memory(Condition *c, char **env) {
CompareOperator operator;
uint64_t m, k;
@@ -1344,6 +1376,7 @@ int condition_test(Condition *c, char **env) {
[CONDITION_ENVIRONMENT] = condition_test_environment,
[CONDITION_CPU_FEATURE] = condition_test_cpufeature,
[CONDITION_OS_RELEASE] = condition_test_osrelease,
[CONDITION_MACHINE_TAG] = condition_test_machine_tag,
[CONDITION_MEMORY_PRESSURE] = condition_test_psi,
[CONDITION_CPU_PRESSURE] = condition_test_psi,
[CONDITION_IO_PRESSURE] = condition_test_psi,
@@ -1471,6 +1504,7 @@ static const char* const _condition_type_table[_CONDITION_TYPE_MAX] = {
[CONDITION_ENVIRONMENT] = "ConditionEnvironment",
[CONDITION_CPU_FEATURE] = "ConditionCPUFeature",
[CONDITION_OS_RELEASE] = "ConditionOSRelease",
[CONDITION_MACHINE_TAG] = "ConditionMachineTag",
[CONDITION_MEMORY_PRESSURE] = "ConditionMemoryPressure",
[CONDITION_CPU_PRESSURE] = "ConditionCPUPressure",
[CONDITION_IO_PRESSURE] = "ConditionIOPressure",
@@ -1528,6 +1562,7 @@ static const char* const _assert_type_table[_CONDITION_TYPE_MAX] = {
[CONDITION_ENVIRONMENT] = "AssertEnvironment",
[CONDITION_CPU_FEATURE] = "AssertCPUFeature",
[CONDITION_OS_RELEASE] = "AssertOSRelease",
[CONDITION_MACHINE_TAG] = "AssertMachineTag",
[CONDITION_MEMORY_PRESSURE] = "AssertMemoryPressure",
[CONDITION_CPU_PRESSURE] = "AssertCPUPressure",
[CONDITION_IO_PRESSURE] = "AssertIOPressure",
+1
View File
@@ -21,6 +21,7 @@ typedef enum ConditionType {
CONDITION_ENVIRONMENT,
CONDITION_CPU_FEATURE,
CONDITION_OS_RELEASE,
CONDITION_MACHINE_TAG,
CONDITION_MEMORY_PRESSURE,
CONDITION_CPU_PRESSURE,
CONDITION_IO_PRESSURE,
+63
View File
@@ -1414,6 +1414,69 @@ TEST(condition_test_os_release) {
condition_free(condition);
}
TEST(condition_test_machine_tag) {
Condition *condition;
/* etc_machine_info() caches the path on first use, so redirect it before anything reads it and
* rewrite the same file (truncating) for each scenario rather than switching paths. */
_cleanup_free_ char *saved = NULL;
ASSERT_OK(free_and_strdup(&saved, getenv("SYSTEMD_ETC_MACHINE_INFO")));
_cleanup_(rm_rf_physical_and_freep) char *d = NULL;
ASSERT_OK(mkdtemp_malloc(NULL, &d));
_cleanup_free_ char *f = path_join(d, "machine-info");
ASSERT_NOT_NULL(f);
ASSERT_OK_ERRNO(setenv("SYSTEMD_ETC_MACHINE_INFO", f, /* overwrite= */ true));
ASSERT_OK(write_string_file(f, "TAGS=\"webserver:frontend:berlin\"\n",
WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE));
/* Exact match */
ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "webserver", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_POSITIVE(condition_test(condition, environ));
condition_free(condition);
/* Glob match */
ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "front*", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_POSITIVE(condition_test(condition, environ));
condition_free(condition);
/* No match */
ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "database", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_ZERO(condition_test(condition, environ));
condition_free(condition);
/* Negation matches when the tag is absent, ... */
ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "database", /* trigger= */ false, /* negate= */ true)));
ASSERT_OK_POSITIVE(condition_test(condition, environ));
condition_free(condition);
/* ... and does not match when the tag is present */
ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "webserver", /* trigger= */ false, /* negate= */ true)));
ASSERT_OK_ZERO(condition_test(condition, environ));
condition_free(condition);
/* Invalid tags in the file are ignored (matching the Tags D-Bus property) */
ASSERT_OK(write_string_file(f, "TAGS=\"in valid:good\"\n",
WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE));
ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "in valid", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_ZERO(condition_test(condition, environ));
condition_free(condition);
ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "good", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_POSITIVE(condition_test(condition, environ));
condition_free(condition);
/* No tags configured at all → never matches */
ASSERT_OK(write_string_file(f, "PRETTY_HOSTNAME=\"x\"\n",
WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE));
ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "webserver", /* trigger= */ false, /* negate= */ false)));
ASSERT_OK_ZERO(condition_test(condition, environ));
condition_free(condition);
ASSERT_OK(set_unset_env("SYSTEMD_ETC_MACHINE_INFO", saved, /* overwrite= */ true));
}
TEST(condition_test_psi) {
Condition *condition;
CGroupMask mask;
+64
View File
@@ -1,7 +1,9 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
#include "hostname-util.h"
#include "string-util.h"
#include "strv.h"
#include "tests.h"
TEST(hostname_is_valid) {
@@ -117,4 +119,66 @@ TEST(split_user_at_host) {
test_split_user_at_host_one("aa@@@bb", "aa", "@@bb", 1);
}
TEST(machine_tag_is_valid) {
assert_se(machine_tag_is_valid("foo"));
assert_se(machine_tag_is_valid("foo-bar.baz"));
assert_se(machine_tag_is_valid("Webserver01"));
assert_se(machine_tag_is_valid("a"));
assert_se(!machine_tag_is_valid(NULL));
assert_se(!machine_tag_is_valid(""));
assert_se(!machine_tag_is_valid("foo:bar")); /* colon is the separator */
assert_se(!machine_tag_is_valid("foo bar"));
assert_se(!machine_tag_is_valid("fööbar")); /* non-ASCII */
assert_se(!machine_tag_is_valid("foo/bar"));
assert_se(!machine_tag_is_valid("foo_bar"));
/* Length boundary: 255 characters is fine, 256 is too long */
_cleanup_free_ char *max = strrep("a", 255), *over = strrep("a", 256);
assert_se(max);
assert_se(over);
assert_se(machine_tag_is_valid(max));
assert_se(!machine_tag_is_valid(over));
}
TEST(machine_tag_list_is_valid) {
assert_se(machine_tag_list_is_valid(NULL)); /* empty list is valid */
assert_se(machine_tag_list_is_valid(STRV_MAKE("a")));
assert_se(machine_tag_list_is_valid(STRV_MAKE("foo", "bar", "c-d.e")));
assert_se(!machine_tag_list_is_valid(STRV_MAKE("foo", "b:c")));
assert_se(!machine_tag_list_is_valid(STRV_MAKE("foo", "")));
}
TEST(machine_tags_from_string) {
_cleanup_strv_free_ char **l = NULL;
ASSERT_OK(machine_tags_from_string(NULL, /* graceful= */ false, &l));
assert_se(strv_isempty(l));
l = strv_free(l);
ASSERT_OK(machine_tags_from_string("", /* graceful= */ true, &l));
assert_se(strv_isempty(l));
l = strv_free(l);
/* Sorted and deduplicated */
ASSERT_OK(machine_tags_from_string("foo:bar:foo:baz", /* graceful= */ false, &l));
assert_se(strv_equal(l, STRV_MAKE("bar", "baz", "foo")));
l = strv_free(l);
/* Graceful: invalid tags are dropped, valid ones kept (sorted/deduplicated) */
ASSERT_OK(machine_tags_from_string("foo:in valid:bar:foo", /* graceful= */ true, &l));
assert_se(strv_equal(l, STRV_MAKE("bar", "foo")));
l = strv_free(l);
/* Graceful: all tags invalid → empty list */
ASSERT_OK(machine_tags_from_string("in valid:also invalid", /* graceful= */ true, &l));
assert_se(strv_isempty(l));
l = strv_free(l);
/* Fatal: a single invalid tag fails the whole parse */
ASSERT_ERROR(machine_tags_from_string("foo:in valid:bar", /* graceful= */ false, &l), EINVAL);
assert_se(!l);
}
DEFINE_TEST_MAIN(LOG_DEBUG);
@@ -24,6 +24,7 @@ AssertHost=
AssertIOPressure=
AssertKernelCommandLine=
AssertKernelVersion=
AssertMachineTag=
AssertMemoryPressure=
AssertNeedsUpdate=
AssertOSRelease=
@@ -72,6 +73,7 @@ ConditionHost=
ConditionIOPressure=
ConditionKernelCommandLine=
ConditionKernelVersion=
ConditionMachineTag=
ConditionMemoryPressure=
ConditionNeedsUpdate=
ConditionOSRelease=
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
# shellcheck disable=SC1091
set -eux
set -o pipefail
# shellcheck source=test/units/util.sh
. "$(dirname "$0")"/util.sh
if ! command -v hostnamectl >/dev/null; then
echo "hostnamectl not found, skipping the test"
exit 0
fi
at_exit() {
set +e
# Restore the original /etc/machine-info (if any) and make hostnamed read it again
if [[ -e /tmp/machine-info.bak ]]; then
mv /tmp/machine-info.bak /etc/machine-info
else
rm -f /etc/machine-info
fi
systemctl stop --job-mode=replace-irreversibly systemd-hostnamed.service
systemctl reset-failed systemd-hostnamed.service
rm -fr "${ROOT:-}"
}
trap at_exit EXIT
# Read the TAGS= field out of /etc/machine-info (robust against the file being
# absent and against TAGS= being unset).
get_tags_from_file() (
set +u
[[ -f /etc/machine-info ]] && . /etc/machine-info
echo "${TAGS:-}"
)
if [[ -f /etc/machine-info ]]; then
cp /etc/machine-info /tmp/machine-info.bak
fi
# Start from a clean slate, and make sure hostnamed re-reads the (now missing) file.
rm -f /etc/machine-info
systemctl stop --job-mode=replace-irreversibly systemd-hostnamed.service || :
systemctl reset-failed systemd-hostnamed.service || :
# --------------------------------------------------------------------------------------------------
# hostnamectl tags <-> /etc/machine-info
# --------------------------------------------------------------------------------------------------
# No tags are configured initially.
assert_eq "$(hostnamectl tags)" ""
# Set some tags. Multiple arguments, colon-separated arguments, duplicates and arbitrary order are
# all accepted and normalized into a sorted, deduplicated, colon-separated list.
hostnamectl tags webserver:frontend frontend berlin
assert_eq "$(hostnamectl tags)" "berlin:frontend:webserver"
# The very same list must show up in the TAGS= field of /etc/machine-info.
grep -qE '^TAGS="?berlin:frontend:webserver"?$' /etc/machine-info
assert_eq "$(get_tags_from_file)" "berlin:frontend:webserver"
# Setting tags again replaces the previous list rather than extending it.
hostnamectl tags database
assert_eq "$(hostnamectl tags)" "database"
assert_eq "$(get_tags_from_file)" "database"
# Invalid tags (only ASCII alphanumerics, '-' and '.' are allowed) are refused, leaving the
# previously configured tags untouched.
(! hostnamectl tags "invalid tag")
(! hostnamectl tags "invalid/tag")
assert_eq "$(hostnamectl tags)" "database"
# Clearing all tags via a single empty string argument. When TAGS= was the only field, hostnamed
# removes /etc/machine-info altogether.
hostnamectl tags ""
assert_eq "$(hostnamectl tags)" ""
assert_eq "$(get_tags_from_file)" ""
# --------------------------------------------------------------------------------------------------
# ConditionMachineTag=/AssertMachineTag= in a transient unit
# --------------------------------------------------------------------------------------------------
hostnamectl tags alpha:beta:gamma
assert_eq "$(hostnamectl tags)" "alpha:beta:gamma"
# When the condition matches, the unit is started and 'false' actually runs, so systemd-run fails.
# When the condition does not match, the unit is skipped (not failed) and systemd-run succeeds.
(! systemd-run --wait --pipe -p ConditionMachineTag=beta false)
systemd-run --wait --pipe -p ConditionMachineTag=delta false
# Globs are matched against each individual tag.
(! systemd-run --wait --pipe -p ConditionMachineTag='al*' false)
systemd-run --wait --pipe -p ConditionMachineTag='z*' false
# Negation inverts the result.
systemd-run --wait --pipe -p ConditionMachineTag='!beta' false
(! systemd-run --wait --pipe -p ConditionMachineTag='!delta' false)
# Asserts behave like conditions, except a failing assert puts the unit into a failed state, which
# systemd-run propagates as a non-zero exit code.
systemd-run -p AssertMachineTag=beta -p Type=oneshot true
(! systemd-run -p AssertMachineTag=delta -p Type=oneshot true)
# --------------------------------------------------------------------------------------------------
# systemd-firstboot --machine-tags= and the firstboot.machine-tags credential
# --------------------------------------------------------------------------------------------------
if command -v systemd-firstboot >/dev/null; then
# The firstboot.machine-tags credential is split on ':', deduplicated and sorted, and written
# into /etc/machine-info underneath the target root.
ROOT="$(mktemp -d)"
systemd-run --wait --pipe --service-type=exec \
-p SetCredential=firstboot.machine-tags:webserver:frontend:webserver:berlin \
systemd-firstboot --root="$ROOT"
grep -qE '^TAGS="?berlin:frontend:webserver"?$' "$ROOT/etc/machine-info"
# An invalid tag anywhere in the credential causes the whole list to be ignored, so no
# machine-info file is written.
rm -fr "$ROOT"
ROOT="$(mktemp -d)"
systemd-run --wait --pipe --service-type=exec \
-p SetCredential=firstboot.machine-tags:'good:bad/tag' \
systemd-firstboot --root="$ROOT"
test ! -e "$ROOT/etc/machine-info"
# The --machine-tags= switch is normalized the same way and takes precedence over the credential.
rm -fr "$ROOT"
ROOT="$(mktemp -d)"
systemd-run --wait --pipe --service-type=exec \
-p SetCredential=firstboot.machine-tags:ignored \
systemd-firstboot --root="$ROOT" --machine-tags=database:cache:database
grep -qE '^TAGS="?cache:database"?$' "$ROOT/etc/machine-info"
# An invalid tag passed on the command line is a hard error.
rm -fr "$ROOT"
ROOT="$(mktemp -d)"
(! systemd-firstboot --root="$ROOT" --machine-tags='good:bad/tag')
fi