]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
hostnamed: add AddAndRemoveTags() D-Bus method
authorLennart Poettering <lennart@amutable.com>
Fri, 29 May 2026 16:06:04 +0000 (18:06 +0200)
committerLennart Poettering <lennart@amutable.com>
Mon, 1 Jun 2026 08:56:16 +0000 (10:56 +0200)
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
src/hostname/hostnamed.c

index d5571de207ff63c0e62b9d12056a3cbecd35be3a..67b52e1ed418c587ef09b62b65ec0423d08e07e9 100644 (file)
@@ -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 {
 
     <variablelist class="dbus-method" generated="True" extra-ref="SetTags()"/>
 
+    <variablelist class="dbus-method" generated="True" extra-ref="AddAndRemoveTags()"/>
+
     <variablelist class="dbus-method" generated="True" extra-ref="GetProductUUID()"/>
 
     <variablelist class="dbus-method" generated="True" extra-ref="GetHardwareSerial()"/>
@@ -385,6 +389,13 @@ node /org/freedesktop/hostname1 {
       alphanumeric characters, <literal>-</literal> and <literal>.</literal>). Passing an empty list removes
       the <varname>TAGS=</varname> field from <filename>/etc/machine-info</filename>.</para>
 
+      <para><function>AddAndRemoveTags()</function> incrementally updates the <varname>Tags</varname>
+      property instead of replacing it. It takes two lists of machine tags: the tags in <varname>add</varname>
+      are added to the current list, and the tags in <varname>remove</varname> are dropped from it (the
+      resulting list is <varname>(current ∪ add) \ remove</varname>, 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 <function>SetTags()</function>.</para>
+
       <para><varname>PrettyHostname</varname>, <varname>IconName</varname>, <varname>Chassis</varname>,
       <varname>Deployment</varname>, <varname>Location</varname>, and <varname>Tags</varname> are stored in
       <filename>/etc/machine-info</filename>. See
@@ -421,7 +432,8 @@ node /org/freedesktop/hostname1 {
       <function>SetStaticHostname()</function> and <function>SetPrettyHostname()</function> it is
       <interfacename>org.freedesktop.hostname1.set-static-hostname</interfacename>. For
       <function>SetIconName()</function>, <function>SetChassis()</function>, <function>SetDeployment()</function>,
-      <function>SetLocation()</function> and <function>SetTags()</function> it is
+      <function>SetLocation()</function>, <function>SetTags()</function> and
+      <function>AddAndRemoveTags()</function> it is
       <interfacename>org.freedesktop.hostname1.set-machine-info</interfacename>.</para>
     </refsect2>
   </refsect1>
@@ -527,6 +539,7 @@ node /org/freedesktop/hostname1 {
       <para><varname>OperatingSystemFancyName</varname> was added in version 260.</para>
       <para><function>GetMachineInfo()</function>, <varname>Tags</varname> and
       <function>SetTags()</function> were added in version 261.</para>
+      <para><function>AddAndRemoveTags()</function> was added in version 262.</para>
     </refsect2>
   </refsect1>
 
index 745eeaa7a7cab5779d5921d1787f156aeb56186c..bb30b4c90d2f16e43ba07f983d9c7bfef2e6092f 100644 (file)
@@ -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, &current);
+        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, &current);
+        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),