From 4f0271e92324a790a4f9c4932a35f871293637dd Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Thu, 30 Apr 2026 09:18:41 +0200 Subject: [PATCH 1/4] core: report unsupported service fields in varlink calls Just like for the unsupported/bad exec_fields we should show a message about what field is bad for service parameters. This commit adds it using the same pattern. The JSON parser works in fail-fast mode so we only display the first bad field (and it depends on the parser what it finds first). --- src/core/varlink-unit.c | 7 +++++-- test/units/TEST-26-SYSTEMCTL.sh | 13 +++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/core/varlink-unit.c b/src/core/varlink-unit.c index 964efa3534..c90ff987e8 100644 --- a/src/core/varlink-unit.c +++ b/src/core/varlink-unit.c @@ -753,7 +753,8 @@ typedef struct StartTransientContextParameters { const char *description; TransientExecContextParameters exec; TransientServiceParameters service; - const char *bad_exec_field; /* Set by inner Exec dispatcher to the unknown sub-property name */ + const char *bad_exec_field; /* Set by inner Exec dispatcher to the unknown sub-property name */ + const char *bad_service_field; } StartTransientContextParameters; static void start_transient_context_parameters_done(StartTransientContextParameters *p) { @@ -885,7 +886,7 @@ static int dispatch_transient_service(const char *name, sd_json_variant *variant StartTransientContextParameters *p = ASSERT_PTR(userdata); p->service.present = true; - return sd_json_dispatch(variant, service_dispatch, /* flags= */ 0, &p->service); + return sd_json_dispatch_full(variant, service_dispatch, /* bad= */ NULL, /* flags= */ 0, &p->service, &p->bad_service_field); } static int dispatch_transient_context(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { @@ -911,6 +912,8 @@ static int dispatch_transient_context(const char *name, sd_json_variant *variant * actual sub-property. */ if (streq(bad_field, "Exec") && !isempty(p->context.bad_exec_field)) p->unsupported_property = strjoin("Exec.", p->context.bad_exec_field); + else if (streq(bad_field, "Service") && !isempty(p->context.bad_service_field)) + p->unsupported_property = strjoin("Service.", p->context.bad_service_field); else p->unsupported_property = strdup(bad_field); if (!p->unsupported_property) diff --git a/test/units/TEST-26-SYSTEMCTL.sh b/test/units/TEST-26-SYSTEMCTL.sh index a98ef7646d..d489cc3ec8 100755 --- a/test/units/TEST-26-SYSTEMCTL.sh +++ b/test/units/TEST-26-SYSTEMCTL.sh @@ -690,8 +690,17 @@ varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ '{"context":{"ID":"varlink-transient-exec.slice","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false}}}}' |& grep "io.systemd.Unit.UnitTypeNotSupported" # Unknown field in Exec is rejected as PropertyNotSupported defer_transient_cleanup varlink-transient-unknown-exec.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-unknown-exec.service","Exec":{"RootDirectory":"/tmp"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "io.systemd.Unit.PropertyNotSupported" +unsupported_exec=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-unknown-exec.service","Exec":{"RootDirectory":"/tmp"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true) +echo "$unsupported_exec" | grep "io.systemd.Unit.PropertyNotSupported" +echo "$unsupported_exec" | grep "Exec.RootDirectory" +# Service field declared in the IDL but not yet settable at creation is rejected as PropertyNotSupported, +# and the offending sub-property is identified +defer_transient_cleanup varlink-transient-unknown-service.service +unsupported_service=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-unknown-service.service","Service":{"Type":"oneshot","Restart":"always","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true) +echo "$unsupported_service" | grep "io.systemd.Unit.PropertyNotSupported" +echo "$unsupported_service" | grep "Service.Restart" set -o pipefail transient_cleanup From ee7bd1d8a0c7dd237adc0fa1c7b12dd63969ab42 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 18 May 2026 18:32:23 +0200 Subject: [PATCH 2/4] core: add User,Group,SupplementaryGroups,Nice to varlink Unit.StartTransient This commit adds more writable fields to the io.systemd.Unit.StartTransient varlink method. With this its possible to set: User,Group,SupplementaryGroups,Nice values. Plus tests for them. --- src/core/varlink-unit.c | 73 ++++++++++++++++++++++++++++++--- test/units/TEST-26-SYSTEMCTL.sh | 28 +++++++++++++ 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/src/core/varlink-unit.c b/src/core/varlink-unit.c index c90ff987e8..da0c0d234a 100644 --- a/src/core/varlink-unit.c +++ b/src/core/varlink-unit.c @@ -18,12 +18,14 @@ #include "manager.h" #include "path-util.h" #include "pidref.h" +#include "process-util.h" #include "selinux-access.h" #include "service.h" #include "set.h" #include "strv.h" #include "unit-name.h" #include "unit.h" +#include "user-util.h" #include "varlink-automount.h" #include "varlink-cgroup.h" #include "varlink-common.h" @@ -685,6 +687,11 @@ typedef struct TransientExecContextParameters { bool set_credentials_encrypted_set; TransientSetCredential *set_credentials_encrypted; size_t n_set_credentials_encrypted; + + const char *user; + const char *group; + char **supplementary_groups; + int nice; /* INT_MAX means unset */ } TransientExecContextParameters; static void transient_set_credential_array_free(TransientSetCredential *items, size_t n) { @@ -696,6 +703,7 @@ static void transient_set_credential_array_free(TransientSetCredential *items, s static void transient_exec_context_parameters_done(TransientExecContextParameters *p) { assert(p); strv_free(p->environment); + strv_free(p->supplementary_groups); transient_set_credential_array_free(p->set_credentials, p->n_set_credentials); transient_set_credential_array_free(p->set_credentials_encrypted, p->n_set_credentials_encrypted); } @@ -862,12 +870,16 @@ static int dispatch_transient_set_credential_encrypted(const char *name, sd_json } static int dispatch_transient_exec_context(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { - /* Key names compatible with D-Bus property names */ + /* Key names compatible with D-Bus property names. */ static const sd_json_dispatch_field exec_dispatch[] = { - { "WorkingDirectory", SD_JSON_VARIANT_OBJECT, dispatch_transient_working_directory, 0, 0 }, - { "Environment", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_transient_environment, 0, 0 }, - { "SetCredential", SD_JSON_VARIANT_ARRAY, dispatch_transient_set_credential, 0, 0 }, - { "SetCredentialEncrypted", SD_JSON_VARIANT_ARRAY, dispatch_transient_set_credential_encrypted, 0, 0 }, + { "Environment", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_transient_environment, 0, 0 }, + { "Group", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(TransientExecContextParameters, group), SD_JSON_RELAX }, + { "Nice", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_int32, offsetof(TransientExecContextParameters, nice), 0 }, + { "SetCredential", SD_JSON_VARIANT_ARRAY, dispatch_transient_set_credential, 0, 0 }, + { "SetCredentialEncrypted", SD_JSON_VARIANT_ARRAY, dispatch_transient_set_credential_encrypted, 0, 0 }, + { "SupplementaryGroups", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(TransientExecContextParameters, supplementary_groups), 0 }, + { "User", SD_JSON_VARIANT_STRING, json_dispatch_const_user_group_name, offsetof(TransientExecContextParameters, user), SD_JSON_RELAX }, + { "WorkingDirectory", SD_JSON_VARIANT_OBJECT, dispatch_transient_working_directory, 0, 0 }, {} }; @@ -1026,6 +1038,56 @@ static int transient_exec_context_apply_properties(Unit *u, ExecContext *c, Tran return r; } + /* User/Group names already validated by json_dispatch_const_user_group_name() in the dispatch table: + * either NULL or a non-empty validated name. */ + if (p->user) { + r = free_and_strdup(&c->user, p->user); + if (r < 0) + return r; + + unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE|UNIT_ESCAPE_SPECIFIERS, "User", + "User=%s", p->user); + } + + if (p->group) { + r = free_and_strdup(&c->group, p->group); + if (r < 0) + return r; + + unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE|UNIT_ESCAPE_SPECIFIERS, "Group", + "Group=%s", p->group); + } + + if (p->supplementary_groups) { + _cleanup_free_ char *joined = NULL; + + STRV_FOREACH(g, p->supplementary_groups) + if (!isempty(*g) && !valid_user_group_name(*g, VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid supplementary group name: %s", *g); + + r = strv_extend_strv(&c->supplementary_groups, p->supplementary_groups, /* filter_duplicates= */ true); + if (r < 0) + return r; + + joined = strv_join(p->supplementary_groups, " "); + if (!joined) + return -ENOMEM; + + unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE|UNIT_ESCAPE_SPECIFIERS, "SupplementaryGroups", + "SupplementaryGroups=%s", joined); + } + + if (p->nice != INT_MAX) { + if (!nice_is_valid(p->nice)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid Nice= value: %i", p->nice); + + c->nice = p->nice; + c->nice_set = true; + + unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE, "Nice", "Nice=%i", p->nice); + } + return 0; } @@ -1125,6 +1187,7 @@ int vl_method_start_transient_unit(sd_varlink *link, sd_json_variant *parameters .notify_unit_changes = -1, .context.service.type = _SERVICE_TYPE_INVALID, .context.service.remain_after_exit = -1, + .context.exec.nice = INT_MAX, }; Manager *manager = ASSERT_PTR(userdata); const char *bad_field = NULL; diff --git a/test/units/TEST-26-SYSTEMCTL.sh b/test/units/TEST-26-SYSTEMCTL.sh index d489cc3ec8..93afa655a2 100755 --- a/test/units/TEST-26-SYSTEMCTL.sh +++ b/test/units/TEST-26-SYSTEMCTL.sh @@ -656,6 +656,26 @@ timeout 30 bash -c "until systemctl is-active varlink-transient-cred.service; do grep '^secret-value$' "$CRED_OUTPUT" >/dev/null rm -f "$CRED_OUTPUT" +# Exec.User, Exec.Group, Exec.SupplementaryGroups, Exec.Nice +# The nobody group is different on different distros so resolve here. +NOBODY_GROUP=$(id -gn nobody) +defer_transient_cleanup varlink-transient-ids.service +ids_payload=$(jq -cn --arg g "$NOBODY_GROUP" \ + '{context:{ID:"varlink-transient-ids.service", + Exec:{User:"nobody",Group:$g,SupplementaryGroups:[$g],Nice:5}, + Service:{Type:"oneshot",RemainAfterExit:true, + ExecStart:[{path:"/bin/true"}]}}}') +result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient "$ids_payload") +echo "$result" | jq -e '.context.Exec.User == "nobody"' +echo "$result" | jq --arg g "$NOBODY_GROUP" -e '.context.Exec.Group == $g' +echo "$result" | jq --arg g "$NOBODY_GROUP" -e '.context.Exec.SupplementaryGroups == [$g]' +echo "$result" | jq -e '.context.Exec.Nice == 5' +timeout 30 bash -c 'until systemctl is-active varlink-transient-ids.service; do sleep 0.5; done' +systemctl show -P User varlink-transient-ids.service | grep '^nobody$' >/dev/null +systemctl show -P Group varlink-transient-ids.service | grep "^${NOBODY_GROUP}$" >/dev/null +systemctl show -P SupplementaryGroups varlink-transient-ids.service | grep "${NOBODY_GROUP}" >/dev/null +systemctl show -P Nice varlink-transient-ids.service | grep '^5$' >/dev/null + # Error cases: verify specific varlink error types set +o pipefail varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ @@ -677,6 +697,14 @@ varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ defer_transient_cleanup varlink-transient-bad-env.service varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ '{"context":{"ID":"varlink-transient-bad-env.service","Exec":{"Environment":["not_an_env_var"]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "io.systemd.Unit.BadUnitSetting" +# Invalid User= name is rejected at JSON dispatch time as a parameter error +defer_transient_cleanup varlink-transient-bad-user.service +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-bad-user.service","Exec":{"User":"bad/user"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "Invalid argument" +# Out-of-range Nice= value is rejected +defer_transient_cleanup varlink-transient-bad-nice.service +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-bad-nice.service","Exec":{"Nice":100},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "io.systemd.Unit.BadUnitSetting" # Invalid credential ID defer_transient_cleanup varlink-transient-bad-cred-id.service varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ From ddedddffde27e5834e4a90edcbc6e70aedf218ca Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 20 May 2026 08:52:20 +0200 Subject: [PATCH 3/4] core: tweak error handling around VARLINK_ERROR_UNIT_BAD_SETTING This commit move the erros in StartTransient from VARLINK_ERROR_UNIT_BAD_SETTING to SD_VARLINK_ERROR_INVALID_PARAMETER and it also ensures we have the bad field in the error. Thanks to Ivan Kruglov for suggesting this. --- src/core/varlink-unit.c | 73 ++++++++++++++++++++++++--------- test/units/TEST-26-SYSTEMCTL.sh | 50 ++++++++++++++-------- 2 files changed, 88 insertions(+), 35 deletions(-) diff --git a/src/core/varlink-unit.c b/src/core/varlink-unit.c index da0c0d234a..142de0db8f 100644 --- a/src/core/varlink-unit.c +++ b/src/core/varlink-unit.c @@ -976,7 +976,12 @@ static int transient_apply_set_credentials( return 0; } -static int transient_exec_context_apply_properties(Unit *u, ExecContext *c, TransientExecContextParameters *p) { +static int transient_exec_context_apply_properties( + Unit *u, + ExecContext *c, + TransientExecContextParameters *p, + const char **reterr_field) { + int r; assert(u); @@ -987,23 +992,35 @@ static int transient_exec_context_apply_properties(Unit *u, ExecContext *c, Tran TransientWorkingDirectory *wd = &p->working_directory; _cleanup_free_ char *simplified = NULL; - if (wd->home && wd->path) + if (wd->home && wd->path) { + if (reterr_field) + *reterr_field = "Exec.WorkingDirectory"; return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "WorkingDirectory: 'home' and 'path' are mutually exclusive"); - if (!wd->home && !wd->path) + } + if (!wd->home && !wd->path) { + if (reterr_field) + *reterr_field = "Exec.WorkingDirectory"; return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "WorkingDirectory: must specify either 'home' or 'path'"); + } if (!wd->home) { - if (!path_is_absolute(wd->path)) + if (!path_is_absolute(wd->path)) { + if (reterr_field) + *reterr_field = "Exec.WorkingDirectory"; return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "WorkingDirectory: expects an absolute path"); + } r = path_simplify_alloc(wd->path, &simplified); if (r < 0) return r; - if (!path_is_normalized(simplified)) + if (!path_is_normalized(simplified)) { + if (reterr_field) + *reterr_field = "Exec.WorkingDirectory"; return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "WorkingDirectory: expects a normalized path"); + } } free_and_replace(c->working_directory, simplified); @@ -1018,22 +1035,29 @@ static int transient_exec_context_apply_properties(Unit *u, ExecContext *c, Tran if (p->environment_set) { r = exec_context_apply_environment(u, c, p->environment, UNIT_RUNTIME|UNIT_PRIVATE); - if (r == -E2BIG) - return log_debug_errno(r, "Too many environment assignments."); - if (r == -EINVAL) - return log_debug_errno(r, "Invalid Environment list."); + if (IN_SET(r, -E2BIG, -EINVAL)) { + if (reterr_field) + *reterr_field = "Exec.Environment"; + /* Convert E2BIG into EINVAL to ensure upper layers get the right error context */ + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + r == -E2BIG ? "Too many environment assignments." : "Invalid Environment list."); + } if (r < 0) return r; } if (p->set_credentials_set) { r = transient_apply_set_credentials(u, c, p->set_credentials, p->n_set_credentials, /* encrypted= */ false); + if (r == -EINVAL && reterr_field) + *reterr_field = "Exec.SetCredential"; if (r < 0) return r; } if (p->set_credentials_encrypted_set) { r = transient_apply_set_credentials(u, c, p->set_credentials_encrypted, p->n_set_credentials_encrypted, /* encrypted= */ true); + if (r == -EINVAL && reterr_field) + *reterr_field = "Exec.SetCredentialEncrypted"; if (r < 0) return r; } @@ -1062,9 +1086,12 @@ static int transient_exec_context_apply_properties(Unit *u, ExecContext *c, Tran _cleanup_free_ char *joined = NULL; STRV_FOREACH(g, p->supplementary_groups) - if (!isempty(*g) && !valid_user_group_name(*g, VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX)) + if (!valid_user_group_name(*g, VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX)) { + if (reterr_field) + *reterr_field = "Exec.SupplementaryGroups"; return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid supplementary group name: %s", *g); + } r = strv_extend_strv(&c->supplementary_groups, p->supplementary_groups, /* filter_duplicates= */ true); if (r < 0) @@ -1079,8 +1106,11 @@ static int transient_exec_context_apply_properties(Unit *u, ExecContext *c, Tran } if (p->nice != INT_MAX) { - if (!nice_is_valid(p->nice)) + if (!nice_is_valid(p->nice)) { + if (reterr_field) + *reterr_field = "Exec.Nice"; return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid Nice= value: %i", p->nice); + } c->nice = p->nice; c->nice_set = true; @@ -1091,7 +1121,7 @@ static int transient_exec_context_apply_properties(Unit *u, ExecContext *c, Tran return 0; } -static int transient_service_apply_properties(Service *s, TransientServiceParameters *sp) { +static int transient_service_apply_properties(Service *s, TransientServiceParameters *sp, const char **reterr_field) { Unit *u = UNIT(ASSERT_PTR(s)); int r; @@ -1111,8 +1141,11 @@ static int transient_service_apply_properties(Service *s, TransientServiceParame _cleanup_(exec_command_freep) ExecCommand *c = NULL; _cleanup_strv_free_ char **argv = NULL; - if (!filename_or_absolute_path_is_valid(item->path)) + if (!filename_or_absolute_path_is_valid(item->path)) { + if (reterr_field) + *reterr_field = "Service.ExecStart"; return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid ExecStart path: %s", item->path); + } if (!strv_isempty(item->arguments)) { argv = strv_copy(item->arguments); @@ -1243,16 +1276,17 @@ int vl_method_start_transient_unit(sd_varlink *link, sd_json_variant *parameters /* Apply unit-level properties from context */ r = transient_unit_apply_properties(u, &p.context); if (r == -EINVAL) - return sd_varlink_error(link, VARLINK_ERROR_UNIT_BAD_SETTING, NULL); + return sd_varlink_error_invalid_parameter_name(link, "context"); if (r < 0) return sd_varlink_error_errno(link, r); /* Apply exec-specific properties from context.Exec */ ExecContext *c = unit_get_exec_context(u); if (c) { - r = transient_exec_context_apply_properties(u, c, &p.context.exec); + bad_field = NULL; + r = transient_exec_context_apply_properties(u, c, &p.context.exec, &bad_field); if (r == -EINVAL) - return sd_varlink_error(link, VARLINK_ERROR_UNIT_BAD_SETTING, NULL); + return sd_varlink_error_invalid_parameter_name(link, bad_field ?: "Exec"); if (r < 0) return sd_varlink_error_errno(link, r); } else if (p.context.exec.present) @@ -1261,9 +1295,10 @@ int vl_method_start_transient_unit(sd_varlink *link, sd_json_variant *parameters /* Apply service-specific properties from context.Service */ Service *s = SERVICE(u); if (s) { - r = transient_service_apply_properties(s, &p.context.service); + bad_field = NULL; + r = transient_service_apply_properties(s, &p.context.service, &bad_field); if (r == -EINVAL) - return sd_varlink_error(link, VARLINK_ERROR_UNIT_BAD_SETTING, NULL); + return sd_varlink_error_invalid_parameter_name(link, bad_field ?: "Service"); if (r < 0) return sd_varlink_error_errno(link, r); } else if (p.context.service.present) @@ -1273,7 +1308,7 @@ int vl_method_start_transient_unit(sd_varlink *link, sd_json_variant *parameters manager_dispatch_load_queue(manager); if (u->load_state == UNIT_BAD_SETTING) - return sd_varlink_error(link, VARLINK_ERROR_UNIT_BAD_SETTING, NULL); + return sd_varlink_error_invalid_parameter_name(link, "context"); if (!UNIT_IS_LOAD_COMPLETE(u->load_state)) return sd_varlink_error(link, VARLINK_ERROR_UNIT_NO_SUCH_UNIT, NULL); diff --git a/test/units/TEST-26-SYSTEMCTL.sh b/test/units/TEST-26-SYSTEMCTL.sh index 93afa655a2..d50ae68ef8 100755 --- a/test/units/TEST-26-SYSTEMCTL.sh +++ b/test/units/TEST-26-SYSTEMCTL.sh @@ -682,37 +682,55 @@ varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ '{"context":{"ID":"varlink-transient-exists.service","Service":{"ExecStart":[{"path":"/usr/bin/sleep","arguments":["/usr/bin/sleep","infinity"]}]}}}' |& grep "io.systemd.Unit.UnitExists" varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ '{"context":{"ID":"varlink-transient-test.target","Description":"test"}}' |& grep "io.systemd.Unit.UnitTypeNotSupported" +# Apply-time and dispatch-time validation errors both surface as +# org.varlink.service.InvalidParameter, with the offending field name in the +# response parameters. Use --graceful to treat the expected error as success +# so jq can assert on the dumped parameters JSON directly. +expect_invalid_parameter() { + local payload="$1" field="$2" + varlinkctl call --graceful=org.varlink.service.InvalidParameter \ + "$MANAGER_SOCKET" io.systemd.Unit.StartTransient "$payload" \ + | jq -e --arg f "$field" '.parameter == $f' >/dev/null +} defer_transient_cleanup varlink-transient-bad.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-bad.service","Service":{"Type":"simple"}}}' |& grep "io.systemd.Unit.BadUnitSetting" +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad.service","Service":{"Type":"simple"}}}' \ + "context" # Invalid ExecStart path: exercises filename_or_absolute_path_is_valid() in transient_service_apply_properties() defer_transient_cleanup varlink-transient-badpath.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-badpath.service","Service":{"Type":"simple","ExecStart":[{"path":""}]}}}' |& grep "io.systemd.Unit.BadUnitSetting" +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-badpath.service","Service":{"Type":"simple","ExecStart":[{"path":""}]}}}' \ + "Service.ExecStart" # Relative WorkingDirectory path is rejected defer_transient_cleanup varlink-transient-bad-wd.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-bad-wd.service","Exec":{"WorkingDirectory":{"path":"relative/path","missingOK":false}},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "io.systemd.Unit.BadUnitSetting" +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-wd.service","Exec":{"WorkingDirectory":{"path":"relative/path","missingOK":false}},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "Exec.WorkingDirectory" # Malformed environment entry (not KEY=VALUE) defer_transient_cleanup varlink-transient-bad-env.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-bad-env.service","Exec":{"Environment":["not_an_env_var"]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "io.systemd.Unit.BadUnitSetting" +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-env.service","Exec":{"Environment":["not_an_env_var"]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "Exec.Environment" # Invalid User= name is rejected at JSON dispatch time as a parameter error defer_transient_cleanup varlink-transient-bad-user.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-bad-user.service","Exec":{"User":"bad/user"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "Invalid argument" +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-user.service","Exec":{"User":"bad/user"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "context" # Out-of-range Nice= value is rejected defer_transient_cleanup varlink-transient-bad-nice.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-bad-nice.service","Exec":{"Nice":100},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "io.systemd.Unit.BadUnitSetting" +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-nice.service","Exec":{"Nice":100},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "Exec.Nice" # Invalid credential ID defer_transient_cleanup varlink-transient-bad-cred-id.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-bad-cred-id.service","Exec":{"SetCredential":[{"id":"bad/id","value":"YWJj"}]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "io.systemd.Unit.BadUnitSetting" +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-cred-id.service","Exec":{"SetCredential":[{"id":"bad/id","value":"YWJj"}]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "Exec.SetCredential" # Invalid base64 value for credential (rejected at JSON dispatch time as a parameter error) defer_transient_cleanup varlink-transient-bad-cred-value.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-bad-cred-value.service","Exec":{"SetCredential":[{"id":"mycred","value":"!!!not_base64!!!"}]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "Invalid argument" +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-cred-value.service","Exec":{"SetCredential":[{"id":"mycred","value":"!!!not_base64!!!"}]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "context" # Exec on a unit type without an exec context (.slice) is rejected varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ '{"context":{"ID":"varlink-transient-exec.slice","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false}}}}' |& grep "io.systemd.Unit.UnitTypeNotSupported" From 619ea8f57344068250d46d598184d4de61dbd099 Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Wed, 20 May 2026 11:47:00 +0200 Subject: [PATCH 4/4] core: move StartTransient varlink tests to the right place This commit moves the io.systemd.Unit.StartTransient tests into the right place in TEST-74-AUX-UTILS.varlinkctl-unit.sh. Thanks to Ivan Kruglov for suggesting this. --- test/units/TEST-26-SYSTEMCTL.sh | 233 ------------------ .../TEST-74-AUX-UTILS.varlinkctl-unit.sh | 233 ++++++++++++++++++ 2 files changed, 233 insertions(+), 233 deletions(-) diff --git a/test/units/TEST-26-SYSTEMCTL.sh b/test/units/TEST-26-SYSTEMCTL.sh index d50ae68ef8..75feedd3b0 100755 --- a/test/units/TEST-26-SYSTEMCTL.sh +++ b/test/units/TEST-26-SYSTEMCTL.sh @@ -519,239 +519,6 @@ systemctl show -P Markers "$UNIT_NAME" | grep needs-stop (! systemctl show -P Markers "$UNIT_NAME" | grep needs-reload) varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties "{\"runtime\": true, \"name\": \"$UNIT_NAME\", \"properties\": {\"Markers\": []}}" -# Test io.systemd.Unit.StartTransient -MANAGER_SOCKET="/run/systemd/io.systemd.Manager" - -TRANSIENT_UNITS=() -defer_transient_cleanup() { - TRANSIENT_UNITS+=("$1") -} -transient_cleanup() { - for u in "${TRANSIENT_UNITS[@]}"; do - systemctl stop "$u" 2>/dev/null || true - systemctl reset-failed "$u" 2>/dev/null || true - done -} -trap transient_cleanup EXIT - -# Basic oneshot transient service -defer_transient_cleanup varlink-transient-test.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-test.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | grep '"context"' >/dev/null -echo "$result" | grep '"runtime"' >/dev/null - -# Wait for completion -timeout 30 bash -c 'until systemctl show -P ActiveState varlink-transient-test.service | grep inactive >/dev/null; do sleep 0.5; done' -systemctl show -P Result varlink-transient-test.service | grep success >/dev/null - -# With explicit mode -defer_transient_cleanup varlink-transient-test2.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-test2.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"mode":"fail"}') -echo "$result" | grep '"context"' >/dev/null - -# Streaming with notifyJobChanges: should get intermediate state updates and a final result -# Note: use --slurp + any() rather than 'select() -e' because in jq 1.6 (shipped on -# CentOS 9) -e checks only the last input record's output, so a select() that filters -# out the trailing record makes jq exit non-zero even when earlier records match. -defer_transient_cleanup varlink-transient-test3.service -result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-test3.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"notifyJobChanges":true}') -printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.State == "waiting")' >/dev/null -printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.Result == "done")' >/dev/null - -# Fire-and-forget: --more without notify flags should return immediately with context+runtime -defer_transient_cleanup varlink-transient-fireforget.service -result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-fireforget.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}') -printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .context)' >/dev/null -printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .runtime)' >/dev/null - -# Streaming with notifyUnitChanges: should get unit state change notifications -defer_transient_cleanup varlink-transient-unitnotify.service -result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-unitnotify.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"notifyUnitChanges":true}') -printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .runtime.ActiveState)' >/dev/null - -# Streaming with both notifyJobChanges and notifyUnitChanges -defer_transient_cleanup varlink-transient-both.service -result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-both.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"notifyJobChanges":true,"notifyUnitChanges":true}') -printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.State)' >/dev/null -printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .runtime.ActiveState)' >/dev/null -printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.Result == "done")' >/dev/null - -# prepare for the error case below: create a long-running service, then try to create it again while it's active -defer_transient_cleanup varlink-transient-exists.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-exists.service","Service":{"ExecStart":[{"path":"/usr/bin/sleep","arguments":["/usr/bin/sleep","infinity"]}]}}}' -timeout 10 bash -c 'until systemctl is-active varlink-transient-exists.service; do sleep 0.5; done' - -# Multiple ExecStart commands (oneshot allows multiple) -defer_transient_cleanup varlink-transient-multi.service -result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-multi.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"},{"path":"/bin/true"}]}},"notifyJobChanges":true}') -printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.Result == "done")' >/dev/null - -# Transient service with Description and RemainAfterExit -defer_transient_cleanup varlink-transient-desc.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-desc.service","Description":"Test description property","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.Description == "Test description property"' -echo "$result" | jq -e '.context.Service.Type == "oneshot"' -echo "$result" | jq -e '.context.Service.RemainAfterExit == true' -echo "$result" | jq -e '.context.Service.ExecStart[0].path == "/bin/true"' -echo "$result" | jq -e '.runtime' - -# Transient service with explicit arguments -defer_transient_cleanup varlink-transient-args.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-args.service","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/echo","arguments":["/bin/echo","hello"]}]}}}') -echo "$result" | jq -e '.context' -echo "$result" | jq -e '.runtime' -echo "$result" | jq -e '.context.Service.ExecStart[0].path == "/bin/echo"' -echo "$result" | jq -e '.context.Service.ExecStart[0].arguments == ["/bin/echo", "hello"]' -timeout 30 bash -c 'until systemctl is-active varlink-transient-args.service; do sleep 0.5; done' - -# Verify that omitting arguments defaults argv[0] to the path -defer_transient_cleanup varlink-transient-noargs.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-noargs.service","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.Service.ExecStart[0].arguments == ["/bin/true"]' -timeout 30 bash -c 'until systemctl is-active varlink-transient-noargs.service; do sleep 0.5; done' - -# Exec.WorkingDirectory and Exec.Environment -defer_transient_cleanup varlink-transient-exec.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-exec.service","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false},"Environment":["FOO=bar","BAZ=qux"]},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.Exec.WorkingDirectory.path == "/tmp"' -echo "$result" | jq -e '.context.Exec.Environment | index("FOO=bar") != null' -echo "$result" | jq -e '.context.Exec.Environment | index("BAZ=qux") != null' -timeout 30 bash -c 'until systemctl is-active varlink-transient-exec.service; do sleep 0.5; done' -systemctl show -P WorkingDirectory varlink-transient-exec.service | grep '^/tmp$' >/dev/null -systemctl show -P Environment varlink-transient-exec.service | grep 'FOO=bar' >/dev/null -systemctl show -P Environment varlink-transient-exec.service | grep 'BAZ=qux' >/dev/null - -# WorkingDirectory with missingOK=true (path does not exist but unit still starts) -defer_transient_cleanup varlink-transient-wd-missing.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-wd-missing.service","Exec":{"WorkingDirectory":{"path":"/nonexistent/path","missingOK":true}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' -timeout 30 bash -c 'until systemctl is-active varlink-transient-wd-missing.service; do sleep 0.5; done' - -# WorkingDirectory with home=true, missingOK omitted (defaults to false) -defer_transient_cleanup varlink-transient-wd-home.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-wd-home.service","Exec":{"WorkingDirectory":{"home":true}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' -timeout 30 bash -c 'until systemctl is-active varlink-transient-wd-home.service; do sleep 0.5; done' -systemctl show -P WorkingDirectory varlink-transient-wd-home.service | grep '^~$' >/dev/null - -# Exec.SetCredential: pass a credential and verify the running process can read it -defer_transient_cleanup varlink-transient-cred.service -CRED_VALUE_B64=$(printf 'secret-value' | base64 -w0) -CRED_OUTPUT=$(mktemp) -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - "{\"context\":{\"ID\":\"varlink-transient-cred.service\",\"Exec\":{\"SetCredential\":[{\"id\":\"mycred\",\"value\":\"${CRED_VALUE_B64}\"}]},\"Service\":{\"Type\":\"oneshot\",\"RemainAfterExit\":true,\"ExecStart\":[{\"path\":\"/bin/sh\",\"arguments\":[\"/bin/sh\",\"-c\",\"cat \$CREDENTIALS_DIRECTORY/mycred > ${CRED_OUTPUT}\"]}]}}}" -timeout 30 bash -c "until systemctl is-active varlink-transient-cred.service; do sleep 0.5; done" -grep '^secret-value$' "$CRED_OUTPUT" >/dev/null -rm -f "$CRED_OUTPUT" - -# Exec.User, Exec.Group, Exec.SupplementaryGroups, Exec.Nice -# The nobody group is different on different distros so resolve here. -NOBODY_GROUP=$(id -gn nobody) -defer_transient_cleanup varlink-transient-ids.service -ids_payload=$(jq -cn --arg g "$NOBODY_GROUP" \ - '{context:{ID:"varlink-transient-ids.service", - Exec:{User:"nobody",Group:$g,SupplementaryGroups:[$g],Nice:5}, - Service:{Type:"oneshot",RemainAfterExit:true, - ExecStart:[{path:"/bin/true"}]}}}') -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient "$ids_payload") -echo "$result" | jq -e '.context.Exec.User == "nobody"' -echo "$result" | jq --arg g "$NOBODY_GROUP" -e '.context.Exec.Group == $g' -echo "$result" | jq --arg g "$NOBODY_GROUP" -e '.context.Exec.SupplementaryGroups == [$g]' -echo "$result" | jq -e '.context.Exec.Nice == 5' -timeout 30 bash -c 'until systemctl is-active varlink-transient-ids.service; do sleep 0.5; done' -systemctl show -P User varlink-transient-ids.service | grep '^nobody$' >/dev/null -systemctl show -P Group varlink-transient-ids.service | grep "^${NOBODY_GROUP}$" >/dev/null -systemctl show -P SupplementaryGroups varlink-transient-ids.service | grep "${NOBODY_GROUP}" >/dev/null -systemctl show -P Nice varlink-transient-ids.service | grep '^5$' >/dev/null - -# Error cases: verify specific varlink error types -set +o pipefail -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-exists.service","Service":{"ExecStart":[{"path":"/usr/bin/sleep","arguments":["/usr/bin/sleep","infinity"]}]}}}' |& grep "io.systemd.Unit.UnitExists" -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-test.target","Description":"test"}}' |& grep "io.systemd.Unit.UnitTypeNotSupported" -# Apply-time and dispatch-time validation errors both surface as -# org.varlink.service.InvalidParameter, with the offending field name in the -# response parameters. Use --graceful to treat the expected error as success -# so jq can assert on the dumped parameters JSON directly. -expect_invalid_parameter() { - local payload="$1" field="$2" - varlinkctl call --graceful=org.varlink.service.InvalidParameter \ - "$MANAGER_SOCKET" io.systemd.Unit.StartTransient "$payload" \ - | jq -e --arg f "$field" '.parameter == $f' >/dev/null -} -defer_transient_cleanup varlink-transient-bad.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad.service","Service":{"Type":"simple"}}}' \ - "context" -# Invalid ExecStart path: exercises filename_or_absolute_path_is_valid() in transient_service_apply_properties() -defer_transient_cleanup varlink-transient-badpath.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-badpath.service","Service":{"Type":"simple","ExecStart":[{"path":""}]}}}' \ - "Service.ExecStart" -# Relative WorkingDirectory path is rejected -defer_transient_cleanup varlink-transient-bad-wd.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-wd.service","Exec":{"WorkingDirectory":{"path":"relative/path","missingOK":false}},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "Exec.WorkingDirectory" -# Malformed environment entry (not KEY=VALUE) -defer_transient_cleanup varlink-transient-bad-env.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-env.service","Exec":{"Environment":["not_an_env_var"]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "Exec.Environment" -# Invalid User= name is rejected at JSON dispatch time as a parameter error -defer_transient_cleanup varlink-transient-bad-user.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-user.service","Exec":{"User":"bad/user"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "context" -# Out-of-range Nice= value is rejected -defer_transient_cleanup varlink-transient-bad-nice.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-nice.service","Exec":{"Nice":100},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "Exec.Nice" -# Invalid credential ID -defer_transient_cleanup varlink-transient-bad-cred-id.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-cred-id.service","Exec":{"SetCredential":[{"id":"bad/id","value":"YWJj"}]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "Exec.SetCredential" -# Invalid base64 value for credential (rejected at JSON dispatch time as a parameter error) -defer_transient_cleanup varlink-transient-bad-cred-value.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-cred-value.service","Exec":{"SetCredential":[{"id":"mycred","value":"!!!not_base64!!!"}]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "context" -# Exec on a unit type without an exec context (.slice) is rejected -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-exec.slice","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false}}}}' |& grep "io.systemd.Unit.UnitTypeNotSupported" -# Unknown field in Exec is rejected as PropertyNotSupported -defer_transient_cleanup varlink-transient-unknown-exec.service -unsupported_exec=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-unknown-exec.service","Exec":{"RootDirectory":"/tmp"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true) -echo "$unsupported_exec" | grep "io.systemd.Unit.PropertyNotSupported" -echo "$unsupported_exec" | grep "Exec.RootDirectory" -# Service field declared in the IDL but not yet settable at creation is rejected as PropertyNotSupported, -# and the offending sub-property is identified -defer_transient_cleanup varlink-transient-unknown-service.service -unsupported_service=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-unknown-service.service","Service":{"Type":"oneshot","Restart":"always","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true) -echo "$unsupported_service" | grep "io.systemd.Unit.PropertyNotSupported" -echo "$unsupported_service" | grep "Service.Restart" -set -o pipefail - -transient_cleanup -trap - EXIT - # --dry-run with destructive verbs # kexec is skipped intentionally, as it requires a bit more involved setup VERBS=( diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh index 2311e14a14..c631c6f889 100755 --- a/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh +++ b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh @@ -81,3 +81,236 @@ varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$timer_par testuser_uid=$(id -u testuser) systemd-run --wait --pipe --user --machine testuser@ \ varlinkctl --more call "/run/user/$testuser_uid/systemd/io.systemd.Manager" io.systemd.Unit.List '{}' + +# Test io.systemd.Unit.StartTransient +MANAGER_SOCKET="/run/systemd/io.systemd.Manager" + +TRANSIENT_UNITS=() +defer_transient_cleanup() { + TRANSIENT_UNITS+=("$1") +} +transient_cleanup() { + for u in "${TRANSIENT_UNITS[@]}"; do + systemctl stop "$u" 2>/dev/null || true + systemctl reset-failed "$u" 2>/dev/null || true + done +} +trap transient_cleanup EXIT + +# Basic oneshot transient service +defer_transient_cleanup varlink-transient-test.service +result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-test.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}') +echo "$result" | grep '"context"' >/dev/null +echo "$result" | grep '"runtime"' >/dev/null + +# Wait for completion +timeout 30 bash -c 'until systemctl show -P ActiveState varlink-transient-test.service | grep inactive >/dev/null; do sleep 0.5; done' +systemctl show -P Result varlink-transient-test.service | grep success >/dev/null + +# With explicit mode +defer_transient_cleanup varlink-transient-test2.service +result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-test2.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"mode":"fail"}') +echo "$result" | grep '"context"' >/dev/null + +# Streaming with notifyJobChanges: should get intermediate state updates and a final result +# Note: use --slurp + any() rather than 'select() -e' because in jq 1.6 (shipped on +# CentOS 9) -e checks only the last input record's output, so a select() that filters +# out the trailing record makes jq exit non-zero even when earlier records match. +defer_transient_cleanup varlink-transient-test3.service +result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-test3.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"notifyJobChanges":true}') +printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.State == "waiting")' >/dev/null +printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.Result == "done")' >/dev/null + +# Fire-and-forget: --more without notify flags should return immediately with context+runtime +defer_transient_cleanup varlink-transient-fireforget.service +result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-fireforget.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}') +printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .context)' >/dev/null +printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .runtime)' >/dev/null + +# Streaming with notifyUnitChanges: should get unit state change notifications +defer_transient_cleanup varlink-transient-unitnotify.service +result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-unitnotify.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"notifyUnitChanges":true}') +printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .runtime.ActiveState)' >/dev/null + +# Streaming with both notifyJobChanges and notifyUnitChanges +defer_transient_cleanup varlink-transient-both.service +result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-both.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}},"notifyJobChanges":true,"notifyUnitChanges":true}') +printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.State)' >/dev/null +printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .runtime.ActiveState)' >/dev/null +printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.Result == "done")' >/dev/null + +# prepare for the error case below: create a long-running service, then try to create it again while it's active +defer_transient_cleanup varlink-transient-exists.service +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-exists.service","Service":{"ExecStart":[{"path":"/usr/bin/sleep","arguments":["/usr/bin/sleep","infinity"]}]}}}' +timeout 10 bash -c 'until systemctl is-active varlink-transient-exists.service; do sleep 0.5; done' + +# Multiple ExecStart commands (oneshot allows multiple) +defer_transient_cleanup varlink-transient-multi.service +result=$(varlinkctl call --more "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-multi.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"},{"path":"/bin/true"}]}},"notifyJobChanges":true}') +printf '%s' "$result" | jq --seq --slurp -e 'any(.[]; .job.Result == "done")' >/dev/null + +# Transient service with Description and RemainAfterExit +defer_transient_cleanup varlink-transient-desc.service +result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-desc.service","Description":"Test description property","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') +echo "$result" | jq -e '.context.Description == "Test description property"' +echo "$result" | jq -e '.context.Service.Type == "oneshot"' +echo "$result" | jq -e '.context.Service.RemainAfterExit == true' +echo "$result" | jq -e '.context.Service.ExecStart[0].path == "/bin/true"' +echo "$result" | jq -e '.runtime' + +# Transient service with explicit arguments +defer_transient_cleanup varlink-transient-args.service +result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-args.service","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/echo","arguments":["/bin/echo","hello"]}]}}}') +echo "$result" | jq -e '.context' +echo "$result" | jq -e '.runtime' +echo "$result" | jq -e '.context.Service.ExecStart[0].path == "/bin/echo"' +echo "$result" | jq -e '.context.Service.ExecStart[0].arguments == ["/bin/echo", "hello"]' +timeout 30 bash -c 'until systemctl is-active varlink-transient-args.service; do sleep 0.5; done' + +# Verify that omitting arguments defaults argv[0] to the path +defer_transient_cleanup varlink-transient-noargs.service +result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-noargs.service","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') +echo "$result" | jq -e '.context.Service.ExecStart[0].arguments == ["/bin/true"]' +timeout 30 bash -c 'until systemctl is-active varlink-transient-noargs.service; do sleep 0.5; done' + +# Exec.WorkingDirectory and Exec.Environment +defer_transient_cleanup varlink-transient-exec.service +result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-exec.service","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false},"Environment":["FOO=bar","BAZ=qux"]},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') +echo "$result" | jq -e '.context.Exec.WorkingDirectory.path == "/tmp"' +echo "$result" | jq -e '.context.Exec.Environment | index("FOO=bar") != null' +echo "$result" | jq -e '.context.Exec.Environment | index("BAZ=qux") != null' +timeout 30 bash -c 'until systemctl is-active varlink-transient-exec.service; do sleep 0.5; done' +systemctl show -P WorkingDirectory varlink-transient-exec.service | grep '^/tmp$' >/dev/null +systemctl show -P Environment varlink-transient-exec.service | grep 'FOO=bar' >/dev/null +systemctl show -P Environment varlink-transient-exec.service | grep 'BAZ=qux' >/dev/null + +# WorkingDirectory with missingOK=true (path does not exist but unit still starts) +defer_transient_cleanup varlink-transient-wd-missing.service +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-wd-missing.service","Exec":{"WorkingDirectory":{"path":"/nonexistent/path","missingOK":true}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' +timeout 30 bash -c 'until systemctl is-active varlink-transient-wd-missing.service; do sleep 0.5; done' + +# WorkingDirectory with home=true, missingOK omitted (defaults to false) +defer_transient_cleanup varlink-transient-wd-home.service +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-wd-home.service","Exec":{"WorkingDirectory":{"home":true}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' +timeout 30 bash -c 'until systemctl is-active varlink-transient-wd-home.service; do sleep 0.5; done' +systemctl show -P WorkingDirectory varlink-transient-wd-home.service | grep '^~$' >/dev/null + +# Exec.SetCredential: pass a credential and verify the running process can read it +defer_transient_cleanup varlink-transient-cred.service +CRED_VALUE_B64=$(printf 'secret-value' | base64 -w0) +CRED_OUTPUT=$(mktemp) +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + "{\"context\":{\"ID\":\"varlink-transient-cred.service\",\"Exec\":{\"SetCredential\":[{\"id\":\"mycred\",\"value\":\"${CRED_VALUE_B64}\"}]},\"Service\":{\"Type\":\"oneshot\",\"RemainAfterExit\":true,\"ExecStart\":[{\"path\":\"/bin/sh\",\"arguments\":[\"/bin/sh\",\"-c\",\"cat \$CREDENTIALS_DIRECTORY/mycred > ${CRED_OUTPUT}\"]}]}}}" +timeout 30 bash -c "until systemctl is-active varlink-transient-cred.service; do sleep 0.5; done" +grep '^secret-value$' "$CRED_OUTPUT" >/dev/null +rm -f "$CRED_OUTPUT" + +# Exec.User, Exec.Group, Exec.SupplementaryGroups, Exec.Nice +# The nobody group is different on different distros so resolve here. +NOBODY_GROUP=$(id -gn nobody) +defer_transient_cleanup varlink-transient-ids.service +ids_payload=$(jq -cn --arg g "$NOBODY_GROUP" \ + '{context:{ID:"varlink-transient-ids.service", + Exec:{User:"nobody",Group:$g,SupplementaryGroups:[$g],Nice:5}, + Service:{Type:"oneshot",RemainAfterExit:true, + ExecStart:[{path:"/bin/true"}]}}}') +result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient "$ids_payload") +echo "$result" | jq -e '.context.Exec.User == "nobody"' +echo "$result" | jq --arg g "$NOBODY_GROUP" -e '.context.Exec.Group == $g' +echo "$result" | jq --arg g "$NOBODY_GROUP" -e '.context.Exec.SupplementaryGroups == [$g]' +echo "$result" | jq -e '.context.Exec.Nice == 5' +timeout 30 bash -c 'until systemctl is-active varlink-transient-ids.service; do sleep 0.5; done' +systemctl show -P User varlink-transient-ids.service | grep '^nobody$' >/dev/null +systemctl show -P Group varlink-transient-ids.service | grep "^${NOBODY_GROUP}$" >/dev/null +systemctl show -P SupplementaryGroups varlink-transient-ids.service | grep "${NOBODY_GROUP}" >/dev/null +systemctl show -P Nice varlink-transient-ids.service | grep '^5$' >/dev/null + +# Error cases: verify specific varlink error types +set +o pipefail +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-exists.service","Service":{"ExecStart":[{"path":"/usr/bin/sleep","arguments":["/usr/bin/sleep","infinity"]}]}}}' |& grep "io.systemd.Unit.UnitExists" +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-test.target","Description":"test"}}' |& grep "io.systemd.Unit.UnitTypeNotSupported" +# Apply-time and dispatch-time validation errors both surface as +# org.varlink.service.InvalidParameter, with the offending field name in the +# response parameters. Use --graceful to treat the expected error as success +# so jq can assert on the dumped parameters JSON directly. +expect_invalid_parameter() { + local payload="$1" field="$2" + varlinkctl call --graceful=org.varlink.service.InvalidParameter \ + "$MANAGER_SOCKET" io.systemd.Unit.StartTransient "$payload" \ + | jq -e --arg f "$field" '.parameter == $f' >/dev/null +} +defer_transient_cleanup varlink-transient-bad.service +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad.service","Service":{"Type":"simple"}}}' \ + "context" +# Invalid ExecStart path: exercises filename_or_absolute_path_is_valid() in transient_service_apply_properties() +defer_transient_cleanup varlink-transient-badpath.service +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-badpath.service","Service":{"Type":"simple","ExecStart":[{"path":""}]}}}' \ + "Service.ExecStart" +# Relative WorkingDirectory path is rejected +defer_transient_cleanup varlink-transient-bad-wd.service +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-wd.service","Exec":{"WorkingDirectory":{"path":"relative/path","missingOK":false}},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "Exec.WorkingDirectory" +# Malformed environment entry (not KEY=VALUE) +defer_transient_cleanup varlink-transient-bad-env.service +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-env.service","Exec":{"Environment":["not_an_env_var"]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "Exec.Environment" +# Invalid User= name is rejected at JSON dispatch time as a parameter error +defer_transient_cleanup varlink-transient-bad-user.service +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-user.service","Exec":{"User":"bad/user"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "context" +# Out-of-range Nice= value is rejected +defer_transient_cleanup varlink-transient-bad-nice.service +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-nice.service","Exec":{"Nice":100},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "Exec.Nice" +# Invalid credential ID +defer_transient_cleanup varlink-transient-bad-cred-id.service +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-cred-id.service","Exec":{"SetCredential":[{"id":"bad/id","value":"YWJj"}]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "Exec.SetCredential" +# Invalid base64 value for credential (rejected at JSON dispatch time as a parameter error) +defer_transient_cleanup varlink-transient-bad-cred-value.service +expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-cred-value.service","Exec":{"SetCredential":[{"id":"mycred","value":"!!!not_base64!!!"}]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "context" +# Exec on a unit type without an exec context (.slice) is rejected +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-exec.slice","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false}}}}' |& grep "io.systemd.Unit.UnitTypeNotSupported" +# Unknown field in Exec is rejected as PropertyNotSupported +defer_transient_cleanup varlink-transient-unknown-exec.service +unsupported_exec=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-unknown-exec.service","Exec":{"RootDirectory":"/tmp"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true) +echo "$unsupported_exec" | grep "io.systemd.Unit.PropertyNotSupported" +echo "$unsupported_exec" | grep "Exec.RootDirectory" +# Service field declared in the IDL but not yet settable at creation is rejected as PropertyNotSupported, +# and the offending sub-property is identified +defer_transient_cleanup varlink-transient-unknown-service.service +unsupported_service=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-unknown-service.service","Service":{"Type":"oneshot","Restart":"always","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true) +echo "$unsupported_service" | grep "io.systemd.Unit.PropertyNotSupported" +echo "$unsupported_service" | grep "Service.Restart" +set -o pipefail + +transient_cleanup +trap - EXIT