<xi:include href="version-info.xml" xpointer="v216"/></listitem>
</varlistentry>
+ <varlistentry>
+ <term><varname>TAGS=</varname></term>
+
+ <listitem><para>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 (<literal>webserver</literal>, <literal>database</literal>), the
+ fleet or organizational unit it belongs to, or any other administrator-defined attribute. Example:
+ <literal>TAGS=webserver:frontend:berlin</literal>.</para>
+
+ <para>Each individual tag must be 1…255 characters long and may consist only of the ASCII
+ alphanumeric characters, <literal>-</literal> and <literal>.</literal>.</para>
+
+ <para>The configured tags may be matched against with the
+ <varname>ConditionMachineTag=</varname> and <varname>AssertMachineTag=</varname> unit settings, see
+ <citerefentry><refentrytitle>systemd.unit</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+ for details. They may be queried and changed with the <command>tags</command> command of
+ <citerefentry><refentrytitle>hostnamectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
+
+ <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+ </varlistentry>
+
<varlistentry>
<term><varname>HARDWARE_VENDOR=</varname></term>
<programlisting>PRETTY_HOSTNAME="Lennart's Tablet"
ICON_NAME=computer-tablet
CHASSIS=tablet
-DEPLOYMENT=production</programlisting>
+DEPLOYMENT=production
+TAGS=demo:berlin</programlisting>
</refsect1>
<refsect1>
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);
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")
<variablelist class="dbus-method" generated="True" extra-ref="SetLocation()"/>
+ <variablelist class="dbus-method" generated="True" extra-ref="SetTags()"/>
+
<variablelist class="dbus-method" generated="True" extra-ref="GetProductUUID()"/>
<variablelist class="dbus-method" generated="True" extra-ref="GetHardwareSerial()"/>
<variablelist class="dbus-property" generated="True" extra-ref="Location"/>
+ <variablelist class="dbus-property" generated="True" extra-ref="Tags"/>
+
<variablelist class="dbus-property" generated="True" extra-ref="KernelName"/>
<variablelist class="dbus-property" generated="True" extra-ref="KernelRelease"/>
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.</para>
+ <para>The <varname>Tags</varname> property exposes a list of <emphasis>machine tags</emphasis>: 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, <literal>-</literal> and <literal>.</literal>. The tags are stored as a
+ colon-separated list in the <varname>TAGS=</varname> field of <filename>/etc/machine-info</filename>, see
+ <citerefentry><refentrytitle>machine-info</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
+ details. If no tags are configured this property is the empty list.</para>
+
<para>Note that <filename>systemd-hostnamed</filename> starts only on request and terminates after a
short idle period. This effectively means that <function>PropertyChanged</function> messages are not sent
out for changes made directly on the files (as in: administrator edits the files with vi). This is
deployment environment), and <varname>Location</varname> (physical system location), respectively.
</para>
+ <para><function>SetTags()</function> sets the machine tags exposed by the <varname>Tags</varname>
+ property. It takes a list of strings, each of which must be a valid machine tag (1…255 ASCII
+ 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><varname>PrettyHostname</varname>, <varname>IconName</varname>, <varname>Chassis</varname>,
- <varname>Deployment</varname>, and <varname>Location</varname> are stored in
+ <varname>Deployment</varname>, <varname>Location</varname>, and <varname>Tags</varname> are stored in
<filename>/etc/machine-info</filename>. See
<citerefentry><refentrytitle>machine-info</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
the semantics of those settings.</para>
<interfacename>org.freedesktop.hostname1.set-hostname</interfacename>. For
<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>
- and <function>SetLocation()</function> it is
+ <function>SetIconName()</function>, <function>SetChassis()</function>, <function>SetDeployment()</function>,
+ <function>SetLocation()</function> and <function>SetTags()</function> it is
<interfacename>org.freedesktop.hostname1.set-machine-info</interfacename>.</para>
</refsect2>
</refsect1>
<varname>OperatingSystemImageVersion</varname>, <varname>HardwareSKU</varname>, and
<varname>HardwareVersion</varname> were added in version 258.</para>
<para><varname>OperatingSystemFancyName</varname> was added in version 260.</para>
- <para><function>GetMachineInfo()</function> was added in version 261.</para>
+ <para><function>GetMachineInfo()</function>, <varname>Tags</varname> and
+ <function>SetTags()</function> were added in version 261.</para>
</refsect2>
</refsect1>
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;
+}
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);
#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"
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,
PROP_OS_SUPPORT_END,
PROP_OS_IMAGE_ID,
PROP_OS_IMAGE_VERSION,
+
_PROP_MAX,
_PROP_INVALID = -EINVAL,
} HostProperty;
PROP_CHASSIS,
PROP_DEPLOYMENT,
PROP_LOCATION,
+ PROP_TAGS,
PROP_HARDWARE_VENDOR,
PROP_HARDWARE_MODEL,
PROP_HARDWARE_SKU,
"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],
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;
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]));
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,
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);
{ "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 },
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),
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),
/* 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) {
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);