core: better errors and more fields for io.systemd.Unit.StartTransient (#42161)
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. --- 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).
This commit is contained in:
+125
-24
@@ -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);
|
||||
}
|
||||
@@ -753,7 +761,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) {
|
||||
@@ -861,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 },
|
||||
{}
|
||||
};
|
||||
|
||||
@@ -885,7 +898,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 +924,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)
|
||||
@@ -961,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);
|
||||
@@ -972,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);
|
||||
@@ -1003,30 +1035,93 @@ 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;
|
||||
}
|
||||
|
||||
/* 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 (!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)
|
||||
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)) {
|
||||
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;
|
||||
|
||||
unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE, "Nice", "Nice=%i", p->nice);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -1046,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);
|
||||
@@ -1122,6 +1220,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;
|
||||
@@ -1177,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)
|
||||
@@ -1195,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)
|
||||
@@ -1207,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);
|
||||
|
||||
|
||||
@@ -519,184 +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"
|
||||
|
||||
# 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"
|
||||
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"
|
||||
# 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"
|
||||
# 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"
|
||||
# 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"
|
||||
# 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"
|
||||
# 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"
|
||||
# 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
|
||||
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"
|
||||
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=(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user