diff --git a/TODO.md b/TODO.md index 354af92755..03d0462ef7 100644 --- a/TODO.md +++ b/TODO.md @@ -265,9 +265,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later - sysupdate: add concept for enabling/disabling specific components explicitly, just like features. -- udev: add a MACHINE_TAGS field, that augments /etc/machine-info configured - tags. - - hostnamectl: management, collation of all tags. four sources: udev, /etc/machine-info, credentials, and /etc/machine-tags.d/*.conf @@ -780,7 +777,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later - clean up date formatting and parsing so that all absolute/relative timestamps we format can also be parsed - **complete varlink introspection comments:** - - io.systemd.Hostname - io.systemd.ManagedOOM - io.systemd.Network - io.systemd.PCRLock @@ -2431,8 +2427,8 @@ SPDX-License-Identifier: LGPL-2.1-or-later PR_SET_DUMPABLE so that it cannot be ptraced from the host. Should have CAP_SYS_BPF as only service around. -- SIGRTMIN+18 and memory pressure handling should still be added to: hostnamed, - localed, oomd, timedated. +- SIGRTMIN+18 and memory pressure handling should still be added to: localed, + oomd, timedated. - socket units: allow creating a udev monitor socket with ListenDevices= or so, with matches, then activate app through that passing socket over diff --git a/man/hostnamectl.xml b/man/hostnamectl.xml index 878837bee1..c120b938c9 100644 --- a/man/hostnamectl.xml +++ b/man/hostnamectl.xml @@ -177,6 +177,14 @@ resulting list is sorted before being stored. To remove all tags, invoke the command with a single empty string argument. + Alternatively, each argument may be prefixed with + or - + to incrementally update the configured tags rather than replacing them: an argument prefixed with + + adds all of its (colon-separated) tags to the list, one prefixed with + - removes them. For example, hostnamectl tags +foo:bar -quux:ooo + adds the tags foo and bar and removes quux + and ooo. The prefix applies to the whole argument, i.e. to every tag listed within + it. Prefixed and unprefixed arguments may not be combined in a single invocation. + 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 diff --git a/man/org.freedesktop.hostname1.xml b/man/org.freedesktop.hostname1.xml index d5571de207..67b52e1ed4 100644 --- a/man/org.freedesktop.hostname1.xml +++ b/man/org.freedesktop.hostname1.xml @@ -57,6 +57,8 @@ node /org/freedesktop/hostname1 { SetLocation(in s location, in b interactive); SetTags(in as tags); + AddAndRemoveTags(in as add, + in as remove); GetProductUUID(in b interactive, out ay uuid); GetHardwareSerial(out s serial); @@ -146,6 +148,8 @@ node /org/freedesktop/hostname1 { + + @@ -385,6 +389,13 @@ node /org/freedesktop/hostname1 { alphanumeric characters, - and .). Passing an empty list removes the TAGS= field from /etc/machine-info. + AddAndRemoveTags() incrementally updates the Tags + property instead of replacing it. It takes two lists of machine tags: the tags in add + are added to the current list, and the tags in remove are dropped from it (the + resulting list is (current ∪ add) \ remove, sorted and deduplicated). This is useful + when several independent agents each manage a subset of the tags without racing on a read-modify-write + of the full list. Each tag must be a valid machine tag, as for SetTags(). + PrettyHostname, IconName, Chassis, Deployment, Location, and Tags are stored in /etc/machine-info. See @@ -421,7 +432,8 @@ node /org/freedesktop/hostname1 { SetStaticHostname() and SetPrettyHostname() it is org.freedesktop.hostname1.set-static-hostname. For SetIconName(), SetChassis(), SetDeployment(), - SetLocation() and SetTags() it is + SetLocation(), SetTags() and + AddAndRemoveTags() it is org.freedesktop.hostname1.set-machine-info. @@ -527,6 +539,7 @@ node /org/freedesktop/hostname1 { OperatingSystemFancyName was added in version 260. GetMachineInfo(), Tags and SetTags() were added in version 261. + AddAndRemoveTags() was added in version 262. diff --git a/man/rules/meson.build b/man/rules/meson.build index 92238b717a..a3d8500cc6 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1107,6 +1107,7 @@ manpages = [ ['systemd-loop@.service', '8', [], ''], ['systemd-machine-id-commit.service', '8', [], ''], ['systemd-machine-id-setup', '1', [], ''], + ['systemd-machine-tag@.service', '8', [], 'ENABLE_HOSTNAMED'], ['systemd-machined.service', '8', ['systemd-machined'], 'ENABLE_MACHINED'], ['systemd-makefs@.service', '8', diff --git a/man/systemd-machine-tag@.service.xml b/man/systemd-machine-tag@.service.xml new file mode 100644 index 0000000000..b5bc1d81c7 --- /dev/null +++ b/man/systemd-machine-tag@.service.xml @@ -0,0 +1,65 @@ + + + + + + + + + systemd-machine-tag@.service + systemd + + + + systemd-machine-tag@.service + 8 + + + + systemd-machine-tag@.service + Add a machine tag to the host + + + + systemd-machine-tag@.service + + + + Description + + systemd-machine-tag@TAG.service is a service + template that adds the machine tag TAG (taken from the instance name) to the + host's list of machine tags. Whenever an instance of this template is pulled in, the corresponding tag is + added; the operation is idempotent, so pulling it in repeatedly has no further effect. + + The primary use of this template is automatic, hardware-driven tagging: the + SYSTEMD_MACHINE_TAG udev property, when set on a device, causes the matching instance + of this template to be pulled in (via SYSTEMD_WANTS=), so that plugging in or + detecting a specific device automatically tags the host. See + systemd.device5 for + details on the SYSTEMD_WANTS= mechanism. + + Machine tags may be inspected and edited with the tags verb of + hostnamectl1, and are + stored in the TAGS= field of + machine-info5. Each + tag must be 1…255 characters long and consist only of ASCII alphanumeric characters, + - and .. + + + + + + See Also + + systemd1 + hostnamectl1 + systemd-hostnamed.service8 + machine-info5 + udev7 + + + + diff --git a/rules.d/99-systemd.rules.in b/rules.d/99-systemd.rules.in index e7b6947e81..fb7d6462af 100644 --- a/rules.d/99-systemd.rules.in +++ b/rules.d/99-systemd.rules.in @@ -98,4 +98,7 @@ SUBSYSTEM=="tpm", KERNEL=="tpm[0-9]*", TAG+="systemd" SUBSYSTEM=="block", ENV{ID_CDROM}=="1", ENV{ID_PART_GPT_AUTO_ROOT_DISK_NEEDS_LOOP}=="1", \ ENV{SYSTEMD_WANTS}+="systemd-loop@.service" +# A device carrying the SYSTEMD_MACHINE_TAG property auto-tags the host with the specified machine tag. +ENV{SYSTEMD_MACHINE_TAG}=="?*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="systemd-machine-tag@$env{SYSTEMD_MACHINE_TAG}.service" + LABEL="systemd_end" diff --git a/src/basic/strv.c b/src/basic/strv.c index 4044a129c6..e4f9373f8a 100644 --- a/src/basic/strv.c +++ b/src/basic/strv.c @@ -812,16 +812,14 @@ bool strv_is_uniq(char * const *l) { } char** strv_remove(char **l, const char *s) { - char **f, **t; - if (!l) return NULL; assert(s); - /* Drops every occurrence of s in the string list, edits - * in-place. */ + /* Drops every occurrence of s in the string list, edits in-place. */ + char **f, **t; for (f = t = l; *f; f++) if (streq(*f, s)) free(*f); @@ -832,6 +830,17 @@ char** strv_remove(char **l, const char *s) { return l; } +char** strv_remove_strv(char **l, char *const*ll) { + + if (strv_isempty(l)) + return l; + + STRV_FOREACH(i, ll) + strv_remove(l, *i); + + return l; +} + bool strv_overlap(char * const *a, char * const *b) { POINTER_MAY_BE_NULL(a); POINTER_MAY_BE_NULL(b); diff --git a/src/basic/strv.h b/src/basic/strv.h index cf6fa158b6..7bac1b66fa 100644 --- a/src/basic/strv.h +++ b/src/basic/strv.h @@ -85,6 +85,7 @@ int strv_consume_pair(char ***l, char *a, char *b); int strv_consume_prepend(char ***l, char *value); char** strv_remove(char **l, const char *s); +char** strv_remove_strv(char **l, char *const*ll); char** strv_uniq(char **l); bool strv_is_uniq(char * const *l) _pure_; diff --git a/src/hostname/hostnamectl.c b/src/hostname/hostnamectl.c index d52580fbf5..a6ecad5178 100644 --- a/src/hostname/hostnamectl.c +++ b/src/hostname/hostnamectl.c @@ -772,25 +772,75 @@ static int verb_get_or_set_tags(int argc, char *argv[], uintptr_t _data, void *u return 0; } - _cleanup_strv_free_ char **l = NULL; + _cleanup_strv_free_ char **plain = NULL, **add = NULL, **remove = NULL; + bool prefixed = false, unprefixed = false; for (int i = 1; i < argc; i++) { - r = strv_split_and_extend(&l, argv[i], ":", /* filter_duplicates= */ true); - if (r < 0) + const char *e = argv[i]; + char ***dest; + + /* The first character of each argument selects the mode for all (colon-separated) tags in + * that argument: a leading '+' adds them, a leading '-' removes them, and otherwise the + * argument lists tags to set verbatim. Setting and adding/removing may not be mixed. */ + if (e[0] == '+') { + dest = &add; + e++; + prefixed = true; + } else if (e[0] == '-') { + dest = &remove; + e++; + prefixed = true; + } else { + dest = &plain; + unprefixed = true; + } + + if (isempty(e)) { + if (dest == &plain && !*dest) { + *dest = new0(char*, 1); + if (!*dest) + return log_oom(); + } + } else if (strv_split_and_extend(dest, e, ":", /* filter_duplicates= */ true) < 0) return log_oom(); } - strv_sort(l); + if (prefixed && unprefixed) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Plain tags may not be combined with '+'/'-' prefixed tags."); (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 bus_log_create_error(r); - r = sd_bus_message_append_strv(m, l); - if (r < 0) - return bus_log_create_error(r); + if (!prefixed) { + /* No prefixes used: replace the whole tag list with the given tags (possibly empty, which + * clears all tags). */ + strv_sort(plain); + + r = bus_message_new_method_call(bus, &m, bus_hostname, "SetTags"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_strv(m, plain); + if (r < 0) + return bus_log_create_error(r); + } else { + /* Prefixes used: incrementally add and/or remove tags. */ + strv_sort(add); + strv_sort(remove); + + r = bus_message_new_method_call(bus, &m, bus_hostname, "AddAndRemoveTags"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_strv(m, add); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_strv(m, remove); + if (r < 0) + return bus_log_create_error(r); + } r = sd_bus_call(bus, m, /* usec= */ 0, &error, /* ret_reply= */ NULL); if (r < 0) diff --git a/src/hostname/hostnamed.c b/src/hostname/hostnamed.c index 73c0f2e8ef..098ce80500 100644 --- a/src/hostname/hostnamed.c +++ b/src/hostname/hostnamed.c @@ -17,6 +17,7 @@ #include "bus-object.h" #include "bus-polkit.h" #include "bus-util.h" +#include "common-signal.h" #include "constants.h" #include "daemon-util.h" #include "device-private.h" @@ -36,7 +37,6 @@ #include "nulstr-util.h" #include "os-util.h" #include "parse-util.h" -#include "path-util.h" #include "service-util.h" #include "stat-util.h" #include "string-util.h" @@ -541,7 +541,7 @@ static int get_firmware_date(Context *c, usec_t *ret) { return 0; } -static const char* valid_chassis(const char *chassis) { +static const char* chassis_is_valid(const char *chassis) { assert(chassis); return nulstr_get( @@ -558,10 +558,29 @@ static const char* valid_chassis(const char *chassis) { chassis); } -static bool valid_deployment(const char *deployment) { +static bool deployment_is_valid(const char *deployment) { assert(deployment); - return in_charset(deployment, VALID_DEPLOYMENT_CHARS); + return !isempty(deployment) && + in_charset(deployment, VALID_DEPLOYMENT_CHARS); +} + +static bool pretty_hostname_is_valid(const char *pretty_hostname) { + assert(pretty_hostname); + + return string_is_safe(pretty_hostname, STRING_ALLOW_BACKSLASHES|STRING_ALLOW_QUOTES|STRING_ALLOW_GLOBS); +} + +static bool icon_name_is_valid(const char *icon_name) { + assert(icon_name); + + return string_is_safe(icon_name, STRING_FILENAME); +} + +static bool location_is_valid(const char *location) { + assert(location); + + return string_is_safe(location, STRING_ALLOW_BACKSLASHES|STRING_ALLOW_QUOTES|STRING_ALLOW_GLOBS); } static const char* fallback_chassis_by_virtualization(void) { @@ -710,7 +729,7 @@ static const char* fallback_chassis_by_device_tree(Context *c) { * * https://github.com/devicetree-org/devicetree-specification/blob/master/source/chapter3-devicenodes.rst */ - chassis = valid_chassis(type); + chassis = chassis_is_valid(type); if (!chassis) log_debug("Invalid device-tree chassis type \"%s\", ignoring.", type); return chassis; @@ -828,7 +847,7 @@ static int context_write_data_static_hostname(Context *c) { if (isempty(c->data[PROP_STATIC_HOSTNAME])) { if (unlink(etc_hostname()) < 0 && errno != ENOENT) - return -errno; + return log_error_errno(errno, "Failed to remove '%s': %m", etc_hostname()); TAKE_PTR(s); return 0; @@ -836,7 +855,7 @@ static int context_write_data_static_hostname(Context *c) { r = write_string_file(etc_hostname(), c->data[PROP_STATIC_HOSTNAME], WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_LABEL); if (r < 0) - return r; + return log_error_errno(r, "Failed to write '%s': %m", etc_hostname()); TAKE_PTR(s); return 0; @@ -863,19 +882,18 @@ static int context_write_data_machine_info(Context *c) { r = load_env_file(NULL, etc_machine_info(), &l); if (r < 0 && r != -ENOENT) - return r; + return log_error_errno(r, "Failed to read '%s': %m", etc_machine_info()); 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])); - if (r < 0) - return r; + if (strv_env_assign(&l, name[p], empty_to_null(c->data[p])) < 0) + return log_oom(); } if (strv_isempty(l)) { if (unlink(etc_machine_info()) < 0 && errno != ENOENT) - return -errno; + return log_error_errno(errno, "Failed to unlink '%s': %m", etc_machine_info()); TAKE_PTR(s); return 0; @@ -888,7 +906,7 @@ static int context_write_data_machine_info(Context *c) { l, WRITE_ENV_FILE_LABEL); if (r < 0) - return r; + return log_error_errno(r, "Failed to write '%s': %m", etc_machine_info()); TAKE_PTR(s); return 0; @@ -1348,7 +1366,7 @@ static int property_get_vsock_cid( return sd_bus_message_append(reply, "u", (uint32_t) local_cid); } -static int validate_and_substitute_hostname(const char *name, char **ret_substituted, sd_bus_error *error) { +static int validate_and_substitute_hostname(const char *name, char **ret_substituted) { int r; assert(ret_substituted); @@ -1367,12 +1385,24 @@ static int validate_and_substitute_hostname(const char *name, char **ret_substit return log_error_errno(r, "Failed to substitute wildcards in hostname: %m"); if (!hostname_is_valid(substituted, 0)) - return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid hostname '%s'", name); + return -EUCLEAN; *ret_substituted = TAKE_PTR(substituted); return 1; } +static int bus_validate_and_substitute_hostname(const char *name, char **ret_substituted, sd_bus_error *error) { + int r; + + assert(ret_substituted); + + r = validate_and_substitute_hostname(name, ret_substituted); + if (r == -EUCLEAN) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid hostname '%s'", name); + + return r; +} + static int method_set_hostname(sd_bus_message *m, void *userdata, sd_bus_error *error) { Context *c = ASSERT_PTR(userdata); const char *name; @@ -1390,7 +1420,7 @@ static int method_set_hostname(sd_bus_message *m, void *userdata, sd_bus_error * * we might want to adjust hostname source information even if the actual hostname is unchanged. */ _cleanup_free_ char *substituted = NULL; - r = validate_and_substitute_hostname(name, &substituted, error); + r = bus_validate_and_substitute_hostname(name, &substituted, error); if (r < 0) return r; @@ -1441,7 +1471,7 @@ static int method_set_static_hostname(sd_bus_message *m, void *userdata, sd_bus_ return sd_bus_reply_method_return(m, NULL); _cleanup_free_ char *substituted = NULL; - r = validate_and_substitute_hostname(name, &substituted, error); + r = bus_validate_and_substitute_hostname(name, &substituted, error); if (r < 0) return r; @@ -1466,7 +1496,6 @@ static int method_set_static_hostname(sd_bus_message *m, void *userdata, sd_bus_ r = context_write_data_static_hostname(c); if (r < 0) { - log_error_errno(r, "Failed to write static hostname: %m"); if (ERRNO_IS_PRIVILEGE(r)) return sd_bus_error_set(error, BUS_ERROR_FILE_IS_PROTECTED, "Not allowed to update /etc/hostname."); if (r == -EROFS) @@ -1475,10 +1504,8 @@ static int method_set_static_hostname(sd_bus_message *m, void *userdata, sd_bus_ } r = context_update_kernel_hostname(c, /* transient_hostname= */ NULL); - if (r < 0) { - log_error_errno(r, "Failed to set hostname: %m"); + if (r < 0) return sd_bus_error_set_errnof(error, r, "Failed to set hostname: %m"); - } (void) sd_bus_emit_properties_changed(sd_bus_message_get_bus(m), "/org/freedesktop/hostname1", "org.freedesktop.hostname1", @@ -1510,15 +1537,15 @@ static int set_machine_info(Context *c, sd_bus_message *m, int prop, sd_bus_mess /* The icon name might ultimately be used as file * name, so better be safe than sorry */ - if (prop == PROP_ICON_NAME && !filename_is_valid(name)) + if (prop == PROP_ICON_NAME && !icon_name_is_valid(name)) return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid icon name '%s'", name); - if (prop == PROP_PRETTY_HOSTNAME && string_has_cc(name, NULL)) + if (prop == PROP_PRETTY_HOSTNAME && !pretty_hostname_is_valid(name)) return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid pretty hostname '%s'", name); - if (prop == PROP_CHASSIS && !valid_chassis(name)) + if (prop == PROP_CHASSIS && !chassis_is_valid(name)) return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid chassis '%s'", name); - if (prop == PROP_DEPLOYMENT && !valid_deployment(name)) + if (prop == PROP_DEPLOYMENT && !deployment_is_valid(name)) return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid deployment '%s'", name); - if (prop == PROP_LOCATION && string_has_cc(name, NULL)) + if (prop == PROP_LOCATION && !location_is_valid(name)) return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid location '%s'", name); } @@ -1544,7 +1571,6 @@ static int set_machine_info(Context *c, sd_bus_message *m, int prop, sd_bus_mess 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) @@ -1590,6 +1616,52 @@ 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 context_store_tags(Context *c, char **tags) { + int r; + + assert(c); + + /* Persists the given machine tags (which must already be validated, sorted and deduplicated) to + * /etc/machine-info and emits a PropertiesChanged signal on the Tags property. */ + + if (strv_isempty(tags)) + c->data[PROP_TAGS] = mfree(c->data[PROP_TAGS]); + else { + _cleanup_free_ char *j = strv_join(tags, ":"); + if (!j) + return log_oom(); + + free_and_replace(c->data[PROP_TAGS], j); + } + + r = context_write_data_machine_info(c); + if (r < 0) + return r; + + log_info("Changed tags to '%s'", strempty(c->data[PROP_TAGS])); + + (void) sd_bus_emit_properties_changed( + c->bus, + "/org/freedesktop/hostname1", + "org.freedesktop.hostname1", + "Tags", + NULL); + + return 0; +} + +static int bus_error_from_tags_write(sd_bus_error *error, int r) { + assert(error); + assert(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"); +} + static int method_set_tags(sd_bus_message *m, void *userdata, sd_bus_error *error) { Context *c = ASSERT_PTR(userdata); int r; @@ -1615,7 +1687,12 @@ static int method_set_tags(sd_bus_message *m, void *userdata, sd_bus_error *erro context_read_machine_info(c); - if (streq_ptr(empty_to_null(j), empty_to_null(c->data[PROP_TAGS]))) + _cleanup_strv_free_ char **current = NULL; + r = machine_tags_from_string(c->data[PROP_TAGS], /* graceful= */ true, ¤t); + if (r < 0) + return log_error_errno(r, "Failed to parse current machine tags: %m"); + + if (strv_equal(current, tags)) return sd_bus_reply_method_return(m, NULL); r = bus_verify_polkit_async_full( @@ -1631,29 +1708,98 @@ static int method_set_tags(sd_bus_message *m, void *userdata, sd_bus_error *erro 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_store_tags(c, tags); + if (r < 0) + return bus_error_from_tags_write(error, r); - 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"); + return sd_bus_reply_method_return(m, NULL); +} + +static int machine_tags_add_remove(char * const *base, char * const *add, char * const *remove, char ***ret) { + int r; + + assert(ret); + + /* Computes the resulting machine tag list when adding 'add' to and removing 'remove' from the 'base' + * list, i.e. (base ∪ add) \ remove, sorted and deduplicated. Shared by the D-Bus AddAndRemoveTags() + * and Varlink SetTags() implementations. */ + + _cleanup_strv_free_ char **tags = strv_copy(base); + if (!tags) + return -ENOMEM; + + r = strv_extend_strv(&tags, add, /* filter_duplicates= */ true); + if (r < 0) + return r; + + strv_remove_strv(tags, remove); + + strv_sort_uniq(tags); + + if (strv_length(tags) > MACHINE_TAGS_MAX) + return -E2BIG; + + *ret = TAKE_PTR(tags); + return 0; +} + +static int method_add_and_remove_tags(sd_bus_message *m, void *userdata, sd_bus_error *error) { + Context *c = ASSERT_PTR(userdata); + int r; + + assert(m); + + _cleanup_strv_free_ char **add = NULL, **remove = NULL; + r = sd_bus_message_read_strv(m, &add); + if (r < 0) + return r; + r = sd_bus_message_read_strv(m, &remove); + if (r < 0) + return r; + + if (!machine_tag_list_is_valid(add)) { + _cleanup_free_ char *j = strv_join(add, ":"); + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid tags to add '%s'", strna(j)); + } + if (!machine_tag_list_is_valid(remove)) { + _cleanup_free_ char *j = strv_join(remove, ":"); + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid tags to remove '%s'", strna(j)); } - log_info("Changed tags to '%s'", strempty(c->data[PROP_TAGS])); + context_read_machine_info(c); - (void) sd_bus_emit_properties_changed( - sd_bus_message_get_bus(m), - "/org/freedesktop/hostname1", - "org.freedesktop.hostname1", - "Tags", - NULL); + /* Start from the current tags, add the requested ones, then drop the ones to be removed. */ + _cleanup_strv_free_ char **current = NULL; + r = machine_tags_from_string(c->data[PROP_TAGS], /* graceful= */ true, ¤t); + if (r < 0) + return log_error_errno(r, "Failed to parse current machine tags: %m"); + + _cleanup_strv_free_ char **tags = NULL; + r = machine_tags_add_remove(current, add, remove, &tags); + if (r == -E2BIG) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Too many machine tags specified."); + if (r < 0) + return log_oom(); + + if (strv_equal(current, 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 */ + + r = context_store_tags(c, tags); + if (r < 0) + return bus_error_from_tags_write(error, r); return sd_bus_reply_method_return(m, NULL); } @@ -2014,6 +2160,11 @@ static const sd_bus_vtable hostname_vtable[] = { SD_BUS_NO_RESULT, method_set_tags, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("AddAndRemoveTags", + SD_BUS_ARGS("as", add, "as", remove), + SD_BUS_NO_RESULT, + method_add_and_remove_tags, + SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD_WITH_ARGS("GetProductUUID", SD_BUS_ARGS("b", interactive), SD_BUS_RESULT("ay", uuid), @@ -2109,6 +2260,333 @@ static int vl_method_describe(sd_varlink *link, sd_json_variant *parameters, sd_ return sd_varlink_reply(link, v); } +static int vl_validate_and_substitute_hostname(sd_varlink *link, const char *name, char **ret_substituted) { + int r; + + assert(link); + assert(ret_substituted); + + r = validate_and_substitute_hostname(name, ret_substituted); + if (r == -EUCLEAN) + return sd_varlink_error_invalid_parameter_name(link, "newValue"); + + return r; +} + +static int vl_method_set_hostname(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + static const sd_json_dispatch_field dispatch_table[] = { + { "newValue", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, 0, SD_JSON_NULLABLE }, + VARLINK_DISPATCH_POLKIT_FIELD, + {} + }; + + Context *c = ASSERT_PTR(userdata); + int r; + + assert(link); + assert(parameters); + + _cleanup_free_ char *value = NULL; + r = sd_varlink_dispatch(link, parameters, dispatch_table, &value); + if (r != 0) + return r; + + const char *name = empty_to_null(value); + + /* We always go through with the procedure below without comparing to the current hostname, because + * we might want to adjust hostname source information even if the actual hostname is unchanged. */ + + _cleanup_free_ char *substituted = NULL; + r = vl_validate_and_substitute_hostname(link, name, &substituted); + if (r < 0) + return r; + + name = substituted; + + r = varlink_verify_polkit_async( + link, + c->bus, + "org.freedesktop.hostname1.set-hostname", + /* details= */ NULL, + &c->polkit_registry); + if (r <= 0) + return r; + + context_read_etc_hostname(c); + + r = context_update_kernel_hostname(c, name); + if (r < 0) + return r; + if (r > 0) + (void) sd_bus_emit_properties_changed(c->bus, + "/org/freedesktop/hostname1", "org.freedesktop.hostname1", + "Hostname", "HostnameSource", NULL); + + return sd_varlink_reply(link, NULL); +} + +static int vl_method_set_static_hostname(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + static const sd_json_dispatch_field dispatch_table[] = { + { "newValue", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, 0, SD_JSON_NULLABLE }, + VARLINK_DISPATCH_POLKIT_FIELD, + {} + }; + + Context *c = ASSERT_PTR(userdata); + int r; + + assert(link); + assert(parameters); + + _cleanup_free_ char *value = NULL; + r = sd_varlink_dispatch(link, parameters, dispatch_table, &value); + if (r != 0) + return r; + + const char *name = empty_to_null(value); + + context_read_etc_hostname(c); + + if (streq_ptr(name, c->data[PROP_STATIC_HOSTNAME])) + return sd_varlink_reply(link, NULL); + + _cleanup_free_ char *substituted = NULL; + r = vl_validate_and_substitute_hostname(link, name, &substituted); + if (r < 0) + return r; + + r = varlink_verify_polkit_async( + link, + c->bus, + "org.freedesktop.hostname1.set-static-hostname", + /* details= */ NULL, + &c->polkit_registry); + if (r <= 0) + return r; + + r = free_and_strdup_warn(&c->data[PROP_STATIC_HOSTNAME], name); + if (r < 0) + return r; + + free_and_replace(c->data[PROP_STATIC_HOSTNAME_SUBSTITUTED_WILDCARDS], substituted); + + r = context_write_data_static_hostname(c); + if (r < 0) + return r; + + r = context_update_kernel_hostname(c, /* transient_hostname= */ NULL); + if (r < 0) + return r; + + (void) sd_bus_emit_properties_changed(c->bus, + "/org/freedesktop/hostname1", "org.freedesktop.hostname1", + "StaticHostname", "Hostname", "HostnameSource", NULL); + + return sd_varlink_reply(link, NULL); +} + +static int vl_set_machine_info(sd_varlink *link, sd_json_variant *parameters, void *userdata, HostProperty prop) { + static const sd_json_dispatch_field dispatch_table[] = { + { "newValue", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, 0, SD_JSON_NULLABLE }, + VARLINK_DISPATCH_POLKIT_FIELD, + {} + }; + + Context *c = ASSERT_PTR(userdata); + int r; + + assert(link); + assert(parameters); + + _cleanup_free_ char *value = NULL; + r = sd_varlink_dispatch(link, parameters, dispatch_table, &value); + if (r != 0) + return r; + + const char *name = empty_to_null(value), + *polkit_action = "org.freedesktop.hostname1.set-machine-info", + *bus_property, *human_name; + + switch (prop) { + + case PROP_PRETTY_HOSTNAME: + if (name && !pretty_hostname_is_valid(name)) + return sd_varlink_error_invalid_parameter_name(link, "newValue"); + bus_property = "PrettyHostname"; + human_name = "pretty hostname"; + /* Since the pretty hostname should always be changed at the same time as the static one, use + * the same policy action for both... */ + polkit_action = "org.freedesktop.hostname1.set-static-hostname"; + break; + + case PROP_ICON_NAME: + /* The icon name might ultimately be used as file name, so better be safe than sorry. */ + if (name && !icon_name_is_valid(name)) + return sd_varlink_error_invalid_parameter_name(link, "newValue"); + bus_property = "IconName"; + human_name = "icon name"; + break; + + case PROP_CHASSIS: + if (name && !chassis_is_valid(name)) + return sd_varlink_error_invalid_parameter_name(link, "newValue"); + bus_property = "Chassis"; + human_name = "chassis"; + break; + + case PROP_DEPLOYMENT: + if (name && !deployment_is_valid(name)) + return sd_varlink_error_invalid_parameter_name(link, "newValue"); + bus_property = "Deployment"; + human_name = "deployment"; + break; + + case PROP_LOCATION: + if (name && !location_is_valid(name)) + return sd_varlink_error_invalid_parameter_name(link, "newValue"); + bus_property = "Location"; + human_name = "location"; + break; + + default: + assert_not_reached(); + } + + context_read_machine_info(c); + + if (streq_ptr(name, c->data[prop])) + return sd_varlink_reply(link, NULL); + + r = varlink_verify_polkit_async( + link, + c->bus, + polkit_action, + /* details= */ NULL, + &c->polkit_registry); + if (r <= 0) + return r; + + r = free_and_strdup_warn(&c->data[prop], name); + if (r < 0) + return r; + + r = context_write_data_machine_info(c); + if (r < 0) + return r; + + log_info("Changed %s to '%s'", human_name, strna(c->data[prop])); + + (void) sd_bus_emit_properties_changed( + c->bus, + "/org/freedesktop/hostname1", + "org.freedesktop.hostname1", + bus_property, + NULL); + + return sd_varlink_reply(link, NULL); +} + +static int vl_method_set_pretty_hostname(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + return vl_set_machine_info(link, parameters, userdata, PROP_PRETTY_HOSTNAME); +} + +static int vl_method_set_icon_name(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + return vl_set_machine_info(link, parameters, userdata, PROP_ICON_NAME); +} + +static int vl_method_set_chassis(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + return vl_set_machine_info(link, parameters, userdata, PROP_CHASSIS); +} + +static int vl_method_set_deployment(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + return vl_set_machine_info(link, parameters, userdata, PROP_DEPLOYMENT); +} + +static int vl_method_set_location(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + return vl_set_machine_info(link, parameters, userdata, PROP_LOCATION); +} + +typedef struct SetTagsParameters { + char **set; + char **add; + char **remove; +} SetTagsParameters; + +static void set_tags_parameters_done(SetTagsParameters *p) { + assert(p); + + strv_free(p->set); + strv_free(p->add); + strv_free(p->remove); +} + +static int vl_method_set_tags(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { + static const sd_json_dispatch_field dispatch_table[] = { + { "set", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(SetTagsParameters, set), SD_JSON_NULLABLE }, + { "add", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(SetTagsParameters, add), SD_JSON_NULLABLE }, + { "remove", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(SetTagsParameters, remove), SD_JSON_NULLABLE }, + VARLINK_DISPATCH_POLKIT_FIELD, + {} + }; + + Context *c = ASSERT_PTR(userdata); + int r; + + assert(link); + assert(parameters); + + _cleanup_(set_tags_parameters_done) SetTagsParameters p = {}; + r = sd_varlink_dispatch(link, parameters, dispatch_table, &p); + if (r != 0) + return r; + + /* Both an absent 'set' field and an explicit null map to a NULL strv after dispatching, but only the + * former should keep the current tags — an explicit (possibly empty) 'set' resets the list. Hence + * check for the field's presence separately. */ + bool reset = sd_json_variant_by_key(parameters, "set"); + + if (reset && !machine_tag_list_is_valid(p.set)) + return sd_varlink_error_invalid_parameter_name(link, "set"); + if (!machine_tag_list_is_valid(p.add)) + return sd_varlink_error_invalid_parameter_name(link, "add"); + if (!machine_tag_list_is_valid(p.remove)) + return sd_varlink_error_invalid_parameter_name(link, "remove"); + + context_read_machine_info(c); + + _cleanup_strv_free_ char **current = NULL; + r = machine_tags_from_string(c->data[PROP_TAGS], /* graceful= */ true, ¤t); + if (r < 0) + return r; + + /* Use either the explicitly specified tag list or the current one as the basis, then apply the + * additions and removals on top. */ + _cleanup_strv_free_ char **tags = NULL; + r = machine_tags_add_remove(reset ? p.set : current, p.add, p.remove, &tags); + if (r == -E2BIG) + return sd_varlink_error_invalid_parameter_name(link, "add"); + if (r < 0) + return r; + + if (strv_equal(tags, current)) + return sd_varlink_reply(link, NULL); + + r = varlink_verify_polkit_async( + link, + c->bus, + "org.freedesktop.hostname1.set-machine-info", + /* details= */ NULL, + &c->polkit_registry); + if (r <= 0) + return r; + + r = context_store_tags(c, tags); + if (r < 0) + return r; + + return sd_varlink_reply(link, NULL); +} + static int connect_varlink(Context *c) { int r; @@ -2132,10 +2610,18 @@ static int connect_varlink(Context *c) { r = sd_varlink_server_bind_method_many( c->varlink_server, - "io.systemd.Hostname.Describe", vl_method_describe, - "io.systemd.service.Ping", varlink_method_ping, - "io.systemd.service.SetLogLevel", varlink_method_set_log_level, - "io.systemd.service.GetEnvironment", varlink_method_get_environment); + "io.systemd.Hostname.Describe", vl_method_describe, + "io.systemd.Hostname.SetHostname", vl_method_set_hostname, + "io.systemd.Hostname.SetStaticHostname", vl_method_set_static_hostname, + "io.systemd.Hostname.SetPrettyHostname", vl_method_set_pretty_hostname, + "io.systemd.Hostname.SetIconName", vl_method_set_icon_name, + "io.systemd.Hostname.SetChassis", vl_method_set_chassis, + "io.systemd.Hostname.SetDeployment", vl_method_set_deployment, + "io.systemd.Hostname.SetLocation", vl_method_set_location, + "io.systemd.Hostname.SetTags", vl_method_set_tags, + "io.systemd.service.Ping", varlink_method_ping, + "io.systemd.service.SetLogLevel", varlink_method_set_log_level, + "io.systemd.service.GetEnvironment", varlink_method_get_environment); if (r < 0) return log_error_errno(r, "Failed to bind Varlink method calls: %m"); @@ -2207,6 +2693,14 @@ static int run(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to install SIGINT/SIGTERM handlers: %m"); + r = sd_event_add_signal(context.event, /* ret= */ NULL, (SIGRTMIN+18)|SD_EVENT_SIGNAL_PROCMASK, sigrtmin18_handler, /* userdata= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to install SIGRTMIN+18 handler: %m"); + + r = sd_event_add_memory_pressure(context.event, /* ret= */ NULL, /* callback= */ NULL, /* userdata= */ NULL); + if (r < 0) + log_debug_errno(r, "Failed to allocate memory pressure event source, ignoring: %m"); + r = connect_bus(&context); if (r < 0) return r; diff --git a/src/shared/varlink-io.systemd.Hostname.c b/src/shared/varlink-io.systemd.Hostname.c index e06587039b..ca7b2b534b 100644 --- a/src/shared/varlink-io.systemd.Hostname.c +++ b/src/shared/varlink-io.systemd.Hostname.c @@ -6,45 +6,146 @@ static SD_VARLINK_DEFINE_METHOD( Describe, VARLINK_DEFINE_POLKIT_INPUT, + SD_VARLINK_FIELD_COMMENT("The current system hostname."), SD_VARLINK_DEFINE_OUTPUT(Hostname, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("The statically configured hostname, from /etc/hostname. Null if none is configured."), SD_VARLINK_DEFINE_OUTPUT(StaticHostname, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The pretty (free-form, UTF-8) hostname. Null if none is configured."), SD_VARLINK_DEFINE_OUTPUT(PrettyHostname, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The fallback hostname used when neither a static nor a transient hostname is set."), SD_VARLINK_DEFINE_OUTPUT(DefaultHostname, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("Indicates where the current hostname originates from, one of 'static', 'transient' or 'default'."), SD_VARLINK_DEFINE_OUTPUT(HostnameSource, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("The name of an icon representing this system, following the XDG icon naming spec. Null if none is configured."), SD_VARLINK_DEFINE_OUTPUT(IconName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The chassis type of this system, e.g. 'desktop', 'laptop', 'server', 'vm', … Null if unknown."), SD_VARLINK_DEFINE_OUTPUT(Chassis, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("An unique identifier of the system chassis."), SD_VARLINK_DEFINE_OUTPUT(ChassisAssetTag, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The deployment environment of this system, e.g. 'production' or 'staging'. Null if none is configured."), SD_VARLINK_DEFINE_OUTPUT(Deployment, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("A human-readable description of the physical location of this system. Null if none is configured."), SD_VARLINK_DEFINE_OUTPUT(Location, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The operating system kernel name, as reported by uname(2), e.g. 'Linux'."), SD_VARLINK_DEFINE_OUTPUT(KernelName, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("The kernel release string, as reported by uname(2)."), SD_VARLINK_DEFINE_OUTPUT(KernelRelease, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("The kernel version string, as reported by uname(2)."), SD_VARLINK_DEFINE_OUTPUT(KernelVersion, SD_VARLINK_STRING, 0), SD_VARLINK_FIELD_COMMENT("'Pretty' name of the OS. This is the primary OS identifier that is suitable for presentation to the user. Typically includes a version too, but doesn't have to."), SD_VARLINK_DEFINE_OUTPUT(OperatingSystemPrettyName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("'Fancy' name of the OS; may contain non-ASCII Unicode chars, as well as basic ANSI sequences. This is similar to 'OperatingSystemPrettyName', but is preferably used on terminals that support ANSI sequences and full Unicode."), SD_VARLINK_DEFINE_OUTPUT(OperatingSystemFancyName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The CPE name of the OS, from the CPE_NAME= field of os-release."), SD_VARLINK_DEFINE_OUTPUT(OperatingSystemCPEName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The home URL of the OS, from the HOME_URL= field of os-release."), SD_VARLINK_DEFINE_OUTPUT(OperatingSystemHomeURL, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The end-of-support time of the OS, in µs since the UNIX epoch. Null if not known."), SD_VARLINK_DEFINE_OUTPUT(OperatingSystemSupportEnd, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The full contents of os-release, as an array of 'KEY=VALUE' strings."), SD_VARLINK_DEFINE_OUTPUT(OperatingSystemReleaseData, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("The OS image identifier, from the IMAGE_ID= field of os-release."), SD_VARLINK_DEFINE_OUTPUT(OperatingSystemImageID, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The OS image version, from the IMAGE_VERSION= field of os-release."), SD_VARLINK_DEFINE_OUTPUT(OperatingSystemImageVersion, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The full contents of /etc/machine-info, as an array of 'KEY=VALUE' strings."), SD_VARLINK_DEFINE_OUTPUT(MachineInformationData, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("The hardware vendor of this system. Null if not known."), SD_VARLINK_DEFINE_OUTPUT(HardwareVendor, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The hardware model of this system. Null if not known."), SD_VARLINK_DEFINE_OUTPUT(HardwareModel, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The hardware serial number of this system. Reading it requires privileges. Null if not known."), SD_VARLINK_DEFINE_OUTPUT(HardwareSerial, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The hardware SKU (stock keeping unit) of this system. Null if not known."), SD_VARLINK_DEFINE_OUTPUT(HardwareSKU, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The hardware version of this system. Null if not known."), SD_VARLINK_DEFINE_OUTPUT(HardwareVersion, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The firmware version of this system. Null if not known."), SD_VARLINK_DEFINE_OUTPUT(FirmwareVersion, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The firmware vendor of this system. Null if not known."), SD_VARLINK_DEFINE_OUTPUT(FirmwareVendor, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The firmware build date, in µs since the UNIX epoch. Null if not known."), SD_VARLINK_DEFINE_OUTPUT(FirmwareDate, SD_VARLINK_INT, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The 128-bit machine ID of this system, formatted as a hexadecimal string."), SD_VARLINK_DEFINE_OUTPUT(MachineID, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("The 128-bit boot ID of the current boot, formatted as a hexadecimal string."), SD_VARLINK_DEFINE_OUTPUT(BootID, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("The DMI product UUID of this system. Reading it requires privileges. Null if not known."), SD_VARLINK_DEFINE_OUTPUT(ProductUUID, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("The AF_VSOCK context ID (CID) of this system. Null if not known."), SD_VARLINK_DEFINE_OUTPUT(VSockCID, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); +static SD_VARLINK_DEFINE_METHOD( + SetHostname, + SD_VARLINK_FIELD_COMMENT("The transient (runtime) hostname to set. If null the transient hostname is reset, so that the static hostname is used again."), + SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + VARLINK_DEFINE_POLKIT_INPUT); + +static SD_VARLINK_DEFINE_METHOD( + SetStaticHostname, + SD_VARLINK_FIELD_COMMENT("The static hostname to set, stored in /etc/hostname. If null the static hostname is removed."), + SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + VARLINK_DEFINE_POLKIT_INPUT); + +static SD_VARLINK_DEFINE_METHOD( + SetPrettyHostname, + SD_VARLINK_FIELD_COMMENT("The pretty (free-form, UTF-8) hostname to set. If null the pretty hostname is removed."), + SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + VARLINK_DEFINE_POLKIT_INPUT); + +static SD_VARLINK_DEFINE_METHOD( + SetIconName, + SD_VARLINK_FIELD_COMMENT("The icon name to set. If null the icon name is removed."), + SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + VARLINK_DEFINE_POLKIT_INPUT); + +static SD_VARLINK_DEFINE_METHOD( + SetChassis, + SD_VARLINK_FIELD_COMMENT("The chassis type to set. If null the chassis type is removed."), + SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + VARLINK_DEFINE_POLKIT_INPUT); + +static SD_VARLINK_DEFINE_METHOD( + SetDeployment, + SD_VARLINK_FIELD_COMMENT("The deployment environment to set. If null the deployment is removed."), + SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + VARLINK_DEFINE_POLKIT_INPUT); + +static SD_VARLINK_DEFINE_METHOD( + SetLocation, + SD_VARLINK_FIELD_COMMENT("The physical location to set. If null the location is removed."), + SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + VARLINK_DEFINE_POLKIT_INPUT); + +static SD_VARLINK_DEFINE_METHOD( + SetTags, + SD_VARLINK_FIELD_COMMENT("If specified, the machine tag list is first reset to exactly these tags, before the 'add' and 'remove' fields are applied. If null, the current tag list is used as basis instead."), + SD_VARLINK_DEFINE_INPUT(set, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("Machine tags to add to the list."), + SD_VARLINK_DEFINE_INPUT(add, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), + SD_VARLINK_FIELD_COMMENT("Machine tags to remove from the list."), + SD_VARLINK_DEFINE_INPUT(remove, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY), + VARLINK_DEFINE_POLKIT_INPUT); + SD_VARLINK_DEFINE_INTERFACE( io_systemd_Hostname, "io.systemd.Hostname", - &vl_method_Describe); + SD_VARLINK_INTERFACE_COMMENT("APIs for querying and changing the system hostname and related machine metadata."), + SD_VARLINK_SYMBOL_COMMENT("Returns hostname, kernel, OS, hardware, firmware and other machine metadata in one call."), + &vl_method_Describe, + SD_VARLINK_SYMBOL_COMMENT("Sets the transient (runtime) hostname."), + &vl_method_SetHostname, + SD_VARLINK_SYMBOL_COMMENT("Sets the static hostname, stored in /etc/hostname."), + &vl_method_SetStaticHostname, + SD_VARLINK_SYMBOL_COMMENT("Sets the pretty (free-form, UTF-8) hostname."), + &vl_method_SetPrettyHostname, + SD_VARLINK_SYMBOL_COMMENT("Sets the icon name representing this system."), + &vl_method_SetIconName, + SD_VARLINK_SYMBOL_COMMENT("Sets the chassis type of this system."), + &vl_method_SetChassis, + SD_VARLINK_SYMBOL_COMMENT("Sets the deployment environment of this system."), + &vl_method_SetDeployment, + SD_VARLINK_SYMBOL_COMMENT("Sets the physical location of this system."), + &vl_method_SetLocation, + SD_VARLINK_SYMBOL_COMMENT("Edits the machine tag list: optionally resets it to 'set', then adds 'add' and removes 'remove'."), + &vl_method_SetTags); diff --git a/src/test/test-strv.c b/src/test/test-strv.c index 283ae5c865..386c54f787 100644 --- a/src/test/test-strv.c +++ b/src/test/test-strv.c @@ -510,6 +510,26 @@ TEST(strv_overlap) { assert_se(!strv_overlap((char **)input_table, (char**)input_table_unique)); } +TEST(strv_remove_strv) { + _cleanup_strv_free_ char **l = NULL; + + ASSERT_OK(strv_extend_strv(&l, STRV_MAKE("one", "two", "three", "four"), false)); + + ASSERT_PTR_EQ(strv_remove_strv(l, STRV_MAKE("two", "four")), l); + ASSERT_TRUE(strv_equal(l, STRV_MAKE("one", "three"))); + + /* Removing entries not present is a no-op. */ + ASSERT_PTR_EQ(strv_remove_strv(l, STRV_MAKE("nope")), l); + ASSERT_TRUE(strv_equal(l, STRV_MAKE("one", "three"))); + + /* Empty removal list leaves the list untouched. */ + ASSERT_PTR_EQ(strv_remove_strv(l, STRV_EMPTY), l); + ASSERT_TRUE(strv_equal(l, STRV_MAKE("one", "three"))); + + /* Removing from an empty/NULL list is fine. */ + ASSERT_NULL(strv_remove_strv(NULL, STRV_MAKE("one"))); +} + TEST(strv_sort) { const char* input_table[] = { "durian", diff --git a/test/units/TEST-71-HOSTNAME.sh b/test/units/TEST-71-HOSTNAME.sh index 9bd743dcaa..369bed5f6e 100755 --- a/test/units/TEST-71-HOSTNAME.sh +++ b/test/units/TEST-71-HOSTNAME.sh @@ -388,6 +388,123 @@ EOF assert_rc 1 busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s CHASSIS } +testcase_tags() { + if [[ -f /etc/machine-info ]]; then + cp /etc/machine-info /tmp/machine-info.bak + fi + + trap restore_machine_info RETURN + + # Start from a clean slate. + hostnamectl tags "" + assert_eq "$(hostnamectl tags)" "" + + # Plain arguments replace the whole list; each argument may itself be a colon-separated list, and the + # result is deduplicated and sorted. + hostnamectl tags foo bar:baz + assert_eq "$(hostnamectl tags)" "bar:baz:foo" + + # A leading '+' adds, a leading '-' removes, and the prefix applies to all (colon-separated) tags of + # the argument. + hostnamectl tags +quux + assert_eq "$(hostnamectl tags)" "bar:baz:foo:quux" + hostnamectl tags -- -foo + assert_eq "$(hostnamectl tags)" "bar:baz:quux" + hostnamectl tags -- +alpha:beta -bar:baz + assert_eq "$(hostnamectl tags)" "alpha:beta:quux" + + # Adding an existing tag and removing a non-existing one are no-ops. + hostnamectl tags +alpha + assert_eq "$(hostnamectl tags)" "alpha:beta:quux" + hostnamectl tags -- -nonexistent + assert_eq "$(hostnamectl tags)" "alpha:beta:quux" + + # Plain and prefixed arguments may not be mixed. + (! hostnamectl tags foo +bar) + (! hostnamectl tags +bar foo) + # Invalid tag characters are refused. + (! hostnamectl tags "in valid") + (! hostnamectl tags "+in/valid") + + # The AddAndRemoveTags() D-Bus method directly. + hostnamectl tags "" + busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 \ + AddAndRemoveTags asas 2 one two 0 + assert_eq "$(hostnamectl tags)" "one:two" + busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 \ + AddAndRemoveTags asas 1 three 1 one + assert_eq "$(hostnamectl tags)" "three:two" + + # The SetTags() Varlink method: 'set' resets the list, then 'add'/'remove' are applied on top. + varlinkctl call /run/systemd/io.systemd.Hostname io.systemd.Hostname.SetTags \ + '{"set":["x","y"],"add":["z"],"remove":["x"]}' + assert_eq "$(hostnamectl tags)" "y:z" + # An explicit empty 'set' clears the list first. + varlinkctl call /run/systemd/io.systemd.Hostname io.systemd.Hostname.SetTags '{"set":[]}' + assert_eq "$(hostnamectl tags)" "" + # Without 'set' the current list is the basis. + varlinkctl call /run/systemd/io.systemd.Hostname io.systemd.Hostname.SetTags '{"add":["aaa","bbb"]}' + assert_eq "$(hostnamectl tags)" "aaa:bbb" + varlinkctl call /run/systemd/io.systemd.Hostname io.systemd.Hostname.SetTags '{"remove":["aaa"]}' + assert_eq "$(hostnamectl tags)" "bbb" + # Invalid tags are refused. + (! varlinkctl call /run/systemd/io.systemd.Hostname io.systemd.Hostname.SetTags '{"add":["in valid"]}') + + hostnamectl tags "" +} + +testcase_varlink_setters() { + local bus=/run/systemd/io.systemd.Hostname + + if [[ -f /etc/hostname ]]; then + cp /etc/hostname /tmp/hostname.bak + fi + if [[ -f /etc/machine-info ]]; then + cp /etc/machine-info /tmp/machine-info.bak + fi + + trap 'restore_hostname; restore_machine_info' RETURN + + # Static hostname. + varlinkctl call "$bus" io.systemd.Hostname.SetStaticHostname '{"newValue":"vl-static"}' + assert_eq "$(cat /etc/hostname)" "vl-static" + assert_in "Static hostname: vl-static" "$(hostnamectl)" + + varlinkctl call "$bus" io.systemd.Hostname.SetStaticHostname '{"newValue":null}' + test ! -e /etc/hostname + + # Transient hostname; a null value resets it back to the fallback + varlinkctl call "$bus" io.systemd.Hostname.SetHostname '{"newValue":"vl-transient"}' + assert_eq "$(hostname)" "vl-transient" + varlinkctl call "$bus" io.systemd.Hostname.SetHostname '{"newValue":null}' + assert_neq "$(hostname)" "vl-transient" + + # Machine-info fields. + varlinkctl call "$bus" io.systemd.Hostname.SetPrettyHostname '{"newValue":"Pretty VL"}' + assert_eq "$(hostnamectl --json=short | jq --raw-output .PrettyHostname)" "Pretty VL" + + varlinkctl call "$bus" io.systemd.Hostname.SetIconName '{"newValue":"computer-vl"}' + assert_eq "$(hostnamectl --json=short | jq --raw-output .IconName)" "computer-vl" + + varlinkctl call "$bus" io.systemd.Hostname.SetChassis '{"newValue":"tablet"}' + assert_eq "$(hostnamectl chassis)" "tablet" + + varlinkctl call "$bus" io.systemd.Hostname.SetDeployment '{"newValue":"staging"}' + assert_eq "$(hostnamectl --json=short | jq --raw-output .Deployment)" "staging" + + varlinkctl call "$bus" io.systemd.Hostname.SetLocation '{"newValue":"Rack 7"}' + assert_eq "$(hostnamectl --json=short | jq --raw-output .Location)" "Rack 7" + + # A null value removes the field again. + varlinkctl call "$bus" io.systemd.Hostname.SetLocation '{"newValue":null}' + assert_eq "$(hostnamectl --json=short | jq --raw-output .Location)" "null" + + # Invalid values are refused. + (! varlinkctl call "$bus" io.systemd.Hostname.SetChassis '{"newValue":"nonsense"}') + (! varlinkctl call "$bus" io.systemd.Hostname.SetIconName '{"newValue":"in/valid"}') + (! varlinkctl call "$bus" io.systemd.Hostname.SetStaticHostname '{"newValue":"invalid hostname"}') +} + run_testcases touch /testok diff --git a/units/meson.build b/units/meson.build index 11b73b9b1c..47c48bcabc 100644 --- a/units/meson.build +++ b/units/meson.build @@ -503,6 +503,10 @@ units = [ 'file' : 'systemd-machine-id-commit.service', 'symlinks' : ['sysinit.target.wants/'], }, + { + 'file' : 'systemd-machine-tag@.service', + 'conditions' : ['ENABLE_HOSTNAMED'], + }, { 'file' : 'systemd-machined.service.in', 'conditions' : ['ENABLE_MACHINED'], diff --git a/units/systemd-machine-tag@.service b/units/systemd-machine-tag@.service new file mode 100644 index 0000000000..7fc578d7e8 --- /dev/null +++ b/units/systemd-machine-tag@.service @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Add Machine Tag %i +Documentation=man:systemd-machine-tag@.service(8) +DefaultDependencies=no +ConditionPathExists=!/etc/initrd-release +After=local-fs.target systemd-hostnamed.socket +Requisite=local-fs.target systemd-hostnamed.socket +Before=shutdown.target +Conflicts=shutdown.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=varlinkctl call /run/systemd/io.systemd.Hostname io.systemd.Hostname.SetTags '{"add":["%i"]}'