diff --git a/man/hostnamectl.xml b/man/hostnamectl.xml
index c120b938c9..4c1d3cf245 100644
--- a/man/hostnamectl.xml
+++ b/man/hostnamectl.xml
@@ -189,8 +189,11 @@
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
must be 1…255 characters long and consist only of ASCII alphanumeric characters,
- - and .. The tags are stored in the TAGS=
- field of /etc/machine-info; see
+ -, . and =. A tag may optionally be
+ parameterized with a value, in the form
+ key=value, in which case the
+ same key may not be assigned more than one distinct value. The tags are stored in the
+ TAGS= field of /etc/machine-info; see
machine-info5 for
details. They may also be matched against with the
ConditionMachineTag=/AssertMachineTag= unit settings, see
diff --git a/man/machine-info.xml b/man/machine-info.xml
index 252f341bba..6b2d92e7e5 100644
--- a/man/machine-info.xml
+++ b/man/machine-info.xml
@@ -149,7 +149,22 @@
TAGS=webserver:frontend:berlin.
Each individual tag must be 1…255 characters long and may consist only of the ASCII
- alphanumeric characters, - and ..
+ alphanumeric characters, -, . and =. The
+ first character may not be -, . or =, and
+ the last character may not be - or . (unless it takes the
+ parameterized form, see below).
+
+ A tag may optionally be parameterized with a value, in the form
+ key=value. The first
+ = separates the key from the value; any further = characters
+ are part of the value. The key (the part before the first =) follows the same
+ restrictions as an unparameterized tag, in particular it may not be empty and may not end in
+ - or .. The value (the part after the first
+ =) may be empty and is otherwise unrestricted within the allowed character set.
+ Example: TAGS=role=webserver:env=production:berlin. The same key may not be
+ assigned more than one distinct value: role=webserver:role=database is refused
+ (but a key may coexist with the corresponding unparameterized tag, e.g.
+ role:role=webserver).
The configured tags may be matched against with the
ConditionMachineTag= and AssertMachineTag= unit settings, see
@@ -217,7 +232,7 @@
ICON_NAME=computer-tablet
CHASSIS=tablet
DEPLOYMENT=production
-TAGS=demo:berlin
+TAGS=demo:berlin:role=webserver
diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml
index 5d2f32dfce..53de1791d2 100644
--- a/man/systemd.unit.xml
+++ b/man/systemd.unit.xml
@@ -2083,6 +2083,17 @@
negated by prepending an exclamation mark, in which case it is satisfied if none of the configured
tags matches.
+ Tags may be parameterized with a value in the form
+ key=value; the
+ = and the value are part of the tag and thus part of the string the pattern is
+ matched against. Hence ConditionMachineTag=role=webserver matches the tag
+ role=webserver exactly, ConditionMachineTag=role=* matches any
+ value assigned to the role key, and ConditionMachineTag=role
+ (without =) does not match role=webserver.
+ See
+ machine-info5
+ for the precise syntax of machine tags.
+
diff --git a/src/basic/hostname-util.c b/src/basic/hostname-util.c
index e12ec01af2..444c4e1af0 100644
--- a/src/basic/hostname-util.c
+++ b/src/basic/hostname-util.c
@@ -259,13 +259,26 @@ bool machine_tag_is_valid(const char *s) {
if (n <= 0 || n >= 256)
return false;
- /* Don't allow "-" and "." as first or last char. (This is load-bearing, we want that "+"/"-" can be
- * used as prefix for adding/removing tags from the list). */
- if (strchr("-.", s[0]) ||
- strchr("-.", s[n-1]))
+ /* Don't allow "-" and "." as first char. (This is load-bearing, we want that "+"/"-" can be used as
+ * prefix for adding/removing tags from the list). */
+ if (strchr("-.=", s[0]))
return false;
- return in_charset(s, ALPHANUMERICAL "-.");
+ /* We allow parameterization of tags, with a "=" as separator */
+ const char *eq = strchr(s, '=');
+ if (eq) {
+ assert(eq > s);
+
+ /* If there is an '=', then make the same restrictions as for the first char on the last char before it */
+ if (strchr("-.", eq[-1]))
+ return false;
+ } else {
+ /* If there's no '=', then make the restriction on the very last character */
+ if (strchr("-.", s[n-1]))
+ return false;
+ }
+
+ return in_charset(s, ALPHANUMERICAL "-.=");
}
bool machine_tag_list_is_valid(char **l) {
@@ -277,6 +290,23 @@ bool machine_tag_list_is_valid(char **l) {
if (!machine_tag_is_valid(*i))
return false;
+
+ const char *eq = strchr(*i, '=');
+ if (!eq)
+ continue;
+
+ /* Refuse tags with a common part before the '=', that do no also carry the same value. */
+ size_t np = eq - *i + 1;
+ STRV_FOREACH(j, l) {
+ if (j == i)
+ break;
+
+ if (streq(*i, *j)) /* Fully identical is OK */
+ continue;
+
+ if (strneq(*i, *j, np)) /* Not identical, but same key: refuse */
+ return false;
+ }
}
return true;
@@ -320,6 +350,21 @@ int machine_tags_from_string(const char *s, bool graceful, char ***ret) {
if (n > MACHINE_TAGS_MAX)
return -E2BIG;
+ const char *eq = strchr(*i, '=');
+ if (eq) {
+ /* Suppress duplicate assignments */
+ bool skip = false;
+ size_t np = eq - *i + 1;
+ STRV_FOREACH(j, cleaned)
+ if (strneq(*i, *j, np)) {
+ skip = true;
+ break;
+ }
+
+ if (skip)
+ continue;
+ }
+
r = strv_extend(&cleaned, *i);
if (r < 0)
return r;