hostnamed: add AddAndRemoveTags() D-Bus method

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.
This commit is contained in:
Lennart Poettering
2026-06-01 10:56:16 +02:00
parent f45e7b6f8a
commit 3eb956700f
2 changed files with 159 additions and 21 deletions
+14 -1
View File
@@ -57,6 +57,8 @@ node /org/freedesktop/hostname1 {
SetLocation(in s location, SetLocation(in s location,
in b interactive); in b interactive);
SetTags(in as tags); SetTags(in as tags);
AddAndRemoveTags(in as add,
in as remove);
GetProductUUID(in b interactive, GetProductUUID(in b interactive,
out ay uuid); out ay uuid);
GetHardwareSerial(out s serial); 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="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="GetProductUUID()"/>
<variablelist class="dbus-method" generated="True" extra-ref="GetHardwareSerial()"/> <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 alphanumeric characters, <literal>-</literal> and <literal>.</literal>). Passing an empty list removes
the <varname>TAGS=</varname> field from <filename>/etc/machine-info</filename>.</para> 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>, <para><varname>PrettyHostname</varname>, <varname>IconName</varname>, <varname>Chassis</varname>,
<varname>Deployment</varname>, <varname>Location</varname>, and <varname>Tags</varname> are stored in <varname>Deployment</varname>, <varname>Location</varname>, and <varname>Tags</varname> are stored in
<filename>/etc/machine-info</filename>. See <filename>/etc/machine-info</filename>. See
@@ -421,7 +432,8 @@ node /org/freedesktop/hostname1 {
<function>SetStaticHostname()</function> and <function>SetPrettyHostname()</function> it is <function>SetStaticHostname()</function> and <function>SetPrettyHostname()</function> it is
<interfacename>org.freedesktop.hostname1.set-static-hostname</interfacename>. For <interfacename>org.freedesktop.hostname1.set-static-hostname</interfacename>. For
<function>SetIconName()</function>, <function>SetChassis()</function>, <function>SetDeployment()</function>, <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> <interfacename>org.freedesktop.hostname1.set-machine-info</interfacename>.</para>
</refsect2> </refsect2>
</refsect1> </refsect1>
@@ -527,6 +539,7 @@ node /org/freedesktop/hostname1 {
<para><varname>OperatingSystemFancyName</varname> was added in version 260.</para> <para><varname>OperatingSystemFancyName</varname> was added in version 260.</para>
<para><function>GetMachineInfo()</function>, <varname>Tags</varname> and <para><function>GetMachineInfo()</function>, <varname>Tags</varname> and
<function>SetTags()</function> were added in version 261.</para> <function>SetTags()</function> were added in version 261.</para>
<para><function>AddAndRemoveTags()</function> was added in version 262.</para>
</refsect2> </refsect2>
</refsect1> </refsect1>
+145 -20
View 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); 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) { static int method_set_tags(sd_bus_message *m, void *userdata, sd_bus_error *error) {
Context *c = ASSERT_PTR(userdata); Context *c = ASSERT_PTR(userdata);
int r; 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); 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); return sd_bus_reply_method_return(m, NULL);
r = bus_verify_polkit_async_full( 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) if (r == 0)
return 1; /* No authorization for now, but the async polkit stuff will call us again when it has it */ return 1; /* No authorization for now, but the async polkit stuff will call us again when it has it */
if (strv_isempty(tags)) r = context_store_tags(c, tags);
c->data[PROP_TAGS] = mfree(c->data[PROP_TAGS]); if (r < 0)
else return bus_error_from_tags_write(error, r);
free_and_replace(c->data[PROP_TAGS], j);
r = context_write_data_machine_info(c); return sd_bus_reply_method_return(m, NULL);
if (r < 0) { }
log_error_errno(r, "Failed to write machine info: %m");
if (ERRNO_IS_PRIVILEGE(r)) static int machine_tags_add_remove(char * const *base, char * const *add, char * const *remove, char ***ret) {
return sd_bus_error_set(error, BUS_ERROR_FILE_IS_PROTECTED, "Not allowed to update /etc/machine-info."); int r;
if (r == -EROFS)
return sd_bus_error_set(error, BUS_ERROR_READ_ONLY_FILESYSTEM, "/etc/machine-info is in a read-only filesystem."); assert(ret);
return sd_bus_error_set_errnof(error, r, "Failed to write machine info: %m");
/* 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( /* Start from the current tags, add the requested ones, then drop the ones to be removed. */
sd_bus_message_get_bus(m), _cleanup_strv_free_ char **current = NULL;
"/org/freedesktop/hostname1", r = machine_tags_from_string(c->data[PROP_TAGS], /* graceful= */ true, &current);
"org.freedesktop.hostname1", if (r < 0)
"Tags", return log_error_errno(r, "Failed to parse current machine tags: %m");
NULL);
_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); return sd_bus_reply_method_return(m, NULL);
} }
@@ -2009,6 +2129,11 @@ static const sd_bus_vtable hostname_vtable[] = {
SD_BUS_NO_RESULT, SD_BUS_NO_RESULT,
method_set_tags, method_set_tags,
SD_BUS_VTABLE_UNPRIVILEGED), 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_METHOD_WITH_ARGS("GetProductUUID",
SD_BUS_ARGS("b", interactive), SD_BUS_ARGS("b", interactive),
SD_BUS_RESULT("ay", uuid), SD_BUS_RESULT("ay", uuid),