diff --git a/TODO.md b/TODO.md index 5c8f92ac42..6e7cc16112 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/man/hostnamectl.xml b/man/hostnamectl.xml index 8ac18349c6..878837bee1 100644 --- a/man/hostnamectl.xml +++ b/man/hostnamectl.xml @@ -165,6 +165,32 @@ + + + tags [TAG…] + + If no argument is given, print the machine tags currently configured, as a + colon-separated list. If one or more TAG 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. + + 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, + - and .. The tags are stored in the TAGS= + field of /etc/machine-info; see + machine-info5 for + details. They may also be matched against with the + ConditionMachineTag=/AssertMachineTag= unit settings, see + systemd.unit5. + + + + diff --git a/man/machine-info.xml b/man/machine-info.xml index 53c9e64ec4..252f341bba 100644 --- a/man/machine-info.xml +++ b/man/machine-info.xml @@ -139,6 +139,27 @@ + + TAGS= + + 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 (webserver, database), the + fleet or organizational unit it belongs to, or any other administrator-defined attribute. Example: + TAGS=webserver:frontend:berlin. + + Each individual tag must be 1…255 characters long and may consist only of the ASCII + alphanumeric characters, - and .. + + The configured tags may be matched against with the + ConditionMachineTag= and AssertMachineTag= unit settings, see + systemd.unit5 + for details. They may be queried and changed with the tags command of + hostnamectl1. + + + + HARDWARE_VENDOR= @@ -195,7 +216,8 @@ PRETTY_HOSTNAME="Lennart's Tablet" ICON_NAME=computer-tablet CHASSIS=tablet -DEPLOYMENT=production +DEPLOYMENT=production +TAGS=demo:berlin diff --git a/man/org.freedesktop.hostname1.xml b/man/org.freedesktop.hostname1.xml index 3d98b88ebc..d5571de207 100644 --- a/man/org.freedesktop.hostname1.xml +++ b/man/org.freedesktop.hostname1.xml @@ -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 { + + @@ -168,6 +172,8 @@ node /org/freedesktop/hostname1 { + + @@ -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. + The Tags property exposes a list of machine tags: 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, - and .. The tags are stored as a + colon-separated list in the TAGS= field of /etc/machine-info, see + machine-info5 for + details. If no tags are configured this property is the empty list. + Note that systemd-hostnamed starts only on request and terminates after a short idle period. This effectively means that PropertyChanged 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 Location (physical system location), respectively. + SetTags() sets the machine tags exposed by the Tags + property. It takes a list of strings, each of which must be a valid machine tag (1…255 ASCII + alphanumeric characters, - and .). Passing an empty list removes + the TAGS= field from /etc/machine-info. + PrettyHostname, IconName, Chassis, - Deployment, and Location are stored in + Deployment, Location, and Tags are stored in /etc/machine-info. See machine-info5 for the semantics of those settings. @@ -400,8 +420,8 @@ node /org/freedesktop/hostname1 { org.freedesktop.hostname1.set-hostname. For SetStaticHostname() and SetPrettyHostname() it is org.freedesktop.hostname1.set-static-hostname. For - SetIconName(), SetChassis(), SetDeployment() - and SetLocation() it is + SetIconName(), SetChassis(), SetDeployment(), + SetLocation() and SetTags() it is org.freedesktop.hostname1.set-machine-info. @@ -505,7 +525,8 @@ node /org/freedesktop/hostname1 { OperatingSystemImageVersion, HardwareSKU, and HardwareVersion were added in version 258. OperatingSystemFancyName was added in version 260. - GetMachineInfo() was added in version 261. + GetMachineInfo(), Tags and + SetTags() were added in version 261. diff --git a/man/systemd-firstboot.xml b/man/systemd-firstboot.xml index db6f2569a8..252ec73fed 100644 --- a/man/systemd-firstboot.xml +++ b/man/systemd-firstboot.xml @@ -67,6 +67,8 @@ The system hostname + The machine tags + The kernel command line used when installing kernel images The root user's password and shell @@ -198,6 +200,27 @@ + + + + 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, - and .. This + controls the TAGS= field of + machine-info5. The + tags may later be queried and changed with the tags command of + hostnamectl1, and + matched against with the ConditionMachineTag=/AssertMachineTag= + unit settings, see + systemd.unit5. + + Unlike most other settings, machine tags are not prompted for interactively. + + + + @@ -458,6 +481,18 @@ + + + firstboot.machine-tags + + This credential specifies the machine tags to set during first boot, as a + colon-separated list, equivalent to the switch described above. The + tags are written into the TAGS= field of /etc/machine-info + (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. + + + Note that by default the systemd-firstboot.service unit file is set up to diff --git a/man/systemd.system-credentials.xml b/man/systemd.system-credentials.xml index fb1377c560..f316961cd7 100644 --- a/man/systemd.system-credentials.xml +++ b/man/systemd.system-credentials.xml @@ -91,6 +91,21 @@ + + firstboot.machine-tags + + The machine tags to set, as a colon-separated list (e.g. + webserver:frontend:berlin). Read by + systemd-firstboot1, + and only honoured if /etc/machine-info has not been configured before. Written + into the TAGS= field of that file, see + machine-info5 for + details. + + + + + login.issue diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml index 5c3439d193..2990e65891 100644 --- a/man/systemd.unit.xml +++ b/man/systemd.unit.xml @@ -2058,6 +2058,25 @@ + + ConditionMachineTag= + + ConditionMachineTag= 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 TAGS= field of + machine-info5 and + may be queried and changed with the tags command of + hostnamectl1. The + argument is a single tag pattern, which is compared against each of the configured tags using + shell-style globbing (*, ?, []). 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. + + + + + ConditionMemoryPressure= ConditionCPUPressure= @@ -2126,6 +2145,7 @@ AssertCPUs= AssertCPUFeature= AssertOSRelease= + AssertMachineTag= AssertMemoryPressure= AssertCPUPressure= AssertIOPressure= diff --git a/shell-completion/bash/hostnamectl b/shell-completion/bash/hostnamectl index 0baffeed9c..a4ebe25912 100644 --- a/shell-completion/bash/hostnamectl +++ b/shell-completion/bash/hostnamectl @@ -67,7 +67,7 @@ _hostnamectl() { local -A VERBS=( [STANDALONE]='status' [ICONS]='icon-name' - [NAME]='hostname deployment location' + [NAME]='hostname deployment location tags' [CHASSIS]='chassis' ) diff --git a/shell-completion/zsh/_hostnamectl b/shell-completion/zsh/_hostnamectl index 76473937a0..30facfb134 100644 --- a/shell-completion/zsh/_hostnamectl +++ b/shell-completion/zsh/_hostnamectl @@ -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 "$@" diff --git a/src/basic/hostname-util.c b/src/basic/hostname-util.c index 01434d3d64..baa735ab10 100644 --- a/src/basic/hostname-util.c +++ b/src/basic/hostname-util.c @@ -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; +} diff --git a/src/basic/hostname-util.h b/src/basic/hostname-util.h index 73cd83fbb8..f3d904a1bb 100644 --- a/src/basic/hostname-util.h +++ b/src/basic/hostname-util.h @@ -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); diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index be074230ce..60fec56f46 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -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) diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index e328c85500..e922abdd63 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -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; } diff --git a/src/hostname/hostnamectl.c b/src/hostname/hostnamectl.c index 282a16d83c..d52580fbf5 100644 --- a/src/hostname/hostnamectl.c +++ b/src/hostname/hostnamectl.c @@ -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; } diff --git a/src/hostname/hostnamed.c b/src/hostname/hostnamed.c index 2292c07661..a39bf5a57e 100644 --- a/src/hostname/hostnamed.c +++ b/src/hostname/hostnamed.c @@ -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), diff --git a/src/shared/condition.c b/src/shared/condition.c index d0e41bbf71..ae33c18ade 100644 --- a/src/shared/condition.c +++ b/src/shared/condition.c @@ -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", diff --git a/src/shared/condition.h b/src/shared/condition.h index 91af2027df..1f5c33284c 100644 --- a/src/shared/condition.h +++ b/src/shared/condition.h @@ -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, diff --git a/src/test/test-condition.c b/src/test/test-condition.c index bee1014c1c..4f450ab12d 100644 --- a/src/test/test-condition.c +++ b/src/test/test-condition.c @@ -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; diff --git a/src/test/test-hostname-util.c b/src/test/test-hostname-util.c index b3e3b1b84a..1a3cb1dfff 100644 --- a/src/test/test-hostname-util.c +++ b/src/test/test-hostname-util.c @@ -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); diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service index 9fedef5a63..0d6fadcc96 100644 --- a/test/fuzz/fuzz-unit-file/directives-all.service +++ b/test/fuzz/fuzz-unit-file/directives-all.service @@ -24,6 +24,7 @@ AssertHost= AssertIOPressure= AssertKernelCommandLine= AssertKernelVersion= +AssertMachineTag= AssertMemoryPressure= AssertNeedsUpdate= AssertOSRelease= @@ -72,6 +73,7 @@ ConditionHost= ConditionIOPressure= ConditionKernelCommandLine= ConditionKernelVersion= +ConditionMachineTag= ConditionMemoryPressure= ConditionNeedsUpdate= ConditionOSRelease= diff --git a/test/units/TEST-74-AUX-UTILS.machine-tags.sh b/test/units/TEST-74-AUX-UTILS.machine-tags.sh new file mode 100755 index 0000000000..5bd421e489 --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.machine-tags.sh @@ -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