]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
pid1: extend "ConditionFirmware=" for checking SMBIOS system identification information
authorDaniel Braunwarth <daniel@braunwarth.dev>
Sat, 30 Jul 2022 13:20:15 +0000 (15:20 +0200)
committerLuca Boccassi <luca.boccassi@gmail.com>
Thu, 25 Aug 2022 20:44:21 +0000 (21:44 +0100)
NEWS
man/systemd.unit.xml
src/shared/condition.c
src/test/test-condition.c

diff --git a/NEWS b/NEWS
index 685a894d3a0e88fca5e2415eea5a9b3ebc510243..f6f49cb902051b6be6e5733ccd0dd886f17b4bc8 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -44,6 +44,12 @@ CHANGES WITH 252 in spe:
 
         * C.UTF-8 is used as the default locale if nothing else has been configured.
 
+        * Extend [Condition|Assert]Firmware= to conditionalize on certain SMBIOS
+          fields. For example
+          ConditionFirmware=smbios-field(board_name = "Custom Board") will
+          conditionalize a unit so that it is only run when
+          /sys/class/dmi/id/board_name contains "Custom Board" (without quotes).
+
         Changes in sd-boot, bootctl, and the Boot Loader Specification:
 
         * The Boot Loader Specification has been cleaned up and clarified.
index 95f1b98cbd13a13246ef9724774e27f0acada948..a9114fb353f1dc717fa744c3fca61e7b6f06fdb9 100644 (file)
         <varlistentry>
           <term><varname>ConditionFirmware=</varname></term>
 
-          <listitem><para>Check whether the system's firmware is of a certain type. Possible values are:
-          <literal>uefi</literal> (for systems with EFI),
-          <literal>device-tree</literal> (for systems with a device tree) and
-          <literal>device-tree-compatible(xyz)</literal> (for systems with a device tree that is compatible to <literal>xyz</literal>).</para>
+          <listitem><para>Check whether the system's firmware is of a certain type. Multiple values are possible.</para>
+
+          <para><literal>uefi</literal> for systems with EFI.</para>
+
+          <para><literal>device-tree</literal> for systems with a device tree.</para>
+
+          <para><literal>device-tree-compatible(<replaceable>value</replaceable>)</literal> for systems with a device tree that is compatible to
+          <literal>value</literal>.</para>
+
+          <para><literal>smbios-field(<replaceable>field</replaceable> <replaceable>operator</replaceable> <replaceable>value</replaceable>)</literal>
+          for systems with a SMBIOS field containing a certain value.
+          <literal>field</literal> is the name of the SMBIOS field exposed as <literal>sysfs</literal> attribute file
+          below <filename>/sys/class/dmi/id/</filename>.
+          <literal>operator</literal> is one of <literal>&lt;</literal>, <literal>&lt;=</literal>,
+          <literal>&gt;=</literal>, <literal>&gt;</literal>, <literal>=</literal>, <literal>!=</literal> for version
+          comparison, or <literal>=$</literal>, <literal>!=$</literal> for string comparison.
+          <literal>value</literal> is the expected value of the SMBIOS field (shell-style globs are possible if
+          <literal>=$</literal> or<literal>!=$</literal> is used).</para>
           </listitem>
         </varlistentry>
 
