diff options
author | Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl> | 2021-06-25 09:22:50 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-25 09:22:50 +0200 |
commit | a2e2917162e7ca587cbad94b5366f5cee0385b29 (patch) | |
tree | 83181f2197b2c1e2fbe26b15ade4f75a3abd7f32 | |
parent | de61a04b188f81a85cdb5c64ddb4987dcd9d30d3 (diff) | |
parent | 1e26f8a60bd0f3e5acf1931968af053eebb9a454 (diff) | |
download | systemd-a2e2917162e7ca587cbad94b5366f5cee0385b29.tar.gz |
Merge pull request #19941 from bluca/condition_os_release
core: add ConditionOSRelease= directive
-rw-r--r-- | man/systemd.unit.xml | 14 | ||||
-rw-r--r-- | src/basic/extract-word.c | 21 | ||||
-rw-r--r-- | src/basic/extract-word.h | 1 | ||||
-rw-r--r-- | src/core/load-fragment-gperf.gperf.in | 2 | ||||
-rw-r--r-- | src/shared/condition.c | 60 | ||||
-rw-r--r-- | src/shared/condition.h | 1 | ||||
-rw-r--r-- | src/test/test-condition.c | 146 | ||||
-rw-r--r-- | src/test/test-extract-word.c | 52 | ||||
-rw-r--r-- | test/fuzz/fuzz-unit-file/directives-all.service | 2 | ||||
-rw-r--r-- | test/fuzz/fuzz-unit-file/directives.service | 2 |
10 files changed, 292 insertions, 9 deletions
diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml index a17b752d54..78ba9731dd 100644 --- a/man/systemd.unit.xml +++ b/man/systemd.unit.xml @@ -1647,6 +1647,19 @@ </varlistentry> <varlistentry> + <term><varname>ConditionOSRelease=</varname></term> + + <listitem><para>Verify that a specific <literal>key=value</literal> pair is set in the host's + <citerefentry><refentrytitle>os-release</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para> + + <para>Other than exact matching with <literal>=</literal>, and <literal>!=</literal>, relative + comparisons are supported for versioned parameters (e.g. <literal>VERSION_ID</literal>). The + comparator can be one of <literal><</literal>, <literal><=</literal>, <literal>=</literal>, + <literal>!=</literal>, <literal>>=</literal> and <literal>></literal>.</para> + </listitem> + </varlistentry> + + <varlistentry> <term><varname>AssertArchitecture=</varname></term> <term><varname>AssertVirtualization=</varname></term> <term><varname>AssertHost=</varname></term> @@ -1673,6 +1686,7 @@ <term><varname>AssertControlGroupController=</varname></term> <term><varname>AssertMemory=</varname></term> <term><varname>AssertCPUs=</varname></term> + <term><varname>AssertOSRelease=</varname></term> <listitem><para>Similar to the <varname>ConditionArchitecture=</varname>, <varname>ConditionVirtualization=</varname>, …, condition settings described above, these settings diff --git a/src/basic/extract-word.c b/src/basic/extract-word.c index 0c440db691..9f9bb0c791 100644 --- a/src/basic/extract-word.c +++ b/src/basic/extract-word.c @@ -51,7 +51,8 @@ int extract_first_word(const char **p, char **ret, const char *separators, Extra goto finish_force_terminate; else if (strchr(separators, c)) { if (flags & EXTRACT_DONT_COALESCE_SEPARATORS) { - (*p)++; + if (!(flags & EXTRACT_RETAIN_SEPARATORS)) + (*p)++; goto finish_force_next; } } else { @@ -153,16 +154,18 @@ int extract_first_word(const char **p, char **ret, const char *separators, Extra break; } else if (strchr(separators, c)) { if (flags & EXTRACT_DONT_COALESCE_SEPARATORS) { - (*p)++; + if (!(flags & EXTRACT_RETAIN_SEPARATORS)) + (*p)++; goto finish_force_next; } - /* Skip additional coalesced separators. */ - for (;; (*p)++, c = **p) { - if (c == 0) - goto finish_force_terminate; - if (!strchr(separators, c)) - break; - } + if (!(flags & EXTRACT_RETAIN_SEPARATORS)) + /* Skip additional coalesced separators. */ + for (;; (*p)++, c = **p) { + if (c == 0) + goto finish_force_terminate; + if (!strchr(separators, c)) + break; + } goto finish; } diff --git a/src/basic/extract-word.h b/src/basic/extract-word.h index f415872fac..c82ad761ef 100644 --- a/src/basic/extract-word.h +++ b/src/basic/extract-word.h @@ -12,6 +12,7 @@ typedef enum ExtractFlags { EXTRACT_UNQUOTE = 1 << 5, /* Ignore separators in quoting with "" and '', and remove the quotes. */ EXTRACT_DONT_COALESCE_SEPARATORS = 1 << 6, /* Don't treat multiple adjacent separators as one */ EXTRACT_RETAIN_ESCAPE = 1 << 7, /* Treat escape character '\' as any other character without special meaning */ + EXTRACT_RETAIN_SEPARATORS = 1 << 8, /* Do not advance the original string pointer past the separator(s) */ /* Note that if no flags are specified, escaped escape characters will be silently stripped. */ } ExtractFlags; diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index 17f2bea5b8..d343145fa9 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -330,6 +330,7 @@ Unit.ConditionEnvironment, config_parse_unit_condition_string, Unit.ConditionUser, config_parse_unit_condition_string, CONDITION_USER, offsetof(Unit, conditions) 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.AssertPathExists, config_parse_unit_condition_path, CONDITION_PATH_EXISTS, offsetof(Unit, asserts) Unit.AssertPathExistsGlob, config_parse_unit_condition_path, CONDITION_PATH_EXISTS_GLOB, offsetof(Unit, asserts) Unit.AssertPathIsDirectory, config_parse_unit_condition_path, CONDITION_PATH_IS_DIRECTORY, offsetof(Unit, asserts) @@ -356,6 +357,7 @@ Unit.AssertEnvironment, config_parse_unit_condition_string, Unit.AssertUser, config_parse_unit_condition_string, CONDITION_USER, offsetof(Unit, asserts) 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.CollectMode, config_parse_collect_mode, 0, offsetof(Unit, collect_mode) Service.PIDFile, config_parse_pid_file, 0, offsetof(Service, pid_file) Service.ExecCondition, config_parse_exec, SERVICE_EXEC_CONDITION, offsetof(Service, exec_command) diff --git a/src/shared/condition.c b/src/shared/condition.c index 55fb636673..ec9d57b292 100644 --- a/src/shared/condition.c +++ b/src/shared/condition.c @@ -24,6 +24,7 @@ #include "cpu-set-util.h" #include "efi-loader.h" #include "env-file.h" +#include "env-util.h" #include "extract-word.h" #include "fd-util.h" #include "fileio.h" @@ -35,6 +36,7 @@ #include "list.h" #include "macro.h" #include "mountpoint-util.h" +#include "os-util.h" #include "parse-util.h" #include "path-util.h" #include "proc-cmdline.h" @@ -263,6 +265,61 @@ static int condition_test_kernel_version(Condition *c, char **env) { return true; } +static int condition_test_osrelease(Condition *c, char **env) { + const char *parameter = c->parameter; + int r; + + assert(c); + assert(c->parameter); + assert(c->type == CONDITION_OS_RELEASE); + + for (;;) { + _cleanup_free_ char *key = NULL, *condition = NULL, *actual_value = NULL; + OrderOperator order; + const char *word; + bool matches; + + r = extract_first_word(¶meter, &condition, NULL, EXTRACT_UNQUOTE); + if (r < 0) + return log_debug_errno(r, "Failed to parse parameter: %m"); + if (r == 0) + break; + + /* parse_order() needs the string to start with the comparators */ + word = condition; + r = extract_first_word(&word, &key, "!<=>", EXTRACT_RETAIN_SEPARATORS); + if (r < 0) + return log_debug_errno(r, "Failed to parse parameter: %m"); + /* The os-release spec mandates env-var-like key names */ + if (r == 0 || isempty(word) || !env_name_is_valid(key)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "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); + 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"); + + r = parse_os_release(NULL, key, &actual_value); + if (r < 0) + return log_debug_errno(r, "Failed to parse os-release: %m"); + + /* Might not be comparing versions, so do exact string matching */ + if (order == ORDER_EQUAL) + matches = streq_ptr(actual_value, word); + else if (order == ORDER_UNEQUAL) + matches = !streq_ptr(actual_value, word); + else + matches = test_order(strverscmp_improved(actual_value, word), order); + + if (!matches) + return false; + } + + return true; +} + static int condition_test_memory(Condition *c, char **env) { OrderOperator order; uint64_t m, k; @@ -935,6 +992,7 @@ int condition_test(Condition *c, char **env) { [CONDITION_MEMORY] = condition_test_memory, [CONDITION_ENVIRONMENT] = condition_test_environment, [CONDITION_CPU_FEATURE] = condition_test_cpufeature, + [CONDITION_OS_RELEASE] = condition_test_osrelease, }; int r, b; @@ -1059,6 +1117,7 @@ static const char* const condition_type_table[_CONDITION_TYPE_MAX] = { [CONDITION_MEMORY] = "ConditionMemory", [CONDITION_ENVIRONMENT] = "ConditionEnvironment", [CONDITION_CPU_FEATURE] = "ConditionCPUFeature", + [CONDITION_OS_RELEASE] = "ConditionOSRelease", }; DEFINE_STRING_TABLE_LOOKUP(condition_type, ConditionType); @@ -1092,6 +1151,7 @@ static const char* const assert_type_table[_CONDITION_TYPE_MAX] = { [CONDITION_MEMORY] = "AssertMemory", [CONDITION_ENVIRONMENT] = "AssertEnvironment", [CONDITION_CPU_FEATURE] = "AssertCPUFeature", + [CONDITION_OS_RELEASE] = "AssertOSRelease", }; DEFINE_STRING_TABLE_LOOKUP(assert_type, ConditionType); diff --git a/src/shared/condition.h b/src/shared/condition.h index 6678689d7c..3a5420c402 100644 --- a/src/shared/condition.h +++ b/src/shared/condition.h @@ -21,6 +21,7 @@ typedef enum ConditionType { CONDITION_CPUS, CONDITION_ENVIRONMENT, CONDITION_CPU_FEATURE, + CONDITION_OS_RELEASE, CONDITION_NEEDS_UPDATE, CONDITION_FIRST_BOOT, diff --git a/src/test/test-condition.c b/src/test/test-condition.c index adba383fdd..d1dee22bd6 100644 --- a/src/test/test-condition.c +++ b/src/test/test-condition.c @@ -23,6 +23,7 @@ #include "log.h" #include "macro.h" #include "nulstr-util.h" +#include "os-util.h" #include "process-util.h" #include "selinux-util.h" #include "set.h" @@ -890,6 +891,150 @@ static void test_condition_test_environment(void) { test_condition_test_environment_one("EXISTINGENVVAR=", false); } +static void test_condition_test_os_release(void) { + _cleanup_strv_free_ char **os_release_pairs = NULL; + _cleanup_free_ char *version_id = NULL; + const char *key_value_pair; + Condition *condition; + + /* Should not happen, but it's a test so we don't know the environment. */ + if (load_os_release_pairs(NULL, &os_release_pairs) < 0) + return; + if (strv_length(os_release_pairs) < 2) + return; + + condition = condition_new(CONDITION_OS_RELEASE, "_THISHOPEFULLYWONTEXIST=01234 56789", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == 0); + condition_free(condition); + + condition = condition_new(CONDITION_OS_RELEASE, "WRONG FORMAT", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + condition = condition_new(CONDITION_OS_RELEASE, "WRONG!<>=FORMAT", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + condition = condition_new(CONDITION_OS_RELEASE, "WRONG FORMAT=", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + condition = condition_new(CONDITION_OS_RELEASE, "WRONG =FORMAT", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + condition = condition_new(CONDITION_OS_RELEASE, "WRONG = FORMAT", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + condition = condition_new(CONDITION_OS_RELEASE, "WRONGFORMAT= ", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + condition = condition_new(CONDITION_OS_RELEASE, "WRO NG=FORMAT", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == -EINVAL); + condition_free(condition); + + condition = condition_new(CONDITION_OS_RELEASE, "", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ)); + condition_free(condition); + + /* load_os_release_pairs() removes quotes, we have to add them back, + * otherwise we get a string: "PRETTY_NAME=Debian GNU/Linux 10 (buster)" + * which is wrong, as the value is not quoted anymore. */ + const char *quote = strchr(os_release_pairs[1], ' ') ? "\"" : ""; + 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)); + 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) == 0); + condition_free(condition); + + /* Some distros (eg: Arch) do not set VERSION_ID */ + if (parse_os_release(NULL, "VERSION_ID", &version_id) <= 0) + return; + + key_value_pair = strjoina("VERSION_ID", "=", version_id); + condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ)); + condition_free(condition); + + key_value_pair = strjoina("VERSION_ID", "!=", version_id); + condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == 0); + condition_free(condition); + + key_value_pair = strjoina("VERSION_ID", "<=", version_id); + condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ)); + condition_free(condition); + + key_value_pair = strjoina("VERSION_ID", ">=", version_id); + condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ)); + condition_free(condition); + + key_value_pair = strjoina("VERSION_ID", "<", version_id, ".1"); + condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ)); + condition_free(condition); + + key_value_pair = strjoina("VERSION_ID", ">", version_id, ".1"); + condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) == 0); + condition_free(condition); + + key_value_pair = strjoina("VERSION_ID", "=", version_id, " ", 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)); + condition_free(condition); + + key_value_pair = strjoina("VERSION_ID", "!=", version_id, " ", 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) == 0); + condition_free(condition); + + key_value_pair = strjoina("VERSION_ID", "=", version_id, " ", 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) == 0); + condition_free(condition); + + key_value_pair = strjoina("VERSION_ID", "!=", version_id, " ", 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) == 0); + condition_free(condition); + + key_value_pair = strjoina("VERSION_ID", "<", version_id, ".1", " ", 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)); + condition_free(condition); +} + int main(int argc, char *argv[]) { test_setup_logging(LOG_DEBUG); @@ -912,6 +1057,7 @@ int main(int argc, char *argv[]) { #if defined(__i386__) || defined(__x86_64__) test_condition_test_cpufeature(); #endif + test_condition_test_os_release(); return 0; } diff --git a/src/test/test-extract-word.c b/src/test/test-extract-word.c index a392ec7588..8cf0b63a4c 100644 --- a/src/test/test-extract-word.c +++ b/src/test/test-extract-word.c @@ -482,6 +482,58 @@ static void test_extract_first_word(void) { assert_se(extract_first_word(&p, &t, ",", EXTRACT_UNQUOTE) > 0); assert_se(streq(t, "context=system_u:object_r:svirt_sandbox_file_t:s0:c0,c1")); free(t); + + p = "a:b"; + assert_se(extract_first_word(&p, &t, ":", EXTRACT_RETAIN_SEPARATORS) == 1); + assert_se(streq(t, "a")); + assert_se(streq(p, ":b")); + free(t); + assert_se(extract_first_word(&p, &t, ":", EXTRACT_RETAIN_SEPARATORS) == 1); + assert_se(streq(t, "b")); + free(t); + + p = "a>:b"; + assert_se(extract_first_word(&p, &t, ">:", EXTRACT_RETAIN_SEPARATORS) == 1); + assert_se(streq(t, "a")); + assert_se(streq(p, ">:b")); + free(t); + assert_se(extract_first_word(&p, &t, ">:", EXTRACT_RETAIN_SEPARATORS) == 1); + assert_se(streq(t, "b")); + free(t); + + p = "a>:b"; + assert_se(extract_first_word(&p, &t, ">:", EXTRACT_RETAIN_SEPARATORS|EXTRACT_DONT_COALESCE_SEPARATORS) == 1); + assert_se(streq(t, "a")); + assert_se(streq(p, ">:b")); + free(t); + assert_se(extract_first_word(&p, &t, ">:", EXTRACT_RETAIN_SEPARATORS|EXTRACT_DONT_COALESCE_SEPARATORS) == 1); + assert_se(streq(t, "")); + assert_se(streq(p, ">:b")); + free(t); + + p = "a\\:b"; + assert_se(extract_first_word(&p, &t, ":", EXTRACT_RETAIN_SEPARATORS|EXTRACT_RETAIN_ESCAPE) == 1); + assert_se(streq(t, "a\\")); + assert_se(streq(p, ":b")); + free(t); + + p = "a\\:b"; + assert_se(extract_first_word(&p, &t, ":", EXTRACT_RETAIN_SEPARATORS) == 1); + assert_se(streq(t, "a:b")); + assert_se(!p); + free(t); + + p = "a\\:b"; + assert_se(extract_first_word(&p, &t, ":", EXTRACT_RETAIN_SEPARATORS|EXTRACT_UNESCAPE_SEPARATORS) == 1); + assert_se(streq(t, "a:b")); + assert_se(!p); + free(t); + + p = "a\\:a:b"; + assert_se(extract_first_word(&p, &t, ":", EXTRACT_RETAIN_SEPARATORS|EXTRACT_UNESCAPE_SEPARATORS) == 1); + assert_se(streq(t, "a:a")); + assert_se(streq(p, ":b")); + free(t); } static void test_extract_first_word_and_warn(void) { diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service index a152bebd73..3039d1c0cd 100644 --- a/test/fuzz/fuzz-unit-file/directives-all.service +++ b/test/fuzz/fuzz-unit-file/directives-all.service @@ -21,6 +21,7 @@ AssertHost= AssertKernelCommandLine= AssertKernelVersion= AssertNeedsUpdate= +AssertOSRelease= AssertPathExists= AssertPathExistsGlob= AssertPathIsDirectory= @@ -64,6 +65,7 @@ ConditionHost= ConditionKernelCommandLine= ConditionKernelVersion= ConditionNeedsUpdate= +ConditionOSRelease= ConditionPathExists= ConditionPathExistsGlob= ConditionPathIsDirectory= diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service index 1ce6967c65..b5df300a6b 100644 --- a/test/fuzz/fuzz-unit-file/directives.service +++ b/test/fuzz/fuzz-unit-file/directives.service @@ -18,6 +18,7 @@ AssertKernelCommandLine= AssertKernelVersion= AssertMemory= AssertNeedsUpdate= +AssertOSRelease= AssertPathExists= AssertPathExistsGlob= AssertPathIsDirectory= @@ -50,6 +51,7 @@ ConditionKernelCommandLine= ConditionKernelVersion= ConditionMemory= ConditionNeedsUpdate= +ConditionOSRelease= ConditionPathExists= ConditionPathExistsGlob= ConditionPathIsDirectory= |