diff --git a/man/machine-info.xml b/man/machine-info.xml
index 53c9e64ec4..252f341bba 100644
--- a/man/machine-info.xml
+++ b/man/machine-info.xml
@@ -139,6 +139,27 @@
+
+ TAGS=
+
+ A colon-separated list of tags attached to this machine. Tags are short labels that
+ may be used to classify and group machines for management purposes, for example to identify the role
+ a machine plays in a deployment (webserver, database), the
+ fleet or organizational unit it belongs to, or any other administrator-defined attribute. Example:
+ TAGS=webserver:frontend:berlin.
+
+ Each individual tag must be 1…255 characters long and may consist only of the ASCII
+ alphanumeric characters, - and ..
+
+ The configured tags may be matched against with the
+ ConditionMachineTag= and AssertMachineTag= unit settings, see
+ systemd.unit5
+ for details. They may be queried and changed with the tags command of
+ hostnamectl1.
+
+
+
+
HARDWARE_VENDOR=
@@ -195,7 +216,8 @@
PRETTY_HOSTNAME="Lennart's Tablet"
ICON_NAME=computer-tablet
CHASSIS=tablet
-DEPLOYMENT=production
+DEPLOYMENT=production
+TAGS=demo:berlin
diff --git a/man/org.freedesktop.hostname1.xml b/man/org.freedesktop.hostname1.xml
index 3d98b88ebc..d5571de207 100644
--- a/man/org.freedesktop.hostname1.xml
+++ b/man/org.freedesktop.hostname1.xml
@@ -56,6 +56,7 @@ node /org/freedesktop/hostname1 {
in b interactive);
SetLocation(in s location,
in b interactive);
+ SetTags(in as tags);
GetProductUUID(in b interactive,
out ay uuid);
GetHardwareSerial(out s serial);
@@ -73,6 +74,7 @@ node /org/freedesktop/hostname1 {
readonly s Chassis = '...';
readonly s Deployment = '...';
readonly s Location = '...';
+ readonly as Tags = ['...', ...];
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
readonly s KernelName = '...';
@org.freedesktop.DBus.Property.EmitsChangedSignal("const")
@@ -142,6 +144,8 @@ node /org/freedesktop/hostname1 {
+
+
@@ -168,6 +172,8 @@ node /org/freedesktop/hostname1 {
+
+
@@ -268,6 +274,15 @@ node /org/freedesktop/hostname1 {
configure the chassis type if it could not be auto-detected. Set this property to the empty string to
reenable the automatic detection of the chassis type from firmware information.
+ The Tags property exposes a list of machine tags: short
+ labels that may be used to classify and group machines for management purposes, for example to identify
+ the role a machine plays in a deployment, the fleet or organizational unit it belongs to, or any other
+ administrator-defined attribute. Each individual tag is 1…255 characters long and consists only of the
+ ASCII alphanumeric characters, - and .. The tags are stored as a
+ colon-separated list in the TAGS= field of /etc/machine-info, see
+ machine-info5 for
+ details. If no tags are configured this property is the empty list.
+
Note that systemd-hostnamed starts only on request and terminates after a
short idle period. This effectively means that PropertyChanged messages are not sent
out for changes made directly on the files (as in: administrator edits the files with vi). This is
@@ -365,8 +380,13 @@ node /org/freedesktop/hostname1 {
deployment environment), and Location (physical system location), respectively.
+ SetTags() sets the machine tags exposed by the Tags
+ property. It takes a list of strings, each of which must be a valid machine tag (1…255 ASCII
+ alphanumeric characters, - and .). Passing an empty list removes
+ the TAGS= field from /etc/machine-info.
+
PrettyHostname, IconName, Chassis,
- Deployment, and Location are stored in
+ Deployment, Location, and Tags are stored in
/etc/machine-info. See
machine-info5 for
the semantics of those settings.
@@ -400,8 +420,8 @@ node /org/freedesktop/hostname1 {
org.freedesktop.hostname1.set-hostname. For
SetStaticHostname() and SetPrettyHostname() it is
org.freedesktop.hostname1.set-static-hostname. For
- SetIconName(), SetChassis(), SetDeployment()
- and SetLocation() it is
+ SetIconName(), SetChassis(), SetDeployment(),
+ SetLocation() and SetTags() it is
org.freedesktop.hostname1.set-machine-info.
@@ -505,7 +525,8 @@ node /org/freedesktop/hostname1 {
OperatingSystemImageVersion, HardwareSKU, and
HardwareVersion were added in version 258.
OperatingSystemFancyName was added in version 260.
- GetMachineInfo() was added in version 261.
+ GetMachineInfo(), Tags and
+ SetTags() were added in version 261.
diff --git a/src/basic/hostname-util.c b/src/basic/hostname-util.c
index 01434d3d64..baa735ab10 100644
--- a/src/basic/hostname-util.c
+++ b/src/basic/hostname-util.c
@@ -253,3 +253,72 @@ int machine_spec_valid(const char *s) {
return true;
}
+
+bool machine_tag_is_valid(const char *s) {
+ size_t n = strlen_ptr(s);
+ if (n <= 0 || n >= 256)
+ return false;
+
+ return in_charset(s, ALPHANUMERICAL "-.");
+}
+
+bool machine_tag_list_is_valid(char **l) {
+ size_t n = 0;
+ STRV_FOREACH(i, l) {
+ n++;
+ if (n > MACHINE_TAGS_MAX)
+ return false;
+
+ if (!machine_tag_is_valid(*i))
+ return false;
+ }
+
+ return true;
+}
+
+int machine_tags_from_string(const char *s, bool graceful, char ***ret) {
+ int r;
+
+ assert(ret);
+
+ /* Parses the colon-separated TAGS= machine-info field into a sorted, deduplicated strv. Each tag is
+ * validated: if 'graceful' is true invalid tags are silently dropped, otherwise an invalid tag makes
+ * us fail with -EINVAL. The result is NULL if no (valid) tags remain. */
+
+ if (isempty(s)) {
+ *ret = NULL;
+ return 0;
+ }
+
+ _cleanup_strv_free_ char **l = strv_split(s, ":");
+ if (!l)
+ return -ENOMEM;
+
+ strv_sort_uniq(l);
+
+ if (!graceful) {
+ if (!machine_tag_list_is_valid(l))
+ return -EINVAL;
+
+ *ret = strv_isempty(l) ? NULL : TAKE_PTR(l);
+ return 0;
+ }
+
+ size_t n = 0;
+ _cleanup_strv_free_ char **cleaned = NULL;
+ STRV_FOREACH(i, l) {
+ if (!machine_tag_is_valid(*i))
+ continue;
+
+ n++;
+ if (n > MACHINE_TAGS_MAX)
+ return -E2BIG;
+
+ r = strv_extend(&cleaned, *i);
+ if (r < 0)
+ return r;
+ }
+
+ *ret = TAKE_PTR(cleaned);
+ return 0;
+}
diff --git a/src/basic/hostname-util.h b/src/basic/hostname-util.h
index 73cd83fbb8..f3d904a1bb 100644
--- a/src/basic/hostname-util.h
+++ b/src/basic/hostname-util.h
@@ -47,3 +47,9 @@ int get_pretty_hostname(char **ret);
int machine_spec_valid(const char *s);
int split_user_at_host(const char *s, char **ret_user, char **ret_host);
+
+#define MACHINE_TAGS_MAX 1024U
+
+bool machine_tag_is_valid(const char *s);
+bool machine_tag_list_is_valid(char **l);
+int machine_tags_from_string(const char *s, bool graceful, char ***ret);
diff --git a/src/hostname/hostnamed.c b/src/hostname/hostnamed.c
index f8c20ff9c7..a39bf5a57e 100644
--- a/src/hostname/hostnamed.c
+++ b/src/hostname/hostnamed.c
@@ -43,7 +43,6 @@
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
-#include "utf8.h"
#include "varlink-io.systemd.Hostname.h"
#include "varlink-io.systemd.service.h"
#include "varlink-util.h"
@@ -58,12 +57,15 @@ typedef enum {
PROP_STATIC_HOSTNAME,
PROP_STATIC_HOSTNAME_SUBSTITUTED_WILDCARDS,
- /* Read from /etc/machine-info */
+ /* Read from /etc/machine-info (with fallbacks) */
PROP_PRETTY_HOSTNAME,
+ _PROP_MACHINE_INFO_SETTABLE_FIRST = PROP_PRETTY_HOSTNAME,
PROP_ICON_NAME,
PROP_CHASSIS,
PROP_DEPLOYMENT,
PROP_LOCATION,
+ PROP_TAGS,
+ _PROP_MACHINE_INFO_SETTABLE_LAST = PROP_TAGS,
PROP_HARDWARE_VENDOR,
PROP_HARDWARE_MODEL,
PROP_HARDWARE_SKU,
@@ -77,6 +79,7 @@ typedef enum {
PROP_OS_SUPPORT_END,
PROP_OS_IMAGE_ID,
PROP_OS_IMAGE_VERSION,
+
_PROP_MAX,
_PROP_INVALID = -EINVAL,
} HostProperty;
@@ -173,6 +176,7 @@ static void context_read_machine_info(Context *c) {
PROP_CHASSIS,
PROP_DEPLOYMENT,
PROP_LOCATION,
+ PROP_TAGS,
PROP_HARDWARE_VENDOR,
PROP_HARDWARE_MODEL,
PROP_HARDWARE_SKU,
@@ -184,6 +188,7 @@ static void context_read_machine_info(Context *c) {
"CHASSIS", &c->data[PROP_CHASSIS],
"DEPLOYMENT", &c->data[PROP_DEPLOYMENT],
"LOCATION", &c->data[PROP_LOCATION],
+ "TAGS", &c->data[PROP_TAGS],
"HARDWARE_VENDOR", &c->data[PROP_HARDWARE_VENDOR],
"HARDWARE_MODEL", &c->data[PROP_HARDWARE_MODEL],
"HARDWARE_SKU", &c->data[PROP_HARDWARE_SKU],
@@ -840,11 +845,12 @@ static int context_write_data_static_hostname(Context *c) {
static int context_write_data_machine_info(Context *c) {
_cleanup_(unset_statp) struct stat *s = NULL;
static const char * const name[_PROP_MAX] = {
- [PROP_PRETTY_HOSTNAME] = "PRETTY_HOSTNAME",
- [PROP_ICON_NAME] = "ICON_NAME",
- [PROP_CHASSIS] = "CHASSIS",
- [PROP_DEPLOYMENT] = "DEPLOYMENT",
- [PROP_LOCATION] = "LOCATION",
+ [PROP_PRETTY_HOSTNAME] = "PRETTY_HOSTNAME",
+ [PROP_ICON_NAME] = "ICON_NAME",
+ [PROP_CHASSIS] = "CHASSIS",
+ [PROP_DEPLOYMENT] = "DEPLOYMENT",
+ [PROP_LOCATION] = "LOCATION",
+ [PROP_TAGS] = "TAGS",
};
_cleanup_strv_free_ char **l = NULL;
int r;
@@ -859,7 +865,7 @@ static int context_write_data_machine_info(Context *c) {
if (r < 0 && r != -ENOENT)
return r;
- for (HostProperty p = PROP_PRETTY_HOSTNAME; p <= PROP_LOCATION; p++) {
+ for (HostProperty p = _PROP_MACHINE_INFO_SETTABLE_FIRST; p <= _PROP_MACHINE_INFO_SETTABLE_LAST; p++) {
assert(name[p]);
r = strv_env_assign(&l, name[p], empty_to_null(c->data[p]));
@@ -1163,6 +1169,29 @@ static int property_get_machine_info_field(
return sd_bus_message_append(reply, "s", *(char**) userdata);
}
+static int property_get_tags(
+ sd_bus *bus,
+ const char *path,
+ const char *interface,
+ const char *property,
+ sd_bus_message *reply,
+ void *userdata,
+ sd_bus_error *error) {
+
+ Context *c = ASSERT_PTR(userdata);
+ int r;
+
+ context_read_machine_info(c);
+
+ /* Silently drop any invalid tags that might have been written into the file by hand */
+ _cleanup_strv_free_ char **l = NULL;
+ r = machine_tags_from_string(c->data[PROP_TAGS], /* graceful= */ true, &l);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse machine tags '%s', ignoring: %m", strnull(c->data[PROP_TAGS]));
+
+ return sd_bus_message_append_strv(reply, l);
+}
+
static int property_get_os_release_field(
sd_bus *bus,
const char *path,
@@ -1561,6 +1590,74 @@ static int method_set_location(sd_bus_message *m, void *userdata, sd_bus_error *
return set_machine_info(userdata, m, PROP_LOCATION, method_set_location, error);
}
+static int method_set_tags(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+ Context *c = ASSERT_PTR(userdata);
+ int r;
+
+ assert(m);
+
+ _cleanup_strv_free_ char **tags = NULL;
+ r = sd_bus_message_read_strv(m, &tags);
+ if (r < 0)
+ return r;
+
+ strv_sort_uniq(tags);
+
+ if (strv_length(tags) > MACHINE_TAGS_MAX)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Too many machine tags specified.");
+
+ _cleanup_free_ char *j = strv_join(tags, ":");
+ if (!j)
+ return log_oom();
+
+ if (!machine_tag_list_is_valid(tags))
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid tags '%s'", j);
+
+ context_read_machine_info(c);
+
+ if (streq_ptr(empty_to_null(j), empty_to_null(c->data[PROP_TAGS])))
+ return sd_bus_reply_method_return(m, NULL);
+
+ r = bus_verify_polkit_async_full(
+ m,
+ "org.freedesktop.hostname1.set-machine-info",
+ /* details= */ NULL,
+ /* good_user= */ UID_INVALID,
+ /* flags= */ 0,
+ &c->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* No authorization for now, but the async polkit stuff will call us again when it has it */
+
+ if (strv_isempty(tags))
+ c->data[PROP_TAGS] = mfree(c->data[PROP_TAGS]);
+ else
+ free_and_replace(c->data[PROP_TAGS], j);
+
+ r = context_write_data_machine_info(c);
+ if (r < 0) {
+ log_error_errno(r, "Failed to write machine info: %m");
+ if (ERRNO_IS_PRIVILEGE(r))
+ return sd_bus_error_set(error, BUS_ERROR_FILE_IS_PROTECTED, "Not allowed to update /etc/machine-info.");
+ if (r == -EROFS)
+ return sd_bus_error_set(error, BUS_ERROR_READ_ONLY_FILESYSTEM, "/etc/machine-info is in a read-only filesystem.");
+ return sd_bus_error_set_errnof(error, r, "Failed to write machine info: %m");
+ }
+
+ log_info("Changed tags to '%s'", strempty(c->data[PROP_TAGS]));
+
+ (void) sd_bus_emit_properties_changed(
+ sd_bus_message_get_bus(m),
+ "/org/freedesktop/hostname1",
+ "org.freedesktop.hostname1",
+ "Tags",
+ NULL);
+
+ return sd_bus_reply_method_return(m, NULL);
+}
+
static int method_get_product_uuid(sd_bus_message *m, void *userdata, sd_bus_error *error) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
Context *c = ASSERT_PTR(userdata);
@@ -1645,6 +1742,7 @@ static int method_get_machine_info(sd_bus_message *m, void *userdata, sd_bus_err
{ "CHASSIS", PROP_CHASSIS },
{ "DEPLOYMENT", PROP_DEPLOYMENT },
{ "LOCATION", PROP_LOCATION },
+ { "TAGS", PROP_TAGS },
{ "HARDWARE_VENDOR", PROP_HARDWARE_VENDOR },
{ "HARDWARE_MODEL", PROP_HARDWARE_MODEL },
{ "HARDWARE_SKU", PROP_HARDWARE_SKU },
@@ -1853,6 +1951,7 @@ static const sd_bus_vtable hostname_vtable[] = {
SD_BUS_PROPERTY("Chassis", "s", property_get_chassis, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("Deployment", "s", property_get_machine_info_field, offsetof(Context, data[PROP_DEPLOYMENT]), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("Location", "s", property_get_machine_info_field, offsetof(Context, data[PROP_LOCATION]), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
+ SD_BUS_PROPERTY("Tags", "as", property_get_tags, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_PROPERTY("KernelName", "s", property_get_uname_field, offsetof(struct utsname, sysname), SD_BUS_VTABLE_ABSOLUTE_OFFSET|SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("KernelRelease", "s", property_get_uname_field, offsetof(struct utsname, release), SD_BUS_VTABLE_ABSOLUTE_OFFSET|SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("KernelVersion", "s", property_get_uname_field, offsetof(struct utsname, version), SD_BUS_VTABLE_ABSOLUTE_OFFSET|SD_BUS_VTABLE_PROPERTY_CONST),
@@ -1910,6 +2009,11 @@ static const sd_bus_vtable hostname_vtable[] = {
SD_BUS_NO_RESULT,
method_set_location,
SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("SetTags",
+ SD_BUS_ARGS("as", tags),
+ SD_BUS_NO_RESULT,
+ method_set_tags,
+ SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("GetProductUUID",
SD_BUS_ARGS("b", interactive),
SD_BUS_RESULT("ay", uuid),
diff --git a/src/test/test-hostname-util.c b/src/test/test-hostname-util.c
index b3e3b1b84a..1a3cb1dfff 100644
--- a/src/test/test-hostname-util.c
+++ b/src/test/test-hostname-util.c
@@ -1,7 +1,9 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#include "alloc-util.h"
#include "hostname-util.h"
#include "string-util.h"
+#include "strv.h"
#include "tests.h"
TEST(hostname_is_valid) {
@@ -117,4 +119,66 @@ TEST(split_user_at_host) {
test_split_user_at_host_one("aa@@@bb", "aa", "@@bb", 1);
}
+TEST(machine_tag_is_valid) {
+ assert_se(machine_tag_is_valid("foo"));
+ assert_se(machine_tag_is_valid("foo-bar.baz"));
+ assert_se(machine_tag_is_valid("Webserver01"));
+ assert_se(machine_tag_is_valid("a"));
+
+ assert_se(!machine_tag_is_valid(NULL));
+ assert_se(!machine_tag_is_valid(""));
+ assert_se(!machine_tag_is_valid("foo:bar")); /* colon is the separator */
+ assert_se(!machine_tag_is_valid("foo bar"));
+ assert_se(!machine_tag_is_valid("fööbar")); /* non-ASCII */
+ assert_se(!machine_tag_is_valid("foo/bar"));
+ assert_se(!machine_tag_is_valid("foo_bar"));
+
+ /* Length boundary: 255 characters is fine, 256 is too long */
+ _cleanup_free_ char *max = strrep("a", 255), *over = strrep("a", 256);
+ assert_se(max);
+ assert_se(over);
+ assert_se(machine_tag_is_valid(max));
+ assert_se(!machine_tag_is_valid(over));
+}
+
+TEST(machine_tag_list_is_valid) {
+ assert_se(machine_tag_list_is_valid(NULL)); /* empty list is valid */
+ assert_se(machine_tag_list_is_valid(STRV_MAKE("a")));
+ assert_se(machine_tag_list_is_valid(STRV_MAKE("foo", "bar", "c-d.e")));
+
+ assert_se(!machine_tag_list_is_valid(STRV_MAKE("foo", "b:c")));
+ assert_se(!machine_tag_list_is_valid(STRV_MAKE("foo", "")));
+}
+
+TEST(machine_tags_from_string) {
+ _cleanup_strv_free_ char **l = NULL;
+
+ ASSERT_OK(machine_tags_from_string(NULL, /* graceful= */ false, &l));
+ assert_se(strv_isempty(l));
+ l = strv_free(l);
+
+ ASSERT_OK(machine_tags_from_string("", /* graceful= */ true, &l));
+ assert_se(strv_isempty(l));
+ l = strv_free(l);
+
+ /* Sorted and deduplicated */
+ ASSERT_OK(machine_tags_from_string("foo:bar:foo:baz", /* graceful= */ false, &l));
+ assert_se(strv_equal(l, STRV_MAKE("bar", "baz", "foo")));
+ l = strv_free(l);
+
+ /* Graceful: invalid tags are dropped, valid ones kept (sorted/deduplicated) */
+ ASSERT_OK(machine_tags_from_string("foo:in valid:bar:foo", /* graceful= */ true, &l));
+ assert_se(strv_equal(l, STRV_MAKE("bar", "foo")));
+ l = strv_free(l);
+
+ /* Graceful: all tags invalid → empty list */
+ ASSERT_OK(machine_tags_from_string("in valid:also invalid", /* graceful= */ true, &l));
+ assert_se(strv_isempty(l));
+ l = strv_free(l);
+
+ /* Fatal: a single invalid tag fails the whole parse */
+ ASSERT_ERROR(machine_tags_from_string("foo:in valid:bar", /* graceful= */ false, &l), EINVAL);
+ assert_se(!l);
+}
+
DEFINE_TEST_MAIN(LOG_DEBUG);