index 7a983edfd7a28dcf8c6e1bbbf75a99826e4115af..3a09b493fc3dbbdd773f9f31866bbfa5be99ce61 100644 (file)
@@ -1,5 +1,6 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include <ctype.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <fnmatch.h>
@@ -184,6 +185,10 @@ static int condition_test_credential(Condition *c, char **env) {
 typedef enum {
         /* Listed in order of checking. Note that some comparators are prefixes of others, hence the longest
          * should be listed first. */
+        _ORDER_FNMATCH_FIRST,
+        ORDER_FNMATCH_EQUAL = _ORDER_FNMATCH_FIRST,
+        ORDER_FNMATCH_UNEQUAL,
+        _ORDER_FNMATCH_LAST = ORDER_FNMATCH_UNEQUAL,
         ORDER_LOWER_OR_EQUAL,
         ORDER_GREATER_OR_EQUAL,
         ORDER_LOWER,
@@ -194,8 +199,10 @@ typedef enum {
         _ORDER_INVALID = -EINVAL,
 } OrderOperator;
 
-static OrderOperator parse_order(const char **s) {
+static OrderOperator parse_order(const char **s, bool allow_fnmatch) {
         static const char *const prefix[_ORDER_MAX] = {
+                [ORDER_FNMATCH_EQUAL] = "=$",
+                [ORDER_FNMATCH_UNEQUAL] = "!=$",
                 [ORDER_LOWER_OR_EQUAL] = "<=",
                 [ORDER_GREATER_OR_EQUAL] = ">=",
                 [ORDER_LOWER] = "<",
@@ -209,6 +216,8 @@ static OrderOperator parse_order(const char **s) {
 
                 e = startswith(*s, prefix[i]);
                 if (e) {
+                        if (!allow_fnmatch && (i >= _ORDER_FNMATCH_FIRST && i <= _ORDER_FNMATCH_LAST))
+                                break;
                         *s = e;
                         return i;
                 }
@@ -268,7 +277,7 @@ static int condition_test_kernel_version(Condition *c, char **env) {
                         break;
 
                 s = strstrip(word);
-                order = parse_order(&s);
+                order = parse_order(&s, /* allow_fnmatch= */ false);
                 if (order >= 0) {
                         s += strspn(s, WHITESPACE);
                         if (isempty(s)) {
@@ -329,7 +338,7 @@ static int condition_test_osrelease(Condition *c, char **env) {
                                         "Failed to parse parameter, key/value format expected: %m");
 
                 /* Do not allow whitespace after the separator, as that's not a valid os-release format */
-                order = parse_order(&word);
+                order = parse_order(&word, /* allow_fnmatch= */ false);
                 if (order < 0 || isempty(word) || strchr(WHITESPACE, *word) != NULL)
                         return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
                                         "Failed to parse parameter, key/value format expected: %m");
@@ -366,7 +375,7 @@ static int condition_test_memory(Condition *c, char **env) {
         m = physical_memory();
 
         p = c->parameter;
-        order = parse_order(&p);
+        order = parse_order(&p, /* allow_fnmatch= */ false);
         if (order < 0)
                 order = ORDER_GREATER_OR_EQUAL; /* default to >= check, if nothing is specified. */
 
@@ -392,7 +401,7 @@ static int condition_test_cpus(Condition *c, char **env) {
                 return log_debug_errno(n, "Failed to determine CPUs in affinity mask: %m");
 
         p = c->parameter;
-        order = parse_order(&p);
+        order = parse_order(&p, /* allow_fnmatch= */ false);
         if (order < 0)
                 order = ORDER_GREATER_OR_EQUAL; /* default to >= check, if nothing is specified. */
 
@@ -578,8 +587,62 @@ static int condition_test_firmware_devicetree_compatible(const char *dtcarg) {
         return strv_contains(dtcompatlist, dtcarg);
 }
 
+static int condition_test_firmware_smbios_field(const char *expression) {
+        _cleanup_free_ char *field = NULL, *expected_value = NULL, *actual_value = NULL;
+        OrderOperator operator;
+        int r;
+
+        assert(expression);
+
+        /* Parse SMBIOS field */
+        r = extract_first_word(&expression, &field, "!<=>$", EXTRACT_RETAIN_SEPARATORS);
+        if (r < 0)
+                return r;
+        if (r == 0 || isempty(expression))
+                return -EINVAL;
+
+        /* Remove trailing spaces from SMBIOS field */
+        delete_trailing_chars(field, WHITESPACE);
+
+        /* Parse operator */
+        operator = parse_order(&expression, /* allow_fnmatch= */ true);
+        if (operator < 0)
+                return operator;
+
+        /* Parse expected value */
+        r = extract_first_word(&expression, &expected_value, NULL, EXTRACT_UNQUOTE);
+        if (r < 0)
+                return r;
+        if (r == 0 || !isempty(expression))
+                return -EINVAL;
+
+        /* Read actual value from sysfs */
+        if (!filename_is_valid(field))
+                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid SMBIOS field name");
+
+        const char *p = strjoina("/sys/class/dmi/id/", field);
+        r = read_virtual_file(p, SIZE_MAX, &actual_value, NULL);
+        if (r < 0) {
+                log_debug_errno(r, "Failed to read %s: %m", p);
+                if (r == -ENOENT)
+                        return false;
+                return r;
+        }
+
+        /* Remove trailing newline */
+        delete_trailing_chars(actual_value, WHITESPACE);
+
+        /* Finally compare actual and expected value */
+        if (operator == ORDER_FNMATCH_EQUAL)
+                return fnmatch(expected_value, actual_value, FNM_EXTMATCH) != FNM_NOMATCH;
+        if (operator == ORDER_FNMATCH_UNEQUAL)
+                return fnmatch(expected_value, actual_value, FNM_EXTMATCH) == FNM_NOMATCH;
+        return test_order(strverscmp_improved(actual_value, expected_value), operator);
+}
+
 static int condition_test_firmware(Condition *c, char **env) {
-        sd_char *dtc;
+        sd_char *arg;
+        int r;
 
         assert(c);
         assert(c->parameter);
@@ -592,24 +655,40 @@ static int condition_test_firmware(Condition *c, char **env) {
                         return false;
                 } else
                         return true;
-        } else if ((dtc = startswith(c->parameter, "device-tree-compatible("))) {
-                _cleanup_free_ char *dtcarg = NULL;
+        } else if ((arg = startswith(c->parameter, "device-tree-compatible("))) {
+                _cleanup_free_ char *dtc_arg = NULL;
                 char *end;
 
-                end = strchr(dtc, ')');
+                end = strchr(arg, ')');
                 if (!end || *(end + 1) != '\0') {
-                        log_debug("Malformed Firmware condition \"%s\"", c->parameter);
+                        log_debug("Malformed ConditionFirmware=%s", c->parameter);
                         return false;
                 }
 
-                dtcarg = strndup(dtc, end - dtc);
-                if (!dtcarg)
+                dtc_arg = strndup(arg, end - arg);
+                if (!dtc_arg)
                         return -ENOMEM;
 
-                return condition_test_firmware_devicetree_compatible(dtcarg);
+                return condition_test_firmware_devicetree_compatible(dtc_arg);
         } else if (streq(c->parameter, "uefi"))
                 return is_efi_boot();
-        else {
+        else if ((arg = startswith(c->parameter, "smbios-field("))) {
+                _cleanup_free_ char *smbios_arg = NULL;
+                char *end;
+
+                end = strchr(arg, ')');
+                if (!end || *(end + 1) != '\0')
+                        return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Malformed ConditionFirmware=%s: %m", c->parameter);
+
+                smbios_arg = strndup(arg, end - arg);
+                if (!smbios_arg)
+                        return log_oom_debug();
+
+                r = condition_test_firmware_smbios_field(smbios_arg);
+                if (r < 0)
+                        return log_debug_errno(r, "Malformed ConditionFirmware=%s: %m", c->parameter);
+                return r;
+        } else {
                 log_debug("Unsupported Firmware condition \"%s\"", c->parameter);
                 return false;
         }
index 56b5ad88a251588b88dd94cee279bd453858cea5..6cb889c8d69e10bb5c44a368368dec8536aaf079 100644 (file)
@@ -17,6 +17,7 @@
 #include "efi-loader.h"
 #include "env-util.h"
 #include "errno-util.h"
+#include "fileio.h"
 #include "fs-util.h"
 #include "hostname-util.h"
 #include "id128-util.h"
@@ -305,6 +306,136 @@ TEST(condition_test_architecture) {
         condition_free(condition);
 }
 
+TEST(condition_test_firmware_smbios_field) {
+        _cleanup_free_ char *bios_vendor = NULL, *bios_version = NULL;
+        const char *expression;
+        Condition *condition;
+
+        /* Test some malformed smbios-field arguments */
+        condition = condition_new(CONDITION_FIRMWARE, "smbios-field()", false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == -EINVAL);
+        condition_free(condition);
+
+        condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == -EINVAL);
+        condition_free(condition);
+
+        condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed", false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == -EINVAL);
+        condition_free(condition);
+
+        condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed=)", false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == -EINVAL);
+        condition_free(condition);
+
+        condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed=)", false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == -EINVAL);
+        condition_free(condition);
+
+        condition = condition_new(CONDITION_FIRMWARE, "smbios-field(not_existing=nothing garbage)", false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == -EINVAL);
+        condition_free(condition);
+
+        /* Test not existing SMBIOS field */
+        condition = condition_new(CONDITION_FIRMWARE, "smbios-field(not_existing=nothing)", false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == 0);
+        condition_free(condition);
+
+        /* Test with bios_vendor, if available */
+        if (read_virtual_file("/sys/class/dmi/id/bios_vendor", SIZE_MAX, &bios_vendor, NULL) <= 0)
+                return;
+
+        /* remove trailing newline */
+        strstrip(bios_vendor);
+
+        /* Check if the bios_vendor contains any spaces we should quote */
+        const char *quote = strchr(bios_vendor, ' ') ? "\"" : "";
+
+        /* Test equality / inequality using fnmatch() */
+        expression = strjoina("smbios-field(bios_vendor =$ ", quote,  bios_vendor, quote, ")");
+        condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ));
+        condition_free(condition);
+
+        expression = strjoina("smbios-field(bios_vendor=$", quote, bios_vendor, quote, ")");
+        condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ));
+        condition_free(condition);
+
+        expression = strjoina("smbios-field(bios_vendor !=$ ", quote, bios_vendor, quote, ")");
+        condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == 0);
+        condition_free(condition);
+
+        expression = strjoina("smbios-field(bios_vendor!=$", quote, bios_vendor, quote, ")");
+        condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == 0);
+        condition_free(condition);
+
+        expression = strjoina("smbios-field(bios_vendor =$ ", quote,  bios_vendor, "*", quote, ")");
+        condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ));
+        condition_free(condition);
+
+        /* Test version comparison with bios_version, if available */
+        if (read_virtual_file("/sys/class/dmi/id/bios_version", SIZE_MAX, &bios_version, NULL) <= 0)
+                return;
+
+        /* remove trailing newline */
+        strstrip(bios_version);
+
+        /* Check if the bios_version contains any spaces we should quote */
+        quote = strchr(bios_version, ' ') ? "\"" : "";
+
+        expression = strjoina("smbios-field(bios_version = ", quote, bios_version, quote, ")");
+        condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ));
+        condition_free(condition);
+
+        expression = strjoina("smbios-field(bios_version != ", quote, bios_version, quote, ")");
+        condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == 0);
+        condition_free(condition);
+
+        expression = strjoina("smbios-field(bios_version <= ", quote, bios_version, quote, ")");
+        condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ));
+        condition_free(condition);
+
+        expression = strjoina("smbios-field(bios_version >= ", quote, bios_version, quote, ")");
+        condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ));
+        condition_free(condition);
+
+        expression = strjoina("smbios-field(bios_version < ", quote, bios_version, ".1", quote, ")");
+        condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ));
+        condition_free(condition);
+
+        expression = strjoina("smbios-field(bios_version > ", quote, bios_version, ".1", quote, ")");
+        condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == 0);
+        condition_free(condition);
+}
+
 TEST(condition_test_kernel_command_line) {
         Condition *condition;
         int r;
@@ -427,7 +558,7 @@ TEST(condition_test_kernel_version) {
         assert_se(condition_test(condition, environ) == 0);
         condition_free(condition);
 
-        condition = condition_new(CONDITION_KERNEL_VERSION, ">= 4711.8.15", false, false);
+        condition = condition_new(CONDITION_KERNEL_VERSION, " >= 4711.8.15", false, false);
         assert_se(condition);
         assert_se(condition_test(condition, environ) == 0);
         condition_free(condition);
@@ -1042,6 +1173,19 @@ TEST(condition_test_os_release) {
         assert_se(condition_test(condition, environ) == 0);
         condition_free(condition);
 
+        /* Test fnmatch() operators */
+        key_value_pair = strjoina(os_release_pairs[0], "=$", quote, os_release_pairs[1], quote);
+        condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == -EINVAL);
+        condition_free(condition);
+
+        key_value_pair = strjoina(os_release_pairs[0], "!=$", quote, os_release_pairs[1], quote);
+        condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+        assert_se(condition);
+        assert_se(condition_test(condition, environ) == -EINVAL);
+        condition_free(condition);
+
         /* Some distros (eg: Arch) do not set VERSION_ID */
         if (parse_os_release(NULL, "VERSION_ID", &version_id) <= 0)
                 return;