From 8b9d503471f5e4ae0ab67efa0ea9db02c7153f8c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 22:41:32 +0200 Subject: [PATCH 01/13] strv: add strv_remove_strv() This is to strv_remove() what strv_extend_strv() is to strv_extend() --- src/basic/strv.c | 17 +++++++++++++---- src/basic/strv.h | 1 + src/test/test-strv.c | 20 ++++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) 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/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", From f45e7b6f8a8eecb7ca376ce466bd409cd48254f6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 1 Jun 2026 09:33:40 +0200 Subject: [PATCH 02/13] hostnamed: move error logging into context_write_data_{static_hostname|machine_info}() This is preparation for adding Varlink calls for these operations, as it makes sense to unify log msg generation in one place (i.e. the callee) rather than in two (the two callers, i.e. the D-Bus and the Varlink interfaces). --- src/hostname/hostnamed.c | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/hostname/hostnamed.c b/src/hostname/hostnamed.c index a39bf5a57e..745eeaa7a7 100644 --- a/src/hostname/hostnamed.c +++ b/src/hostname/hostnamed.c @@ -828,7 +828,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 +836,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 +863,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 +887,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; @@ -1466,7 +1465,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 +1473,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", @@ -1544,7 +1540,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) From 3eb956700f26b232bd3bb47f8b1855f70a52ddf4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 18:06:04 +0200 Subject: [PATCH 03/13] hostnamed: add AddAndRemoveTags() D-Bus method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new D-Bus method on org.freedesktop.hostname1 that incrementally updates the machine tags instead of replacing the whole list: it takes two string arrays, one of tags to add and one of tags to remove, and stores (current ∪ add) \ remove. This avoids the read-modify-write race that clients would otherwise hit when several of them each manage a subset of the host's tags. Factor the tag persistence and the write-error-to-D-Bus-error mapping out of method_set_tags() into helpers, and add a machine_tags_add_remove() helper for the set arithmetic, all to be shared with further callers. --- man/org.freedesktop.hostname1.xml | 15 ++- src/hostname/hostnamed.c | 165 ++++++++++++++++++++++++++---- 2 files changed, 159 insertions(+), 21 deletions(-) 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/src/hostname/hostnamed.c b/src/hostname/hostnamed.c index 745eeaa7a7..bb30b4c90d 100644 --- a/src/hostname/hostnamed.c +++ b/src/hostname/hostnamed.c @@ -1585,6 +1585,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; @@ -1610,7 +1656,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( @@ -1626,29 +1677,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); } @@ -2009,6 +2129,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), From 0f790ea87fb27e9fda18c9457cfd253830d93532 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 18:16:21 +0200 Subject: [PATCH 04/13] hostnamed: add SetTags() Varlink method Add a SetTags() method to the io.systemd.Hostname Varlink interface that combines what the D-Bus SetTags() and AddAndRemoveTags() methods do into a single call: it takes three tag lists, 'set', 'add' and 'remove'. If 'set' is supplied the tag list is first reset to it (an explicit empty array clears it), otherwise the current tags are used as basis; then the 'add' tags are added and the 'remove' tags are dropped. The set arithmetic is shared with the D-Bus AddAndRemoveTags() handler via machine_tags_add_remove(). Being reachable over Varlink (and hence socket-activatable without the system bus) this is usable very early at boot, before dbus-daemon is up. --- src/hostname/hostnamed.c | 82 ++++++++++++++++++++++++ src/shared/varlink-io.systemd.Hostname.c | 14 +++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/hostname/hostnamed.c b/src/hostname/hostnamed.c index bb30b4c90d..8849843e13 100644 --- a/src/hostname/hostnamed.c +++ b/src/hostname/hostnamed.c @@ -2229,6 +2229,87 @@ static int vl_method_describe(sd_varlink *link, sd_json_variant *parameters, sd_ return sd_varlink_reply(link, v); } +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; @@ -2253,6 +2334,7 @@ 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.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); diff --git a/src/shared/varlink-io.systemd.Hostname.c b/src/shared/varlink-io.systemd.Hostname.c index e06587039b..e7e03e127e 100644 --- a/src/shared/varlink-io.systemd.Hostname.c +++ b/src/shared/varlink-io.systemd.Hostname.c @@ -44,7 +44,19 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_DEFINE_OUTPUT(ProductUUID, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_OUTPUT(VSockCID, SD_VARLINK_INT, SD_VARLINK_NULLABLE)); +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); + &vl_method_Describe, + SD_VARLINK_SYMBOL_COMMENT("Edits the machine tag list: optionally resets it to 'set', then adds 'add' and removes 'remove'."), + &vl_method_SetTags); From e3f453b3e95a16e544543a395ddda75bf303e166 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 22:05:03 +0200 Subject: [PATCH 05/13] hostnamed: add Varlink equivalents for the hostname/machine-info setters Add SetHostname(), SetStaticHostname(), SetPrettyHostname(), SetIconName(), SetChassis(), SetDeployment() and SetLocation() to the io.systemd.Hostname Varlink interface, mirroring the existing D-Bus methods of the same names. Each takes a single nullable string "newValue" (null clears/resets the value; the method name itself makes clear what the value is) plus the usual allowInteractiveAuthentication polkit input, and applies the same validation, polkit actions and /etc/hostname resp. /etc/machine-info persistence as its D-Bus counterpart. The five machine-info setters share a common helper. The hostname validator is reused from the D-Bus path, translating its INVALID_ARGS error into a Varlink invalid-parameter error. Being reachable over the socket-activated Varlink socket, these work in early boot before the system bus is up. --- src/hostname/hostnamed.c | 283 ++++++++++++++++++++++- src/shared/varlink-io.systemd.Hostname.c | 56 +++++ 2 files changed, 330 insertions(+), 9 deletions(-) diff --git a/src/hostname/hostnamed.c b/src/hostname/hostnamed.c index 8849843e13..42fc050f40 100644 --- a/src/hostname/hostnamed.c +++ b/src/hostname/hostnamed.c @@ -1347,7 +1347,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); @@ -1366,12 +1366,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; @@ -1389,7 +1401,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; @@ -1440,7 +1452,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; @@ -2229,6 +2241,252 @@ 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 && string_has_cc(name, NULL)) + 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 && !filename_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 && !valid_chassis(name)) + return sd_varlink_error_invalid_parameter_name(link, "newValue"); + bus_property = "Chassis"; + human_name = "chassis"; + break; + + case PROP_DEPLOYMENT: + if (name && !valid_deployment(name)) + return sd_varlink_error_invalid_parameter_name(link, "newValue"); + bus_property = "Deployment"; + human_name = "deployment"; + break; + + case PROP_LOCATION: + if (name && string_has_cc(name, NULL)) + 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; @@ -2333,11 +2591,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.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); + "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"); diff --git a/src/shared/varlink-io.systemd.Hostname.c b/src/shared/varlink-io.systemd.Hostname.c index e7e03e127e..ce02f5ecbe 100644 --- a/src/shared/varlink-io.systemd.Hostname.c +++ b/src/shared/varlink-io.systemd.Hostname.c @@ -44,6 +44,48 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_DEFINE_OUTPUT(ProductUUID, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), 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."), @@ -58,5 +100,19 @@ SD_VARLINK_DEFINE_INTERFACE( io_systemd_Hostname, "io.systemd.Hostname", &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); From 0d791bc259098359843209792b9605bd5a1a51a6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 22:21:31 +0200 Subject: [PATCH 06/13] hostnamed: add full introspection comments to io.systemd.Hostname Add a SD_VARLINK_FIELD_COMMENT for every field of the Describe() method that still lacked one, plus an interface comment and a symbol comment for the Describe() method itself, so that introspecting io.systemd.Hostname produces fully documented output. --- src/shared/varlink-io.systemd.Hostname.c | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/shared/varlink-io.systemd.Hostname.c b/src/shared/varlink-io.systemd.Hostname.c index ce02f5ecbe..ca7b2b534b 100644 --- a/src/shared/varlink-io.systemd.Hostname.c +++ b/src/shared/varlink-io.systemd.Hostname.c @@ -6,42 +6,73 @@ 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( @@ -99,6 +130,8 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_DEFINE_INTERFACE( io_systemd_Hostname, "io.systemd.Hostname", + 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, From a6ccfb780a2754a66af3af289932dd47e593e348 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 22:26:37 +0200 Subject: [PATCH 07/13] hostnamed: handle SIGRTMIN+18 and memory pressure Wire up the usual SIGRTMIN+18 control signal handler and a memory pressure event source in hostnamed's event loop, like our other long-running daemons, so it reacts to memory pressure (e.g. by trimming its heap) and supports the common debug/control commands. Watchdog support via sd_event_set_watchdog() was already in place. --- src/hostname/hostnamed.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/hostname/hostnamed.c b/src/hostname/hostnamed.c index 42fc050f40..5f7496f7cf 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" @@ -2674,6 +2675,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; From 8b5d45278beb8e1b621615fea044a9b052ad856a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 23:20:08 +0200 Subject: [PATCH 08/13] hostnamed: clean up and tighten rules on valid machine info settings Let's refactor things so that there's a separate xyz_is_valid() call for each field, and make it mostly wrap string_is_safe(). This tightens the rules on some fields, mostly to refuse newlines and some other things. --- src/hostname/hostnamed.c | 48 +++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/hostname/hostnamed.c b/src/hostname/hostnamed.c index 5f7496f7cf..c52ec7c3d8 100644 --- a/src/hostname/hostnamed.c +++ b/src/hostname/hostnamed.c @@ -37,7 +37,6 @@ #include "nulstr-util.h" #include "os-util.h" #include "parse-util.h" -#include "path-util.h" #include "service-util.h" #include "socket-util.h" #include "stat-util.h" @@ -542,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( @@ -559,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) { @@ -711,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; @@ -1519,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); } @@ -2392,7 +2410,7 @@ static int vl_set_machine_info(sd_varlink *link, sd_json_variant *parameters, vo switch (prop) { case PROP_PRETTY_HOSTNAME: - if (name && string_has_cc(name, NULL)) + if (name && !pretty_hostname_is_valid(name)) return sd_varlink_error_invalid_parameter_name(link, "newValue"); bus_property = "PrettyHostname"; human_name = "pretty hostname"; @@ -2403,28 +2421,28 @@ static int vl_set_machine_info(sd_varlink *link, sd_json_variant *parameters, vo case PROP_ICON_NAME: /* The icon name might ultimately be used as file name, so better be safe than sorry. */ - if (name && !filename_is_valid(name)) + 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 && !valid_chassis(name)) + 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 && !valid_deployment(name)) + 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 && string_has_cc(name, NULL)) + if (name && !location_is_valid(name)) return sd_varlink_error_invalid_parameter_name(link, "newValue"); bus_property = "Location"; human_name = "location"; From 775c7213288c9cf800036762d6dda8c4e33f35ac Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 18:20:17 +0200 Subject: [PATCH 09/13] hostnamectl: allow incrementally adding/removing tags Teach the "tags" verb to accept arguments prefixed with '+' or '-': a '+'-prefixed argument adds all of its (colon-separated) tags to the configured list, a '-'-prefixed one removes them, mapping onto the new AddAndRemoveTags() D-Bus method. The prefix applies to the whole argument, so e.g. hostnamectl tags +foo:bar -quux:ooo adds foo and bar and removes quux and ooo. Plain (replace) and prefixed (add/remove) arguments may not be mixed in one invocation. --- man/hostnamectl.xml | 8 +++++ src/hostname/hostnamectl.c | 70 ++++++++++++++++++++++++++++++++------ 2 files changed, 68 insertions(+), 10 deletions(-) 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/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) From 432f54118d2f3e48ab34b8008259193a03369e91 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 18:27:09 +0200 Subject: [PATCH 10/13] units: add systemd-machine-tag@.service template Add a service template that adds the machine tag given as its instance name to the host's machine tags. Whenever an instance is pulled in, the tag is added; the operation is idempotent. It talks to hostnamed over Varlink (which is socket-activated), so it works very early at boot, before the system bus is up. It is ordered after local-fs.target so that /etc/machine-info can be written, requires hostnamed's socket and the local filesystems to be available, and does not run in the initrd. --- man/rules/meson.build | 1 + man/systemd-machine-tag@.service.xml | 65 ++++++++++++++++++++++++++++ units/meson.build | 4 ++ units/systemd-machine-tag@.service | 23 ++++++++++ 4 files changed, 93 insertions(+) create mode 100644 man/systemd-machine-tag@.service.xml create mode 100644 units/systemd-machine-tag@.service diff --git a/man/rules/meson.build b/man/rules/meson.build index d3df447352..f3db05fc78 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/units/meson.build b/units/meson.build index de7a796a1e..db5edb01e6 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"]}' From 4a05010532564ef4d6c6f435bf029eacdf13b5f6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 18:31:17 +0200 Subject: [PATCH 11/13] rules: auto-tag host from the SYSTEMD_MACHINE_TAG udev property When a device carries a SYSTEMD_MACHINE_TAG udev property, tag it for systemd and pull in systemd-machine-tag@.service, which adds the given tag to the host's machine tags. In other words, attaching the property to a device makes the host auto-tag itself with the specified machine tag. --- rules.d/99-systemd.rules.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rules.d/99-systemd.rules.in b/rules.d/99-systemd.rules.in index 98f483503e..0dfed01dfd 100644 --- a/rules.d/99-systemd.rules.in +++ b/rules.d/99-systemd.rules.in @@ -95,4 +95,7 @@ SUBSYSTEM=="tpm", KERNEL=="tpm[0-9]*", TAG+="systemd" SUBSYSTEM=="block", 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" From b3bad277353de139c44df8fbbcdcbd752ca8b1b1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 22:15:14 +0200 Subject: [PATCH 12/13] test: cover hostnamectl tag editing and the new hostnamed Varlink methods Add two testcases to TEST-71-HOSTNAME: - testcase_tags exercises the new "hostnamectl tags" +/- prefix syntax (add/remove, idempotency, the no-mixing rule, invalid input), the AddAndRemoveTags() D-Bus method and the SetTags() Varlink method (set/add/remove, explicit empty set, current-list basis). - testcase_varlink_setters exercises the new SetHostname(), SetStaticHostname(), SetPrettyHostname(), SetIconName(), SetChassis(), SetDeployment() and SetLocation() Varlink methods, including resetting values via null and rejection of invalid input. --- test/units/TEST-71-HOSTNAME.sh | 117 +++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) 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 From 9f62463636cd547465237a92a1af9f117c4bfe78 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 29 May 2026 22:21:45 +0200 Subject: [PATCH 13/13] update TODO --- TODO.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/TODO.md b/TODO.md index 4e5443d049..d369b125e7 100644 --- a/TODO.md +++ b/TODO.md @@ -159,9 +159,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 @@ -684,7 +681,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 @@ -2374,8 +2370,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