]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
condition: add a condition that matches against the machine tags
authorLennart Poettering <lennart@amutable.com>
Wed, 20 May 2026 21:23:15 +0000 (23:23 +0200)
committerLennart Poettering <lennart@amutable.com>
Thu, 21 May 2026 16:30:16 +0000 (18:30 +0200)
man/systemd.unit.xml
src/core/load-fragment-gperf.gperf.in
src/shared/condition.c
src/shared/condition.h
src/test/test-condition.c
test/fuzz/fuzz-unit-file/directives-all.service

index 5c3439d193bfe7a82cd0d850630a45abab632212..2990e658911529fa768375a2ab102fdd1b141f10 100644 (file)
           </listitem>
         </varlistentry>
 
+        <varlistentry>
+          <term><varname>ConditionMachineTag=</varname></term>
+
+          <listitem><para><varname>ConditionMachineTag=</varname> 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 <varname>TAGS=</varname> field of
+          <citerefentry><refentrytitle>machine-info</refentrytitle><manvolnum>5</manvolnum></citerefentry> and
+          may be queried and changed with the <command>tags</command> command of
+          <citerefentry><refentrytitle>hostnamectl</refentrytitle><manvolnum>1</manvolnum></citerefentry>. The
+          argument is a single tag pattern, which is compared against each of the configured tags using
+          shell-style globbing (<literal>*</literal>, <literal>?</literal>, <literal>[]</literal>). 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.</para>
+
+          <xi:include href="version-info.xml" xpointer="v261"/>
+          </listitem>
+        </varlistentry>
+
         <varlistentry>
           <term><varname>ConditionMemoryPressure=</varname></term>
           <term><varname>ConditionCPUPressure=</varname></term>
           <term><varname>AssertCPUs=</varname></term>
           <term><varname>AssertCPUFeature=</varname></term>
           <term><varname>AssertOSRelease=</varname></term>
+          <term><varname>AssertMachineTag=</varname></term>
           <term><varname>AssertMemoryPressure=</varname></term>
           <term><varname>AssertCPUPressure=</varname></term>
           <term><varname>AssertIOPressure=</varname></term>
index be074230ce5c1222ca9ef49c9c801129617091c9..60fec56f46a7b52cd9777689e4efac3f2b9f2035 100644 (file)
@@ -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)
index d0e41bbf716886c95214fb0d85e220cad0e1446a..ae33c18ade04cfbf6c2c071d41599b97583548ec 100644 (file)
@@ -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",
index 91af2027df6159baf539cd3b213f369147a4d2d2..1f5c33284c3fe92bcb7d4999f12765a5b6d617d8 100644 (file)
@@ -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,
index bee1014c1cbf2d44f212fdbef44a2683e4ed5348..4f450ab12d8d4030b6386fa9edd35f062819671b 100644 (file)
@@ -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;
index 9fedef5a63a9ec7ec81f19590e3ded90f1b139e8..0d6fadcc967edb94ee97179714a4aa696c79ecdf 100644 (file)
@@ -24,6 +24,7 @@ AssertHost=
 AssertIOPressure=
 AssertKernelCommandLine=
 AssertKernelVersion=
+AssertMachineTag=
 AssertMemoryPressure=
 AssertNeedsUpdate=
 AssertOSRelease=
@@ -72,6 +73,7 @@ ConditionHost=
 ConditionIOPressure=
 ConditionKernelCommandLine=
 ConditionKernelVersion=
+ConditionMachineTag=
 ConditionMemoryPressure=
 ConditionNeedsUpdate=
 ConditionOSRelease=