diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml
index 5c3439d193..2990e65891 100644
--- a/man/systemd.unit.xml
+++ b/man/systemd.unit.xml
@@ -2058,6 +2058,25 @@
+
+ ConditionMachineTag=
+
+ ConditionMachineTag= may be used to match against the tags
+ assigned to the local machine. Machine tags are short labels that classify and group machines for
+ management purposes; they are configured in the TAGS= field of
+ machine-info5 and
+ may be queried and changed with the tags command of
+ hostnamectl1. The
+ argument is a single tag pattern, which is compared against each of the configured tags using
+ shell-style globbing (*, ?, []). The
+ condition is satisfied if at least one of the configured tags matches the pattern. The test may be
+ negated by prepending an exclamation mark, in which case it is satisfied if none of the configured
+ tags matches.
+
+
+
+
+
ConditionMemoryPressure=
ConditionCPUPressure=
@@ -2126,6 +2145,7 @@
AssertCPUs=
AssertCPUFeature=
AssertOSRelease=
+ AssertMachineTag=
AssertMemoryPressure=
AssertCPUPressure=
AssertIOPressure=
diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in
index be074230ce..60fec56f46 100644
--- a/src/core/load-fragment-gperf.gperf.in
+++ b/src/core/load-fragment-gperf.gperf.in
@@ -398,6 +398,7 @@ Unit.ConditionUser, config_parse_unit_condition_string
Unit.ConditionGroup, config_parse_unit_condition_string, CONDITION_GROUP, offsetof(Unit, conditions)
Unit.ConditionControlGroupController, config_parse_unit_condition_string, CONDITION_CONTROL_GROUP_CONTROLLER, offsetof(Unit, conditions)
Unit.ConditionOSRelease, config_parse_unit_condition_string, CONDITION_OS_RELEASE, offsetof(Unit, conditions)
+Unit.ConditionMachineTag, config_parse_unit_condition_string, CONDITION_MACHINE_TAG, offsetof(Unit, conditions)
Unit.ConditionMemoryPressure, config_parse_unit_condition_string, CONDITION_MEMORY_PRESSURE, offsetof(Unit, conditions)
Unit.ConditionCPUPressure, config_parse_unit_condition_string, CONDITION_CPU_PRESSURE, offsetof(Unit, conditions)
Unit.ConditionIOPressure, config_parse_unit_condition_string, CONDITION_IO_PRESSURE, offsetof(Unit, conditions)
@@ -434,6 +435,7 @@ Unit.AssertUser, config_parse_unit_condition_string
Unit.AssertGroup, config_parse_unit_condition_string, CONDITION_GROUP, offsetof(Unit, asserts)
Unit.AssertControlGroupController, config_parse_unit_condition_string, CONDITION_CONTROL_GROUP_CONTROLLER, offsetof(Unit, asserts)
Unit.AssertOSRelease, config_parse_unit_condition_string, CONDITION_OS_RELEASE, offsetof(Unit, asserts)
+Unit.AssertMachineTag, config_parse_unit_condition_string, CONDITION_MACHINE_TAG, offsetof(Unit, asserts)
Unit.AssertMemoryPressure, config_parse_unit_condition_string, CONDITION_MEMORY_PRESSURE, offsetof(Unit, asserts)
Unit.AssertCPUPressure, config_parse_unit_condition_string, CONDITION_CPU_PRESSURE, offsetof(Unit, asserts)
Unit.AssertIOPressure, config_parse_unit_condition_string, CONDITION_IO_PRESSURE, offsetof(Unit, asserts)
diff --git a/src/shared/condition.c b/src/shared/condition.c
index d0e41bbf71..ae33c18ade 100644
--- a/src/shared/condition.c
+++ b/src/shared/condition.c
@@ -35,6 +35,7 @@
#include "glob-util.h"
#include "hmac.h"
#include "hostname-setup.h"
+#include "hostname-util.h"
#include "id128-util.h"
#include "ima-util.h"
#include "initrd-util.h"
@@ -316,6 +317,37 @@ static int condition_test_osrelease(Condition *c, char **env) {
return true;
}
+static int condition_test_machine_tag(Condition *c, char **env) {
+ int r;
+
+ assert(c);
+ assert(c->parameter);
+ assert(c->type == CONDITION_MACHINE_TAG);
+
+ _cleanup_free_ char *tags = NULL;
+ r = parse_env_file(
+ /* f= */ NULL, etc_machine_info(),
+ "TAGS", &tags);
+ if (r < 0 && r != -ENOENT) {
+ log_debug_errno(r, "Failed to read /etc/machine-info, ignoring: %m");
+ return false;
+ }
+
+ /* Silently ignore invalid tags, matching the Tags D-Bus property */
+ _cleanup_strv_free_ char **l = NULL;
+ r = machine_tags_from_string(tags, /* graceful= */ true, &l);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to parse machine tags '%s', ignoring: %m", tags);
+ return false;
+ }
+
+ STRV_FOREACH(i, l)
+ if (fnmatch(c->parameter, *i, /* flags= */ 0) == 0)
+ return true;
+
+ return false;
+}
+
static int condition_test_memory(Condition *c, char **env) {
CompareOperator operator;
uint64_t m, k;
@@ -1344,6 +1376,7 @@ int condition_test(Condition *c, char **env) {
[CONDITION_ENVIRONMENT] = condition_test_environment,
[CONDITION_CPU_FEATURE] = condition_test_cpufeature,
[CONDITION_OS_RELEASE] = condition_test_osrelease,
+ [CONDITION_MACHINE_TAG] = condition_test_machine_tag,
[CONDITION_MEMORY_PRESSURE] = condition_test_psi,
[CONDITION_CPU_PRESSURE] = condition_test_psi,
[CONDITION_IO_PRESSURE] = condition_test_psi,
@@ -1471,6 +1504,7 @@ static const char* const _condition_type_table[_CONDITION_TYPE_MAX] = {
[CONDITION_ENVIRONMENT] = "ConditionEnvironment",
[CONDITION_CPU_FEATURE] = "ConditionCPUFeature",
[CONDITION_OS_RELEASE] = "ConditionOSRelease",
+ [CONDITION_MACHINE_TAG] = "ConditionMachineTag",
[CONDITION_MEMORY_PRESSURE] = "ConditionMemoryPressure",
[CONDITION_CPU_PRESSURE] = "ConditionCPUPressure",
[CONDITION_IO_PRESSURE] = "ConditionIOPressure",
@@ -1528,6 +1562,7 @@ static const char* const _assert_type_table[_CONDITION_TYPE_MAX] = {
[CONDITION_ENVIRONMENT] = "AssertEnvironment",
[CONDITION_CPU_FEATURE] = "AssertCPUFeature",
[CONDITION_OS_RELEASE] = "AssertOSRelease",
+ [CONDITION_MACHINE_TAG] = "AssertMachineTag",
[CONDITION_MEMORY_PRESSURE] = "AssertMemoryPressure",
[CONDITION_CPU_PRESSURE] = "AssertCPUPressure",
[CONDITION_IO_PRESSURE] = "AssertIOPressure",
diff --git a/src/shared/condition.h b/src/shared/condition.h
index 91af2027df..1f5c33284c 100644
--- a/src/shared/condition.h
+++ b/src/shared/condition.h
@@ -21,6 +21,7 @@ typedef enum ConditionType {
CONDITION_ENVIRONMENT,
CONDITION_CPU_FEATURE,
CONDITION_OS_RELEASE,
+ CONDITION_MACHINE_TAG,
CONDITION_MEMORY_PRESSURE,
CONDITION_CPU_PRESSURE,
CONDITION_IO_PRESSURE,
diff --git a/src/test/test-condition.c b/src/test/test-condition.c
index bee1014c1c..4f450ab12d 100644
--- a/src/test/test-condition.c
+++ b/src/test/test-condition.c
@@ -1414,6 +1414,69 @@ TEST(condition_test_os_release) {
condition_free(condition);
}
+TEST(condition_test_machine_tag) {
+ Condition *condition;
+
+ /* etc_machine_info() caches the path on first use, so redirect it before anything reads it and
+ * rewrite the same file (truncating) for each scenario rather than switching paths. */
+ _cleanup_free_ char *saved = NULL;
+ ASSERT_OK(free_and_strdup(&saved, getenv("SYSTEMD_ETC_MACHINE_INFO")));
+
+ _cleanup_(rm_rf_physical_and_freep) char *d = NULL;
+ ASSERT_OK(mkdtemp_malloc(NULL, &d));
+
+ _cleanup_free_ char *f = path_join(d, "machine-info");
+ ASSERT_NOT_NULL(f);
+ ASSERT_OK_ERRNO(setenv("SYSTEMD_ETC_MACHINE_INFO", f, /* overwrite= */ true));
+
+ ASSERT_OK(write_string_file(f, "TAGS=\"webserver:frontend:berlin\"\n",
+ WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE));
+
+ /* Exact match */
+ ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "webserver", /* trigger= */ false, /* negate= */ false)));
+ ASSERT_OK_POSITIVE(condition_test(condition, environ));
+ condition_free(condition);
+
+ /* Glob match */
+ ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "front*", /* trigger= */ false, /* negate= */ false)));
+ ASSERT_OK_POSITIVE(condition_test(condition, environ));
+ condition_free(condition);
+
+ /* No match */
+ ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "database", /* trigger= */ false, /* negate= */ false)));
+ ASSERT_OK_ZERO(condition_test(condition, environ));
+ condition_free(condition);
+
+ /* Negation matches when the tag is absent, ... */
+ ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "database", /* trigger= */ false, /* negate= */ true)));
+ ASSERT_OK_POSITIVE(condition_test(condition, environ));
+ condition_free(condition);
+
+ /* ... and does not match when the tag is present */
+ ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "webserver", /* trigger= */ false, /* negate= */ true)));
+ ASSERT_OK_ZERO(condition_test(condition, environ));
+ condition_free(condition);
+
+ /* Invalid tags in the file are ignored (matching the Tags D-Bus property) */
+ ASSERT_OK(write_string_file(f, "TAGS=\"in valid:good\"\n",
+ WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE));
+ ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "in valid", /* trigger= */ false, /* negate= */ false)));
+ ASSERT_OK_ZERO(condition_test(condition, environ));
+ condition_free(condition);
+ ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "good", /* trigger= */ false, /* negate= */ false)));
+ ASSERT_OK_POSITIVE(condition_test(condition, environ));
+ condition_free(condition);
+
+ /* No tags configured at all → never matches */
+ ASSERT_OK(write_string_file(f, "PRETTY_HOSTNAME=\"x\"\n",
+ WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE));
+ ASSERT_NOT_NULL((condition = condition_new(CONDITION_MACHINE_TAG, "webserver", /* trigger= */ false, /* negate= */ false)));
+ ASSERT_OK_ZERO(condition_test(condition, environ));
+ condition_free(condition);
+
+ ASSERT_OK(set_unset_env("SYSTEMD_ETC_MACHINE_INFO", saved, /* overwrite= */ true));
+}
+
TEST(condition_test_psi) {
Condition *condition;
CGroupMask mask;
diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service
index 9fedef5a63..0d6fadcc96 100644
--- a/test/fuzz/fuzz-unit-file/directives-all.service
+++ b/test/fuzz/fuzz-unit-file/directives-all.service
@@ -24,6 +24,7 @@ AssertHost=
AssertIOPressure=
AssertKernelCommandLine=
AssertKernelVersion=
+AssertMachineTag=
AssertMemoryPressure=
AssertNeedsUpdate=
AssertOSRelease=
@@ -72,6 +73,7 @@ ConditionHost=
ConditionIOPressure=
ConditionKernelCommandLine=
ConditionKernelVersion=
+ConditionMachineTag=
ConditionMemoryPressure=
ConditionNeedsUpdate=
ConditionOSRelease=