diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml index 651a73848ee..5dce2ae8fb5 100644 --- a/man/systemd-analyze.xml +++ b/man/systemd-analyze.xml @@ -83,6 +83,12 @@ OPTIONS unit-paths + + systemd-analyze + OPTIONS + condition + CONDITION + systemd-analyze OPTIONS @@ -348,6 +354,33 @@ $ eog targets.svg to retrieve the actual list that the manager uses, with any empty directories omitted. + + <command>systemd-analyze condition <replaceable>CONDITION</replaceable>...</command> + + This command will evaluate Condition*=... and + Assert*=... assignments, and print their values, and + the resulting value of the combined condition set. See + systemd.unit5 + for a list of available conditions and asserts. + + + Evaluate conditions that check kernel versions + + $ systemd-analyze condition 'ConditionKernelVersion = ! <4.0' \ + 'ConditionKernelVersion = >=5.1' \ + 'ConditionACPower=|false' \ + 'ConditionArchitecture=|!arm' \ + 'AssertPathExists=/etc/os-release' +test.service: AssertPathExists=/etc/os-release succeeded. +Asserts succeeded. +test.service: ConditionArchitecture=|!arm succeeded. +test.service: ConditionACPower=|false failed. +test.service: ConditionKernelVersion=>=5.1 succeeded. +test.service: ConditionKernelVersion=!<4.0 succeeded. +Conditions succeeded. + + + <command>systemd-analyze syscall-filter <optional><replaceable>SET</replaceable>...</optional></command> diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml index 81bce696bd3..045931038bb 100644 --- a/man/systemd.unit.xml +++ b/man/systemd.unit.xml @@ -1030,7 +1030,9 @@ exclamation mark, the pipe symbol must be passed first, the exclamation second. Except for ConditionPathIsSymbolicLink=, all path checks follow symlinks. If any of these options is assigned the empty string, the list of conditions is reset completely, all previous - condition settings (of any kind) will have no effect. + condition settings (of any kind) will have no effect. The condition verb of + systemd-analyze1 + can be used to test condition and assert expressions. ConditionArchitecture= may be used to check whether the system is running on a specific @@ -1326,7 +1328,11 @@ Note that neither assertion nor condition expressions result in unit state changes. Also note that both are checked at the time the job is to be executed, i.e. long after depending jobs and it itself were queued. Thus, neither condition nor assertion expressions are suitable for conditionalizing unit - dependencies. + dependencies. + + The condition verb of + systemd-analyze1 + can be used to test condition and assert expressions. diff --git a/src/analyze/analyze-condition.c b/src/analyze/analyze-condition.c new file mode 100644 index 00000000000..d0cefa09920 --- /dev/null +++ b/src/analyze/analyze-condition.c @@ -0,0 +1,155 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include + +#include "analyze-condition.h" +#include "condition.h" +#include "conf-parser.h" +#include "load-fragment.h" +#include "service.h" + +typedef struct condition_definition { + const char *name; + ConfigParserCallback parser; + ConditionType type; +} condition_definition; + +static const condition_definition condition_definitions[] = { + { "ConditionPathExists", config_parse_unit_condition_path, CONDITION_PATH_EXISTS }, + { "ConditionPathExistsGlob", config_parse_unit_condition_path, CONDITION_PATH_EXISTS_GLOB }, + { "ConditionPathIsDirectory", config_parse_unit_condition_path, CONDITION_PATH_IS_DIRECTORY }, + { "ConditionPathIsSymbolicLink", config_parse_unit_condition_path, CONDITION_PATH_IS_SYMBOLIC_LINK }, + { "ConditionPathIsMountPoint", config_parse_unit_condition_path, CONDITION_PATH_IS_MOUNT_POINT }, + { "ConditionPathIsReadWrite", config_parse_unit_condition_path, CONDITION_PATH_IS_READ_WRITE }, + { "ConditionDirectoryNotEmpty", config_parse_unit_condition_path, CONDITION_DIRECTORY_NOT_EMPTY }, + { "ConditionFileNotEmpty", config_parse_unit_condition_path, CONDITION_FILE_NOT_EMPTY }, + { "ConditionFileIsExecutable", config_parse_unit_condition_path, CONDITION_FILE_IS_EXECUTABLE }, + { "ConditionNeedsUpdate", config_parse_unit_condition_path, CONDITION_NEEDS_UPDATE }, + { "ConditionFirstBoot", config_parse_unit_condition_string, CONDITION_FIRST_BOOT }, + { "ConditionKernelCommandLine", config_parse_unit_condition_string, CONDITION_KERNEL_COMMAND_LINE }, + { "ConditionKernelVersion", config_parse_unit_condition_string, CONDITION_KERNEL_VERSION }, + { "ConditionArchitecture", config_parse_unit_condition_string, CONDITION_ARCHITECTURE }, + { "ConditionVirtualization", config_parse_unit_condition_string, CONDITION_VIRTUALIZATION }, + { "ConditionSecurity", config_parse_unit_condition_string, CONDITION_SECURITY }, + { "ConditionCapability", config_parse_unit_condition_string, CONDITION_CAPABILITY }, + { "ConditionHost", config_parse_unit_condition_string, CONDITION_HOST }, + { "ConditionACPower", config_parse_unit_condition_string, CONDITION_AC_POWER }, + { "ConditionUser", config_parse_unit_condition_string, CONDITION_USER }, + { "ConditionGroup", config_parse_unit_condition_string, CONDITION_GROUP }, + { "ConditionControlGroupController", config_parse_unit_condition_string, CONDITION_CONTROL_GROUP_CONTROLLER }, + + { "AssertPathExists", config_parse_unit_condition_path, CONDITION_PATH_EXISTS }, + { "AssertPathExistsGlob", config_parse_unit_condition_path, CONDITION_PATH_EXISTS_GLOB }, + { "AssertPathIsDirectory", config_parse_unit_condition_path, CONDITION_PATH_IS_DIRECTORY }, + { "AssertPathIsSymbolicLink", config_parse_unit_condition_path, CONDITION_PATH_IS_SYMBOLIC_LINK }, + { "AssertPathIsMountPoint", config_parse_unit_condition_path, CONDITION_PATH_IS_MOUNT_POINT }, + { "AssertPathIsReadWrite", config_parse_unit_condition_path, CONDITION_PATH_IS_READ_WRITE }, + { "AssertDirectoryNotEmpty", config_parse_unit_condition_path, CONDITION_DIRECTORY_NOT_EMPTY }, + { "AssertFileNotEmpty", config_parse_unit_condition_path, CONDITION_FILE_NOT_EMPTY }, + { "AssertFileIsExecutable", config_parse_unit_condition_path, CONDITION_FILE_IS_EXECUTABLE }, + { "AssertNeedsUpdate", config_parse_unit_condition_path, CONDITION_NEEDS_UPDATE }, + { "AssertFirstBoot", config_parse_unit_condition_string, CONDITION_FIRST_BOOT }, + { "AssertKernelCommandLine", config_parse_unit_condition_string, CONDITION_KERNEL_COMMAND_LINE }, + { "AssertKernelVersion", config_parse_unit_condition_string, CONDITION_KERNEL_VERSION }, + { "AssertArchitecture", config_parse_unit_condition_string, CONDITION_ARCHITECTURE }, + { "AssertVirtualization", config_parse_unit_condition_string, CONDITION_VIRTUALIZATION }, + { "AssertSecurity", config_parse_unit_condition_string, CONDITION_SECURITY }, + { "AssertCapability", config_parse_unit_condition_string, CONDITION_CAPABILITY }, + { "AssertHost", config_parse_unit_condition_string, CONDITION_HOST }, + { "AssertACPower", config_parse_unit_condition_string, CONDITION_AC_POWER }, + { "AssertUser", config_parse_unit_condition_string, CONDITION_USER }, + { "AssertGroup", config_parse_unit_condition_string, CONDITION_GROUP }, + { "AssertControlGroupController", config_parse_unit_condition_string, CONDITION_CONTROL_GROUP_CONTROLLER }, + + /* deprecated, but we should still parse them */ + { "ConditionNull", config_parse_unit_condition_null, 0 }, + { "AssertNull", config_parse_unit_condition_null, 0 }, +}; + +static int parse_condition(Unit *u, const char *line) { + const char *p; + Condition **target; + + if ((p = startswith(line, "Condition"))) + target = &u->conditions; + else if ((p = startswith(line, "Assert"))) + target = &u->asserts; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot parse \"%s\".", line); + + for (size_t i = 0; i < ELEMENTSOF(condition_definitions); i++) { + const condition_definition *c = &condition_definitions[i]; + + p = startswith(line, c->name); + if (!p) + continue; + p += strspn(p, WHITESPACE); + if (*p != '=') + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected \"=\" in \"%s\".", line); + + p += 1 + strspn(p + 1, WHITESPACE); + + return c->parser(NULL, "(stdin)", 0, NULL, 0, c->name, c->type, p, target, u); + } + + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot parse \"%s\".", line); +} + +_printf_(7, 8) +static int log_helper(void *userdata, int level, int error, const char *file, int line, const char *func, const char *format, ...) { + Unit *u = userdata; + va_list ap; + int r; + + assert(u); + + /* "upgrade" debug messages */ + level = MIN(LOG_INFO, level); + + va_start(ap, format); + r = log_object_internalv(level, error, file, line, func, + NULL, + u->id, + NULL, + NULL, + format, ap); + va_end(ap); + + return r; +} + +int verify_conditions(char **lines, UnitFileScope scope) { + _cleanup_(manager_freep) Manager *m = NULL; + Unit *u; + char **line; + int r, q = 1; + + r = manager_new(scope, MANAGER_TEST_RUN_MINIMAL, &m); + if (r < 0) + return log_error_errno(r, "Failed to initialize manager: %m"); + + log_debug("Starting manager..."); + r = manager_startup(m, NULL, NULL); + if (r < 0) + return r; + + r = unit_new_for_name(m, sizeof(Service), "test.service", &u); + if (r < 0) + return log_error_errno(r, "Failed to create test.service: %m"); + + STRV_FOREACH(line, lines) { + r = parse_condition(u, *line); + if (r < 0) + return r; + } + + r = condition_test_list(u->asserts, assert_type_to_string, log_helper, u); + if (u->asserts) + log_notice("Asserts %s.", r > 0 ? "succeeded" : "failed"); + + q = condition_test_list(u->conditions, condition_type_to_string, log_helper, u); + if (u->conditions) + log_notice("Conditions %s.", q > 0 ? "succeeded" : "failed"); + + return r > 0 && q > 0 ? 0 : -EIO; +} diff --git a/src/analyze/analyze-condition.h b/src/analyze/analyze-condition.h new file mode 100644 index 00000000000..2ef278eb5c8 --- /dev/null +++ b/src/analyze/analyze-condition.h @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ +#pragma once + +#include "install.h" + +int verify_conditions(char **lines, UnitFileScope scope); diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index 5217a92b438..40f54f9d46c 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -13,6 +13,7 @@ #include "sd-bus.h" #include "alloc-util.h" +#include "analyze-condition.h" #include "analyze-security.h" #include "analyze-verify.h" #include "build.h" @@ -1897,6 +1898,10 @@ static int service_watchdogs(int argc, char *argv[], void *userdata) { return 0; } +static int do_condition(int argc, char *argv[], void *userdata) { + return verify_conditions(strv_skip(argv, 1), arg_scope); +} + static int do_verify(int argc, char *argv[], void *userdata) { return verify_units(strv_skip(argv, 1), arg_scope, arg_man, arg_generators); } @@ -1955,6 +1960,7 @@ static int help(int argc, char *argv[], void *userdata) { " cat-config Show configuration file and drop-ins\n" " unit-paths List load directories for units\n" " syscall-filter [NAME...] Print list of syscalls in seccomp filter\n" + " condition CONDITION... Evaluate conditions and asserts\n" " verify FILE... Check unit files for correctness\n" " service-watchdogs [BOOL] Get/set service watchdog state\n" " calendar SPEC... Validate repetitive calendar time events\n" @@ -2157,6 +2163,7 @@ static int run(int argc, char *argv[]) { { "cat-config", 2, VERB_ANY, 0, cat_config }, { "unit-paths", 1, 1, 0, dump_unit_paths }, { "syscall-filter", VERB_ANY, VERB_ANY, 0, dump_syscall_filters }, + { "condition", 2, VERB_ANY, 0, do_condition }, { "verify", 2, VERB_ANY, 0, do_verify }, { "calendar", 2, VERB_ANY, 0, test_calendar }, { "timestamp", 2, VERB_ANY, 0, test_timestamp }, diff --git a/src/analyze/meson.build b/src/analyze/meson.build index 4db4dfa5526..58760d609b3 100644 --- a/src/analyze/meson.build +++ b/src/analyze/meson.build @@ -2,6 +2,8 @@ systemd_analyze_sources = files(''' analyze.c + analyze-condition.c + analyze-condition.h analyze-verify.c analyze-verify.h analyze-security.c diff --git a/src/core/load-fragment-gperf.gperf.m4 b/src/core/load-fragment-gperf.gperf.m4 index f7906b374ac..19ee56662c5 100644 --- a/src/core/load-fragment-gperf.gperf.m4 +++ b/src/core/load-fragment-gperf.gperf.m4 @@ -254,6 +254,7 @@ Unit.SuccessAction, config_parse_emergency_action, 0, Unit.FailureActionExitStatus, config_parse_exit_status, 0, offsetof(Unit, failure_action_exit_status) Unit.SuccessActionExitStatus, config_parse_exit_status, 0, offsetof(Unit, success_action_exit_status) Unit.RebootArgument, config_parse_unit_string_printf, 0, offsetof(Unit, reboot_arg) +m4_dnl Also add any conditions to condition_definitions[] in src/analyze/analyze-condition.c. Unit.ConditionPathExists, config_parse_unit_condition_path, CONDITION_PATH_EXISTS, offsetof(Unit, conditions) Unit.ConditionPathExistsGlob, config_parse_unit_condition_path, CONDITION_PATH_EXISTS_GLOB, offsetof(Unit, conditions) Unit.ConditionPathIsDirectory, config_parse_unit_condition_path, CONDITION_PATH_IS_DIRECTORY, offsetof(Unit, conditions)