diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl-unit-enqueue-job.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit-enqueue-job.sh new file mode 100755 index 00000000000..f84f0fdde49 --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit-enqueue-job.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +at_exit() { + systemctl stop varlink-test-enqueue{,-2}.service varlink-test-slow.service 2>/dev/null || true + if [[ -n ${watcher_pid:-} ]]; then + kill "$watcher_pid" 2>/dev/null || true + wait "$watcher_pid" 2>/dev/null || true + fi + rm -f /run/systemd/system/varlink-test-enqueue{,-2}.service /run/systemd/system/varlink-test-slow.service + rm -f /tmp/enqueue-notifications.json + systemctl daemon-reload +} + +testcase_transactions() { + trap at_exit EXIT + + for unit in varlink-test-enqueue{,-2}.service; do + cat >"/run/systemd/system/$unit" < 1 and .[0].job.JobType == "restart" and .[-1].job.Result == "done" and .[-1].context and .[-1].runtime' + systemctl is-active varlink-test-enqueue.service + + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service"], "jobType": "stop"}' | jq -e '.job.JobType == "stop"' + timeout 10 bash -c 'until systemctl show -P ActiveState varlink-test-enqueue.service | grep inactive >/dev/null; do sleep 0.5; done' + + # Multiple units share a transaction and receive one reply per job. + varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service", "varlink-test-enqueue-2.service"], "jobType": "start"}' | jq -e 'length == 2 and all(.[]; .context and .runtime and .job.Id) and ([.[].job.Unit] | sort == ["varlink-test-enqueue-2.service", "varlink-test-enqueue.service"])' + timeout 10 bash -c 'until systemctl is-active varlink-test-enqueue.service; do sleep 0.5; done' + timeout 10 bash -c 'until systemctl is-active varlink-test-enqueue-2.service; do sleep 0.5; done' + systemctl stop varlink-test-enqueue{,-2}.service + + # Multiple replies require 'more'. + (! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service", "varlink-test-enqueue-2.service"], "jobType": "start"}') + + # A missing unit rejects the entire transaction. + (! varlinkctl call --more /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service", "non-existent.service"], "jobType": "start"}') + (! systemctl is-active varlink-test-enqueue.service) + + # Notifications require 'more' and a single unit. + (! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service"], "jobType": "start", "notifyJobChanges": true}') + (! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service"], "jobType": "start", "notifyUnitChanges": true}') + + (! varlinkctl call --more /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service", "varlink-test-enqueue-2.service"], "jobType": "start", "notifyJobChanges": true}') + + # Error cases + (! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["invalid-unit-name"], "jobType": "start"}') + (! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": [], "jobType": "start"}') +} + +testcase_coalesced_watcher() { + trap at_exit EXIT + + # Coalesced jobs must reject a second watcher. + cat >/run/systemd/system/varlink-test-slow.service </dev/null 2>&1 & + watcher_pid=$! + + # Wait for the first watcher to attach. + timeout 10 bash -c 'until systemctl show -P ActiveState varlink-test-slow.service | grep activating >/dev/null; do sleep 0.5; done' + + set +o pipefail + varlinkctl call --more /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob \ + '{"names": ["varlink-test-slow.service"], "jobType": "start", "notifyJobChanges": true}' |& grep "io.systemd.Unit.AlreadyBeingWatched" + set -o pipefail + + systemctl stop varlink-test-slow.service + wait "$watcher_pid" 2>/dev/null || true + watcher_pid= +} + +testcase_nop_subscription() { + trap at_exit EXIT + + cat >/run/systemd/system/varlink-test-slow.service </tmp/enqueue-notifications.json & + watcher_pid=$! + + timeout 10 bash -c 'until systemctl show -P ActiveState varlink-test-slow.service | grep activating >/dev/null; do sleep 0.5; done' + + # The pending start job keeps the stream open; each line is a unit notification. + sleep 1 + notifications=$(wc -l /dev/null + + # NOP removal must still trigger a unit notification. + for _ in {1..20}; do + [[ "$(wc -l /dev/null || true + watcher_pid= +} + +run_testcases diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl-unit-start-transient.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit-start-transient.sh new file mode 100755 index 00000000000..95b26cb8c15 --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit-start-transient.sh @@ -0,0 +1,726 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +MANAGER_SOCKET="/run/systemd/io.systemd.Manager" + +at_exit() { + local u pid + + for u in "${TRANSIENT_UNITS[@]}"; do + systemctl stop "$u" 2>/dev/null || true + systemctl reset-failed "$u" 2>/dev/null || true + done + for pid in "${CHILD_PIDS[@]}"; do + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + done + rm -f "${TEMP_FILES[@]}" +} + +setup() { + TRANSIENT_UNITS=() + CHILD_PIDS=() + TEMP_FILES=() + trap at_exit EXIT +} + +defer_transient_cleanup() { + TRANSIENT_UNITS+=("$1") +} + +# 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 +} + +testcase_basic() { + setup + + # 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 + + # 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","CollectMode":"inactive_or_failed","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') + echo "$result" | jq -e '.context.Description == "Test description property"' + echo "$result" | jq -e '.context.CollectMode == "inactive_or_failed"' + 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' + systemctl show -P CollectMode varlink-transient-desc.service | grep '^inactive-or-failed$' >/dev/null + + # 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' + + 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" + + # 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" +} + +testcase_notifications() { + setup + + # 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 +} + +testcase_exec_properties() { + setup + + # 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.StandardInput/StandardOutput/StandardError enum properties and Exec.TTYPath + defer_transient_cleanup varlink-transient-stdio.service + result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-stdio.service","Exec":{"StandardInput":"null","StandardOutput":"null","StandardError":"journal","TTYPath":"/dev/tty2"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') + echo "$result" | jq -e '.context.Exec.StandardInput == "null"' + echo "$result" | jq -e '.context.Exec.StandardOutput == "null"' + echo "$result" | jq -e '.context.Exec.StandardError == "journal"' + echo "$result" | jq -e '.context.Exec.TTYPath == "/dev/tty2"' + timeout 30 bash -c 'until systemctl is-active varlink-transient-stdio.service; do sleep 0.5; done' + systemctl show -P StandardInput varlink-transient-stdio.service | grep '^null$' >/dev/null + systemctl show -P StandardOutput varlink-transient-stdio.service | grep '^null$' >/dev/null + systemctl show -P StandardError varlink-transient-stdio.service | grep '^journal$' >/dev/null + systemctl show -P TTYPath varlink-transient-stdio.service | grep '^/dev/tty2$' >/dev/null + # "null" is also the default for StandardInput=, hence additionally verify it was written explicitly + fragment=$(systemctl show -P FragmentPath varlink-transient-stdio.service) + grep '^StandardInput=null$' "$fragment" >/dev/null + + # The combined output types are spelled with a "+" in unit files, but with a "_" in the Varlink IDL (and + # hence in our own replies). The latter spelling must be accepted, so that replies can be fed back in. + defer_transient_cleanup varlink-transient-stdio-combined.service + result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-stdio-combined.service","Exec":{"StandardOutput":"kmsg_console","StandardError":"journal_console"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') + echo "$result" | jq -e '.context.Exec.StandardOutput == "kmsg_console"' + echo "$result" | jq -e '.context.Exec.StandardError == "journal_console"' + timeout 30 bash -c 'until systemctl is-active varlink-transient-stdio-combined.service; do sleep 0.5; done' + systemctl show -P StandardOutput varlink-transient-stdio-combined.service | grep '^kmsg+console$' >/dev/null + systemctl show -P StandardError varlink-transient-stdio-combined.service | grep '^journal+console$' >/dev/null + + # 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 + + # Exec.OOMScoreAdjust, Exec.UMask, Exec.NoNewPrivileges, Exec.MemoryDenyWriteExecute + # (int+validator, mode_t, two tristate-bool shapes) + defer_transient_cleanup varlink-transient-procctl.service + result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-procctl.service","Exec":{"OOMScoreAdjust":250,"UMask":18,"NoNewPrivileges":true,"MemoryDenyWriteExecute":true},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') + echo "$result" | jq -e '.context.Exec.OOMScoreAdjust == 250' + echo "$result" | jq -e '.context.Exec.UMask == 18' + echo "$result" | jq -e '.context.Exec.NoNewPrivileges == true' + timeout 30 bash -c 'until systemctl is-active varlink-transient-procctl.service; do sleep 0.5; done' + systemctl show -P OOMScoreAdjust varlink-transient-procctl.service | grep '^250$' >/dev/null + systemctl show -P UMask varlink-transient-procctl.service | grep '^0022$' >/dev/null + systemctl show -P NoNewPrivileges varlink-transient-procctl.service | grep '^yes$' >/dev/null + systemctl show -P MemoryDenyWriteExecute varlink-transient-procctl.service | grep '^yes$' >/dev/null + + # Exec.RootHashPath / Exec.RootHashSignaturePath: the unit-file directive must be + # "RootHash=" / "RootHashSignature=" (not the JSON name), otherwise the next daemon-reload + # would drop the setting as an unknown key. + defer_transient_cleanup varlink-transient-roothash.service + varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-roothash.service","Exec":{"RootHashPath":"/etc/hostname","RootHashSignaturePath":"/etc/machine-id"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' >/dev/null + fragment=$(systemctl show -P FragmentPath varlink-transient-roothash.service) + test -n "$fragment" + grep '^RootHash=/etc/hostname$' "$fragment" >/dev/null + grep '^RootHashSignature=/etc/machine-id$' "$fragment" >/dev/null + + # 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" + # Out-of-range OOMScoreAdjust= value is rejected + defer_transient_cleanup varlink-transient-bad-oom.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-oom.service","Exec":{"OOMScoreAdjust":9999},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "Exec.OOMScoreAdjust" + # Relative RootDirectory path is rejected + defer_transient_cleanup varlink-transient-bad-rd.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-rd.service","Exec":{"RootDirectory":"relative/path"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "Exec.RootDirectory" + # Note that an unparsable enum value (e.g. StandardOutput=bogus) is rejected by the Varlink IDL + # validation layer before the JSON dispatch even runs, hence there's no point in testing pid1's + # dispatch-level enum handling here — and doing so would trip post.sh's "didn't pass validation" + # journal check, since the IDL layer logs about the rejection. + # Relative TTYPath is rejected + defer_transient_cleanup varlink-transient-bad-ttypath.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-ttypath.service","Exec":{"TTYPath":"relative/tty"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "Exec.TTYPath" + + # The file based stdio types need a companion path, and StandardInput=data a companion payload, neither of + # which can be passed yet, hence they are rejected + for t in file append truncate; do + defer_transient_cleanup "varlink-transient-bad-stdout-$t.service" + expect_invalid_parameter \ + "{\"context\":{\"ID\":\"varlink-transient-bad-stdout-$t.service\",\"Exec\":{\"StandardOutput\":\"$t\"},\"Service\":{\"Type\":\"oneshot\",\"ExecStart\":[{\"path\":\"/bin/true\"}]}}}" \ + "Exec.StandardOutput" + done + defer_transient_cleanup varlink-transient-bad-stderr.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-stderr.service","Exec":{"StandardError":"file"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "Exec.StandardError" + for t in file data; do + defer_transient_cleanup "varlink-transient-bad-stdin-$t.service" + expect_invalid_parameter \ + "{\"context\":{\"ID\":\"varlink-transient-bad-stdin-$t.service\",\"Exec\":{\"StandardInput\":\"$t\"},\"Service\":{\"Type\":\"oneshot\",\"ExecStart\":[{\"path\":\"/bin/true\"}]}}}" \ + "Exec.StandardInput" + done + + # 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":{"AmbientCapabilities":["cap_net_raw"]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true) + echo "$unsupported_exec" | grep "io.systemd.Unit.PropertyNotSupported" + echo "$unsupported_exec" | grep "Exec.AmbientCapabilities" +} + +testcase_exec_sandbox() { + setup + + # Exec sandbox scalars: enum and yes/no-string properties round-trip into the reply + # context (Varlink spelling), systemctl show and the transient fragment (unit-file + # spelling, so the next daemon-reload keeps the setting) + defer_transient_cleanup varlink-transient-sandbox.service + result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-sandbox.service","Exec":{"ProtectSystem":"strict","ProtectHome":"read_only","PrivateTmp":"disconnected","PrivateNetwork":"yes","PrivateUsers":"self"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') + echo "$result" | jq -e '.context.Exec.ProtectSystem == "strict"' + echo "$result" | jq -e '.context.Exec.ProtectHome == "read_only"' + echo "$result" | jq -e '.context.Exec.PrivateTmp == "disconnected"' + echo "$result" | jq -e '.context.Exec.PrivateNetwork == "yes"' + echo "$result" | jq -e '.context.Exec.PrivateUsers == "self"' + timeout 30 bash -c 'until systemctl is-active varlink-transient-sandbox.service; do sleep 0.5; done' + systemctl show -P ProtectSystem varlink-transient-sandbox.service | grep '^strict$' >/dev/null + systemctl show -P ProtectHome varlink-transient-sandbox.service | grep '^read-only$' >/dev/null + systemctl show -P PrivateTmpEx varlink-transient-sandbox.service | grep '^disconnected$' >/dev/null + systemctl show -P PrivateNetwork varlink-transient-sandbox.service | grep '^yes$' >/dev/null + systemctl show -P PrivateUsersEx varlink-transient-sandbox.service | grep '^self$' >/dev/null + fragment=$(systemctl show -P FragmentPath varlink-transient-sandbox.service) + test -n "$fragment" + grep '^ProtectSystem=strict$' "$fragment" >/dev/null + grep '^ProtectHome=read-only$' "$fragment" >/dev/null + grep '^PrivateTmp=disconnected$' "$fragment" >/dev/null + grep '^PrivateNetwork=yes$' "$fragment" >/dev/null + grep '^PrivateUsers=self$' "$fragment" >/dev/null + + # The sandbox is live, not just recorded: the unit only succeeds if /usr is read-only + # and its network namespace differs from PID 1's. Both readlinks are captured first, so + # an unreadable link fails the unit rather than passing it. Comparing namespaces beats + # listing interfaces here: the harness runs with --runtime-network=none, and the netns + # fallback devices come and go with the loaded modules. + defer_transient_cleanup varlink-transient-sandbox-live.service + # shellcheck disable=SC2016 + varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-sandbox-live.service","Exec":{"ProtectSystem":"strict","PrivateNetwork":"yes"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/sh","arguments":["/bin/sh","-c","p=$(readlink /proc/1/ns/net) && s=$(readlink /proc/self/ns/net) && test \"$s\" != \"$p\" && ! touch /usr/varlink-sandbox-canary"]}]}}}' >/dev/null + timeout 30 bash -c 'until systemctl show -P ActiveState varlink-transient-sandbox-live.service | grep -Ex "active|failed" >/dev/null; do sleep 0.5; done' + systemctl show -P Result varlink-transient-sandbox-live.service | grep '^success$' >/dev/null + test ! -e /usr/varlink-sandbox-canary +} + +testcase_kill_properties() { + setup + + # Kill.KillMode/Kill.SendSIGHUP + defer_transient_cleanup varlink-transient-kill.service + result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-kill.service","Kill":{"KillMode":"process","SendSIGHUP":true},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') + echo "$result" | jq -e '.context.Kill.KillMode == "process"' + echo "$result" | jq -e '.context.Kill.SendSIGHUP == true' + timeout 30 bash -c 'until systemctl is-active varlink-transient-kill.service; do sleep 0.5; done' + systemctl show -P KillMode varlink-transient-kill.service | grep '^process$' >/dev/null + systemctl show -P SendSIGHUP varlink-transient-kill.service | grep '^yes$' >/dev/null + + # Same for a Kill field + defer_transient_cleanup varlink-transient-unknown-kill.service + unsupported_kill=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-unknown-kill.service","Kill":{"KillSignal":"SIGINT"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true) + echo "$unsupported_kill" | grep "io.systemd.Unit.PropertyNotSupported" + echo "$unsupported_kill" | grep "Kill.KillSignal" +} + +testcase_exec_flags() { + setup + + # shellcheck disable=SC2016 + TEMP_FILES+=(/tmp/varlink-transient-execflags-expanded '/tmp/varlink-transient-execflags-${FLAGTEST}' + /tmp/varlink-transient-execflags-shell /run/varlink-transient-execflags-{plain,privileged,nosetuid}) + + # ExecStart execution flags, i.e. the Varlink counterparts of the "-", ":", "|", "+" and "!" prefixes of + # unit file ExecStart= lines. First the ones observable from an unprivileged unit: + # - ignoreFailure: a failing command does not fail the unit + # - noEnvExpand: "${VAR}" in the arguments is passed as-is rather than expanded + # - viaShell: the command line is handed to the user's shell via "-c" (argv[0] is normalized to "sh") + rm -f /tmp/varlink-transient-execflags-* + defer_transient_cleanup varlink-transient-execflags.service + # shellcheck disable=SC2016 + result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-execflags.service","Exec":{"Environment":["FLAGTEST=expanded"]},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[ + {"path":"/bin/false","ignoreFailure":true}, + {"path":"/usr/bin/touch","arguments":["touch","/tmp/varlink-transient-execflags-${FLAGTEST}"]}, + {"path":"/usr/bin/touch","arguments":["touch","/tmp/varlink-transient-execflags-${FLAGTEST}"],"noEnvExpand":true}, + {"path":"/bin/sh","arguments":["sh","touch","/tmp/varlink-transient-execflags-shell"],"viaShell":true}]}}}') + echo "$result" | jq -e '.context.Service.ExecStart[0].ignoreFailure == true' + echo "$result" | jq -e '.context.Service.ExecStart[1].noEnvExpand == false' + echo "$result" | jq -e '.context.Service.ExecStart[2].noEnvExpand == true' + echo "$result" | jq -e '.context.Service.ExecStart[3].viaShell == true' + echo "$result" | jq -e '.context.Service.ExecStart[3].path == "/bin/sh"' + echo "$result" | jq -e '.context.Service.ExecStart[3].arguments[0] == "sh"' + # The unit only becomes active once all ExecStart= commands succeeded (or were allowed to fail) + timeout 30 bash -c 'until systemctl is-active varlink-transient-execflags.service; do sleep 0.5; done' + test -e /tmp/varlink-transient-execflags-expanded + # shellcheck disable=SC2016 + test -e '/tmp/varlink-transient-execflags-${FLAGTEST}' + test -e /tmp/varlink-transient-execflags-shell + # The flags must show up as the matching prefix characters in the transient unit file + fragment=$(systemctl show -P FragmentPath varlink-transient-execflags.service) + grep -E '^ExecStart=-"/bin/false"$' "$fragment" >/dev/null + grep -E '^ExecStart=:' "$fragment" >/dev/null + grep -E '^ExecStart=\|' "$fragment" >/dev/null + rm -f /tmp/varlink-transient-execflags-* + + # Now the privilege related ones, in a unit running as an unprivileged user: + # - privileged/noSetuid: the command runs with full privileges, i.e. can write where the user cannot + # - (neither): the command runs as the user, and fails to write there (tolerated via ignoreFailure) + rm -f /run/varlink-transient-execflags-* + defer_transient_cleanup varlink-transient-execflags-priv.service + result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-execflags-priv.service","Exec":{"User":"nobody"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[ + {"path":"/usr/bin/touch","arguments":["touch","/run/varlink-transient-execflags-plain"],"ignoreFailure":true}, + {"path":"/usr/bin/touch","arguments":["touch","/run/varlink-transient-execflags-privileged"],"privileged":true}, + {"path":"/usr/bin/touch","arguments":["touch","/run/varlink-transient-execflags-nosetuid"],"noSetuid":true}]}}}') + echo "$result" | jq -e '.context.Service.ExecStart[0].privileged == false' + echo "$result" | jq -e '.context.Service.ExecStart[1].privileged == true' + echo "$result" | jq -e '.context.Service.ExecStart[2].noSetuid == true' + timeout 30 bash -c 'until systemctl is-active varlink-transient-execflags-priv.service; do sleep 0.5; done' + test ! -e /run/varlink-transient-execflags-plain + test -e /run/varlink-transient-execflags-privileged + test -e /run/varlink-transient-execflags-nosetuid + fragment=$(systemctl show -P FragmentPath varlink-transient-execflags-priv.service) + grep -E '^ExecStart=\+' "$fragment" >/dev/null + grep -E '^ExecStart=!' "$fragment" >/dev/null + rm -f /run/varlink-transient-execflags-* + + # The 'privileged' and 'noSetuid' execution flags are mutually exclusive + defer_transient_cleanup varlink-transient-badflags.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-badflags.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true","privileged":true,"noSetuid":true}]}}}' \ + "Service.ExecStart" +} + +testcase_credentials() { + setup + + # 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) + TEMP_FILES+=("$CRED_OUTPUT") + 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" + + # 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" +} + +testcase_file_descriptors() { + setup + + # Service.Standard{Output,Error}FileDescriptor: connect passed fds (by push order) + # to the unit's stdout/stderr; regular files, so output can be checked after exit. + transient_out=$(mktemp) + TEMP_FILES+=("$transient_out") + transient_err=$(mktemp) + TEMP_FILES+=("$transient_err") + exec {transient_out_fd}>"$transient_out" + exec {transient_err_fd}>"$transient_err" + defer_transient_cleanup varlink-transient-fdpass.service + varlinkctl --push-fd="$transient_out_fd" --push-fd="$transient_err_fd" \ + call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-fdpass.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/sh","arguments":["/bin/sh","-c","echo to-stdout; echo to-stderr >&2"]}],"StandardOutputFileDescriptor":0,"StandardErrorFileDescriptor":1}}}' >/dev/null + # Close our copies so the unit holds the only remaining write ends. + exec {transient_out_fd}>&- + exec {transient_err_fd}>&- + timeout 30 bash -c 'until systemctl show -P ActiveState varlink-transient-fdpass.service | grep inactive >/dev/null; do sleep 0.5; done' + systemctl show -P Result varlink-transient-fdpass.service | grep success >/dev/null + grep -x to-stdout "$transient_out" >/dev/null + grep -x to-stderr "$transient_err" >/dev/null + rm -f "$transient_out" "$transient_err" +} + +testcase_scope() { + setup + + # Starting a transient scope + defer_transient_cleanup varlink-transient-scope.scope + sleep infinity & + scope_child="$!" + CHILD_PIDS+=("$scope_child") + varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + "{\"context\":{\"ID\":\"varlink-transient-scope.scope\",\"Scope\":{\"PIDs\":[{\"pid\":${scope_child}}],\"OOMPolicy\":\"kill\", \"TimeoutStopUSec\":12000000}}}" + timeout 30 bash -c 'until systemctl is-active varlink-transient-scope.scope; do sleep 0.5; done' + systemctl whoami "$scope_child" | grep "^varlink-transient-scope.scope$" >/dev/null + systemctl show -P TimeoutStopUSec varlink-transient-scope.scope | grep 12s >/dev/null + systemctl show -P OOMPolicy varlink-transient-scope.scope | grep kill >/dev/null + + # Bad PID on a scope + defer_transient_cleanup varlink-transient-badpid.scope + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-badpid.scope","Scope":{"PIDs":[{"pid": 1}]}}}' \ + "Scope.PIDs" +} + +testcase_dependencies() { + setup + + # Unit dependencies + defer_transient_cleanup varlink-transient-deps.service + varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-deps.service","After":["varlink-transient-dependency.service"],"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' + timeout 30 bash -c 'until systemctl is-active varlink-transient-deps.service; do sleep 0.5; done' + systemctl show -P After varlink-transient-deps.service | grep varlink-transient-dependency.service >/dev/null + + # Dependency on invalid unit (rejected at dispatch time) + defer_transient_cleanup varlink-invalid-dependency.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-invalid-dependency.service","Requires":["invalid@.service"],"Service":{"ExecStart":[{"path":"/usr/bin/true"}]}}}' \ + "context" + + # Same for non-transient dependency types + defer_transient_cleanup varlink-non-transient-dependency.service + non_transient_dep=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-non-transient-dependency.service","Triggers":["varlink-transient-dependency.service"],"Service":{"ExecStart":[{"path":"/usr/bin/true"}]}}}' 2>&1 || true) + echo "$non_transient_dep" | grep "io.systemd.Unit.PropertyNotSupported" + echo "$non_transient_dep" | grep "Triggers" +} + +testcase_cgroup() { + setup + + # CGroup context: scalar resource-control properties round-trip into the reply + # context, systemctl show and the transient fragment + defer_transient_cleanup varlink-transient-cgroup.service + result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-cgroup.service","CGroup":{"CPUWeight":200,"StartupCPUWeight":150,"CPUQuotaPerSecUSec":500000,"CPUQuotaPeriodUSec":20000,"MemoryAccounting":true,"MemoryLow":8388608,"StartupMemoryLow":4194304,"MemoryHigh":33554432,"MemoryMax":67108864,"TasksAccounting":true,"TasksMax":{"value":16},"IOAccounting":true,"IOWeight":100},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') + echo "$result" | jq -e '.context.CGroup.CPUWeight == 200' + echo "$result" | jq -e '.context.CGroup.StartupCPUWeight == 150' + echo "$result" | jq -e '.context.CGroup.CPUQuotaPerSecUSec == 500000' + echo "$result" | jq -e '.context.CGroup.CPUQuotaPeriodUSec == 20000' + echo "$result" | jq -e '.context.CGroup.StartupMemoryLow == 4194304' + echo "$result" | jq -e '.context.CGroup.MemoryMax == 67108864' + echo "$result" | jq -e '.context.CGroup.TasksMax == {"value":16,"scale":0}' + timeout 30 bash -c 'until systemctl is-active varlink-transient-cgroup.service; do sleep 0.5; done' + systemctl show -P CPUWeight varlink-transient-cgroup.service | grep -Fx 200 >/dev/null + systemctl show -P CPUQuotaPerSecUSec varlink-transient-cgroup.service | grep -Fx 500ms >/dev/null + systemctl show -P CPUQuotaPeriodUSec varlink-transient-cgroup.service | grep -Fx 20ms >/dev/null + systemctl show -P MemoryLow varlink-transient-cgroup.service | grep -Fx 8388608 >/dev/null + systemctl show -P MemoryHigh varlink-transient-cgroup.service | grep -Fx 33554432 >/dev/null + systemctl show -P MemoryMax varlink-transient-cgroup.service | grep -Fx 67108864 >/dev/null + systemctl show -P TasksMax varlink-transient-cgroup.service | grep -Fx 16 >/dev/null + systemctl show -P IOWeight varlink-transient-cgroup.service | grep -Fx 100 >/dev/null + # The fragment must use the unit-file directive spellings (CPUQuota=, not the JSON + # property name), otherwise the next daemon-reload would drop the setting. + fragment=$(systemctl show -P FragmentPath varlink-transient-cgroup.service) + test -n "$fragment" + grep -Fx 'CPUQuota=50.00%' "$fragment" >/dev/null + grep -Fx 'CPUQuotaPeriodSec=20ms' "$fragment" >/dev/null + grep -Fx 'MemoryMax=67108864' "$fragment" >/dev/null + grep -Fx 'TasksMax=16' "$fragment" >/dev/null + + # The special values are written as TasksMax=infinity, CPUWeight=idle and an empty + # StartupCPUWeight= (UINT64_MAX resets a weight to the default) + defer_transient_cleanup varlink-transient-cgroup-special.service + varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-cgroup-special.service","CGroup":{"TasksMax":{"value":18446744073709551615},"CPUWeight":0,"StartupCPUWeight":18446744073709551615},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' >/dev/null + timeout 30 bash -c 'until systemctl is-active varlink-transient-cgroup-special.service; do sleep 0.5; done' + systemctl show -P TasksMax varlink-transient-cgroup-special.service | grep -Fx infinity >/dev/null + fragment=$(systemctl show -P FragmentPath varlink-transient-cgroup-special.service) + test -n "$fragment" + grep -Fx 'TasksMax=infinity' "$fragment" >/dev/null + grep -Fx 'CPUWeight=idle' "$fragment" >/dev/null + grep -Fx 'StartupCPUWeight=' "$fragment" >/dev/null + + # Cgroup limits materialize on the live cgroup + defer_transient_cleanup varlink-transient-cgroup-live.service + varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-cgroup-live.service","CGroup":{"MemoryMax":67108864,"TasksMax":{"value":16}},"Service":{"ExecStart":[{"path":"/usr/bin/sleep","arguments":["/usr/bin/sleep","infinity"]}]}}}' >/dev/null + timeout 10 bash -c 'until systemctl is-active varlink-transient-cgroup-live.service; do sleep 0.5; done' + cgroup_path=$(systemctl show -P ControlGroup varlink-transient-cgroup-live.service) + test -n "$cgroup_path" + grep -Fx 67108864 "/sys/fs/cgroup${cgroup_path}/memory.max" >/dev/null + grep -Fx 16 "/sys/fs/cgroup${cgroup_path}/pids.max" >/dev/null + + # Slice= places the unit under an implicitly created slice + defer_transient_cleanup varlink-transient-slice.service + defer_transient_cleanup varlink-test.slice + result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-slice.service","CGroup":{"Slice":"varlink-test.slice"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') + echo "$result" | jq -e '.context.CGroup.Slice == "varlink-test.slice"' + timeout 30 bash -c 'until systemctl is-active varlink-transient-slice.service; do sleep 0.5; done' + systemctl show -P Slice varlink-transient-slice.service | grep -Fx varlink-test.slice >/dev/null + + # Relative forms: TasksMaxScale/MemoryMaxScale (floats, 1.0 == 100%) are input only, + # converted internally and written to the fragment as permyriad percentages. An + # explicit null is treated as absent (so MemoryMax=null does not conflict with + # MemoryMaxScale), and a zero scale is accepted for the min-0 memory properties, + # as over D-Bus + defer_transient_cleanup varlink-transient-cgroup-scale.service + result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-cgroup-scale.service","CGroup":{"TasksMaxScale":0.5,"MemoryMax":null,"MemoryMaxScale":0.125,"MemoryMinScale":0.0,"TasksAccounting":null},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') + echo "$result" | jq -e '.context.CGroup.TasksMax == {"value":5000,"scale":10000}' + echo "$result" | jq -e '.context.CGroup.TasksMaxScale == null' + timeout 30 bash -c 'until systemctl is-active varlink-transient-cgroup-scale.service; do sleep 0.5; done' + fragment=$(systemctl show -P FragmentPath varlink-transient-cgroup-scale.service) + test -n "$fragment" + grep -Fx 'TasksMax=50.00%' "$fragment" >/dev/null + grep -Fx 'MemoryMax=12.50%' "$fragment" >/dev/null + grep -Fx 'MemoryMin=0.00%' "$fragment" >/dev/null + (! grep -F 'TasksAccounting=' "$fragment") + + # A null scale member of TasksMax is absent, not a request for the relative form + defer_transient_cleanup varlink-transient-cgroup-nullscale.service + result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-cgroup-nullscale.service","CGroup":{"TasksMax":{"value":16,"scale":null}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') + echo "$result" | jq -e '.context.CGroup.TasksMax == {"value":16,"scale":0}' + + # Out-of-range CPUWeight= is rejected + defer_transient_cleanup varlink-transient-bad-weight.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-weight.service","CGroup":{"CPUWeight":123456},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "CGroup.CPUWeight" + # IOWeight=0 is rejected, only the CPU weights accept idle + defer_transient_cleanup varlink-transient-bad-ioweight.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-ioweight.service","CGroup":{"IOWeight":0},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "CGroup.IOWeight" + # MemoryMax=0 is rejected (minimum 1, matching the D-Bus setter) + defer_transient_cleanup varlink-transient-bad-memmax.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-memmax.service","CGroup":{"MemoryMax":0},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "CGroup.MemoryMax" + # The scale member of TasksMax is output only, TasksMaxScale is the input form + defer_transient_cleanup varlink-transient-bad-taskscale-member.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-taskscale-member.service","CGroup":{"TasksMax":{"value":5000,"scale":10000}},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "context" + # Out-of-range TasksMaxScale= (>= 100%) is rejected + defer_transient_cleanup varlink-transient-bad-tasksscale.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-tasksscale.service","CGroup":{"TasksMaxScale":1.5},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "CGroup.TasksMaxScale" + # Absolute and relative forms of the same limit may not be combined + defer_transient_cleanup varlink-transient-bad-taskscombo.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-taskscombo.service","CGroup":{"TasksMax":{"value":16},"TasksMaxScale":0.5},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "CGroup.TasksMaxScale" + # Zero MemoryMaxScale= is rejected + defer_transient_cleanup varlink-transient-bad-memscale.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-memscale.service","CGroup":{"MemoryMaxScale":0.0},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "CGroup.MemoryMaxScale" + # Slice= pointing at a non-slice unit is rejected + defer_transient_cleanup varlink-transient-bad-slice.service + expect_invalid_parameter \ + '{"context":{"ID":"varlink-transient-bad-slice.service","CGroup":{"Slice":"not-a-slice.service"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ + "CGroup.Slice" + # CGroup 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-cgroup.service + unsupported_cgroup=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-unknown-cgroup.service","CGroup":{"DeviceAllow":[]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true) + echo "$unsupported_cgroup" | grep "io.systemd.Unit.PropertyNotSupported" + echo "$unsupported_cgroup" | grep "CGroup.DeviceAllow" +} + +testcase_validation() { + setup + + # Creating an already active unit must fail. + 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' + + # 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" + ) + ( + set +o pipefail + varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-test.target","Description":"test"}}' |& grep "io.systemd.Unit.UnitTypeNotSupported" + ) + + # Exec on a unit type without an exec context (.slice) is rejected + ( + set +o pipefail + 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" + ) + # Same for Kill on a unit type without a kill context + ( + set +o pipefail + varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-kill.slice","Kill":{"KillMode":"process"}}}' |& grep "io.systemd.Unit.UnitTypeNotSupported" + ) +} + +run_testcases diff --git a/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh index 216c9336353..ac25e896e09 100755 --- a/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh +++ b/test/units/TEST-74-AUX-UTILS.varlinkctl-unit.sh @@ -3,806 +3,95 @@ set -eux set -o pipefail -# test io.systemd.Unit -varlinkctl info /run/systemd/io.systemd.Manager -varlinkctl introspect /run/systemd/io.systemd.Manager io.systemd.Unit -varlinkctl --more call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "multi-user.target"}' -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"pid": {"pid": 1}}' -(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' |& grep "called without 'more' flag" >/dev/null) -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "init.scope", "pid": {"pid": 1}}' -(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": ""}') -(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "non-existent.service"}') -(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"pid": {"pid": -1}}' ) -(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "multi-user.target", "pid": {"pid": 1}}') -set +o pipefail -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties '{"runtime": true, "name": "non-existent.service", "properties": {"Markers": ["needs-restart"]}}' |& grep "io.systemd.Unit.NoSuchUnit" -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties '{"runtime": true, "name": "systemd-journald.service", "properties": {"LoadState": "foobar"}}' |& grep "io.systemd.Unit.PropertyNotSupported" -set -o pipefail +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"cgroup": "/init.scope"}' -invocation_id="$(systemctl show -P InvocationID systemd-journald.service)" -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "{\"invocationID\": \"$invocation_id\"}" -# test for KillContext -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"pid": {"pid": 0}}' | jq -e '.context.Kill' -# test for AutomountContext/Runtime -automount_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "automount" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1) -test -n "$automount_id" -# Use jq to JSON-encode the unit name as it may contain backslash escapes (e.g. \x2d) that -# are not valid JSON escape sequences and would be rejected by varlinkctl's JSON parser. -automount_params=$(jq -cn --arg name "$automount_id" '{name: $name}') -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$automount_params" | jq -e '.context.Automount' -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$automount_params" | jq -e '.runtime.Automount' -# test for MountContext/Runtime (skip volatile run-user-*.mount to avoid GC race) -mount_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "mount" and .runtime.LoadState == "loaded" and (.context.ID | startswith("run-user-") | not)) .context.ID // empty' | tail -n 1) -test -n "$mount_id" -mount_params=$(jq -cn --arg name "$mount_id" '{name: $name}') -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$mount_params" | jq -e '.context.Mount' -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$mount_params" | jq -e '.runtime.Mount' -# test for PathContext/Runtime -path_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "path" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1) -test -n "$path_id" -path_params=$(jq -cn --arg name "$path_id" '{name: $name}') -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$path_params" | jq -e '.context.Path' -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$path_params" | jq -e '.runtime.Path' -# test for ServiceContext/Runtime (skip volatile user@*/user-runtime-dir@* to avoid GC race) -service_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "service" and .runtime.LoadState == "loaded" and (.context.ID | test("^(user|user-runtime-dir)@") | not)) .context.ID // empty' | tail -n 1) -test -n "$service_id" -service_params=$(jq -cn --arg name "$service_id" '{name: $name}') -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$service_params" | jq -e '.context.Service' -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$service_params" | jq -e '.runtime.Service' -# test for ScopeContext/Runtime (skip volatile session-*.scope to avoid GC race) -scope_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "scope" and .runtime.LoadState == "loaded" and (.context.ID | startswith("session-") | not)) .context.ID // empty' | tail -n 1) -test -n "$scope_id" -scope_params=$(jq -cn --arg name "$scope_id" '{name: $name}') -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$scope_params" | jq -e '.context.Scope' -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$scope_params" | jq -e '.runtime.Scope' -# test for SocketContext/Runtime -socket_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "socket" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1) -test -n "$socket_id" -socket_params=$(jq -cn --arg name "$socket_id" '{name: $name}') -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$socket_params" | jq -e '.context.Socket' -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$socket_params" | jq -e '.runtime.Socket' -# test for SwapContext/Runtime (swap units may not be present on all systems) -swap_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "swap" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1) -if test -n "$swap_id"; then - swap_params=$(jq -cn --arg name "$swap_id" '{name: $name}') - varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$swap_params" | jq -e '.context.Swap' - varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$swap_params" | jq -e '.runtime.Swap' -fi -# test for TimerContext/Runtime -timer_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "timer" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1) -test -n "$timer_id" -timer_params=$(jq -cn --arg name "$timer_id" '{name: $name}') -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$timer_params" | jq -e '.context.Timer' -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$timer_params" | jq -e '.runtime.Timer' - -# test io.systemd.Unit.EnqueueJob - -TRANSIENT_UNITS=() -at_exit() { - systemctl stop varlink-test-enqueue{,-2}.service varlink-test-slow.service 2>/dev/null || true - rm -f /run/systemd/system/varlink-test-enqueue{,-2}.service /run/systemd/system/varlink-test-slow.service - rm -f /tmp/enqueue-notifications.json - for u in "${TRANSIENT_UNITS[@]}"; do - systemctl stop "$u" 2>/dev/null || true - systemctl reset-failed "$u" 2>/dev/null || true - done - systemctl daemon-reload -} -trap at_exit EXIT - -for unit in varlink-test-enqueue{,-2}.service; do - cat >"/run/systemd/system/$unit" < 1 and .[0].job.JobType == "restart" and .[-1].job.Result == "done" and .[-1].context and .[-1].runtime' -systemctl is-active varlink-test-enqueue.service - -varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service"], "jobType": "stop"}' | jq -e '.job.JobType == "stop"' -timeout 10 bash -c 'until systemctl show -P ActiveState varlink-test-enqueue.service | grep inactive >/dev/null; do sleep 0.5; done' - -# Multiple units share a transaction and receive one reply per job. -varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service", "varlink-test-enqueue-2.service"], "jobType": "start"}' | jq -e 'length == 2 and all(.[]; .context and .runtime and .job.Id) and ([.[].job.Unit] | sort == ["varlink-test-enqueue-2.service", "varlink-test-enqueue.service"])' -timeout 10 bash -c 'until systemctl is-active varlink-test-enqueue.service; do sleep 0.5; done' -timeout 10 bash -c 'until systemctl is-active varlink-test-enqueue-2.service; do sleep 0.5; done' -systemctl stop varlink-test-enqueue{,-2}.service - -# Multiple replies require 'more'. -(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service", "varlink-test-enqueue-2.service"], "jobType": "start"}') - -# A missing unit rejects the entire transaction. -(! varlinkctl call --more /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service", "non-existent.service"], "jobType": "start"}') -(! systemctl is-active varlink-test-enqueue.service) - -# Notifications require 'more' and a single unit. -(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service"], "jobType": "start", "notifyJobChanges": true}') -(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service"], "jobType": "start", "notifyUnitChanges": true}') - -(! varlinkctl call --more /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["varlink-test-enqueue.service", "varlink-test-enqueue-2.service"], "jobType": "start", "notifyJobChanges": true}') - -# Error cases -(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": ["invalid-unit-name"], "jobType": "start"}') -(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob '{"names": [], "jobType": "start"}') - -# Coalesced jobs must reject a second watcher. -cat >/run/systemd/system/varlink-test-slow.service </dev/null 2>&1 & -watcher_pid=$! - -# Wait for the first watcher to attach. -timeout 10 bash -c 'until systemctl show -P ActiveState varlink-test-slow.service | grep activating >/dev/null; do sleep 0.5; done' - -set +o pipefail -varlinkctl call --more /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob \ - '{"names": ["varlink-test-slow.service"], "jobType": "start", "notifyJobChanges": true}' |& grep "io.systemd.Unit.AlreadyBeingWatched" -set -o pipefail - -systemctl stop varlink-test-slow.service -wait "$watcher_pid" 2>/dev/null || true - -# Completing a NOP job must preserve the regular job's unit subscription. -varlinkctl --json=short call --more /run/systemd/io.systemd.Manager io.systemd.Unit.EnqueueJob \ - '{"names": ["varlink-test-slow.service"], "jobType": "start", "notifyUnitChanges": true}' >/tmp/enqueue-notifications.json & -watcher_pid=$! - -timeout 10 bash -c 'until systemctl show -P ActiveState varlink-test-slow.service | grep activating >/dev/null; do sleep 0.5; done' - -# The pending start job keeps the stream open; each line is a unit notification. -sleep 1 -notifications=$(wc -l /dev/null - -# NOP removal must still trigger a unit notification. -for _ in {1..20}; do - [[ "$(wc -l /dev/null || true - -# test io.systemd.Unit in user manager -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" - -defer_transient_cleanup() { - TRANSIENT_UNITS+=("$1") +testcase_list() { + # test io.systemd.Unit + varlinkctl info /run/systemd/io.systemd.Manager + varlinkctl introspect /run/systemd/io.systemd.Manager io.systemd.Unit + varlinkctl --more call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "multi-user.target"}' + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"pid": {"pid": 1}}' + (! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' |& grep "called without 'more' flag" >/dev/null) + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "init.scope", "pid": {"pid": 1}}' + (! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": ""}') + (! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "non-existent.service"}') + (! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"pid": {"pid": -1}}' ) + (! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "multi-user.target", "pid": {"pid": 1}}') + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"cgroup": "/init.scope"}' + invocation_id="$(systemctl show -P InvocationID systemd-journald.service)" + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "{\"invocationID\": \"$invocation_id\"}" } -# 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","CollectMode":"inactive_or_failed","Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.Description == "Test description property"' -echo "$result" | jq -e '.context.CollectMode == "inactive_or_failed"' -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' -systemctl show -P CollectMode varlink-transient-desc.service | grep '^inactive-or-failed$' >/dev/null - -# 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.StandardInput/StandardOutput/StandardError enum properties and Exec.TTYPath -defer_transient_cleanup varlink-transient-stdio.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-stdio.service","Exec":{"StandardInput":"null","StandardOutput":"null","StandardError":"journal","TTYPath":"/dev/tty2"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.Exec.StandardInput == "null"' -echo "$result" | jq -e '.context.Exec.StandardOutput == "null"' -echo "$result" | jq -e '.context.Exec.StandardError == "journal"' -echo "$result" | jq -e '.context.Exec.TTYPath == "/dev/tty2"' -timeout 30 bash -c 'until systemctl is-active varlink-transient-stdio.service; do sleep 0.5; done' -systemctl show -P StandardInput varlink-transient-stdio.service | grep '^null$' >/dev/null -systemctl show -P StandardOutput varlink-transient-stdio.service | grep '^null$' >/dev/null -systemctl show -P StandardError varlink-transient-stdio.service | grep '^journal$' >/dev/null -systemctl show -P TTYPath varlink-transient-stdio.service | grep '^/dev/tty2$' >/dev/null -# "null" is also the default for StandardInput=, hence additionally verify it was written explicitly -fragment=$(systemctl show -P FragmentPath varlink-transient-stdio.service) -grep '^StandardInput=null$' "$fragment" >/dev/null - -# The combined output types are spelled with a "+" in unit files, but with a "_" in the Varlink IDL (and -# hence in our own replies). The latter spelling must be accepted, so that replies can be fed back in. -defer_transient_cleanup varlink-transient-stdio-combined.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-stdio-combined.service","Exec":{"StandardOutput":"kmsg_console","StandardError":"journal_console"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.Exec.StandardOutput == "kmsg_console"' -echo "$result" | jq -e '.context.Exec.StandardError == "journal_console"' -timeout 30 bash -c 'until systemctl is-active varlink-transient-stdio-combined.service; do sleep 0.5; done' -systemctl show -P StandardOutput varlink-transient-stdio-combined.service | grep '^kmsg+console$' >/dev/null -systemctl show -P StandardError varlink-transient-stdio-combined.service | grep '^journal+console$' >/dev/null - -# Kill.KillMode/Kill.SendSIGHUP -defer_transient_cleanup varlink-transient-kill.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-kill.service","Kill":{"KillMode":"process","SendSIGHUP":true},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.Kill.KillMode == "process"' -echo "$result" | jq -e '.context.Kill.SendSIGHUP == true' -timeout 30 bash -c 'until systemctl is-active varlink-transient-kill.service; do sleep 0.5; done' -systemctl show -P KillMode varlink-transient-kill.service | grep '^process$' >/dev/null -systemctl show -P SendSIGHUP varlink-transient-kill.service | grep '^yes$' >/dev/null - -# ExecStart execution flags, i.e. the Varlink counterparts of the "-", ":", "|", "+" and "!" prefixes of -# unit file ExecStart= lines. First the ones observable from an unprivileged unit: -# - ignoreFailure: a failing command does not fail the unit -# - noEnvExpand: "${VAR}" in the arguments is passed as-is rather than expanded -# - viaShell: the command line is handed to the user's shell via "-c" (argv[0] is normalized to "sh") -rm -f /tmp/varlink-transient-execflags-* -defer_transient_cleanup varlink-transient-execflags.service -# shellcheck disable=SC2016 -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-execflags.service","Exec":{"Environment":["FLAGTEST=expanded"]},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[ - {"path":"/bin/false","ignoreFailure":true}, - {"path":"/usr/bin/touch","arguments":["touch","/tmp/varlink-transient-execflags-${FLAGTEST}"]}, - {"path":"/usr/bin/touch","arguments":["touch","/tmp/varlink-transient-execflags-${FLAGTEST}"],"noEnvExpand":true}, - {"path":"/bin/sh","arguments":["sh","touch","/tmp/varlink-transient-execflags-shell"],"viaShell":true}]}}}') -echo "$result" | jq -e '.context.Service.ExecStart[0].ignoreFailure == true' -echo "$result" | jq -e '.context.Service.ExecStart[1].noEnvExpand == false' -echo "$result" | jq -e '.context.Service.ExecStart[2].noEnvExpand == true' -echo "$result" | jq -e '.context.Service.ExecStart[3].viaShell == true' -echo "$result" | jq -e '.context.Service.ExecStart[3].path == "/bin/sh"' -echo "$result" | jq -e '.context.Service.ExecStart[3].arguments[0] == "sh"' -# The unit only becomes active once all ExecStart= commands succeeded (or were allowed to fail) -timeout 30 bash -c 'until systemctl is-active varlink-transient-execflags.service; do sleep 0.5; done' -test -e /tmp/varlink-transient-execflags-expanded -# shellcheck disable=SC2016 -test -e '/tmp/varlink-transient-execflags-${FLAGTEST}' -test -e /tmp/varlink-transient-execflags-shell -# The flags must show up as the matching prefix characters in the transient unit file -fragment=$(systemctl show -P FragmentPath varlink-transient-execflags.service) -grep -E '^ExecStart=-"/bin/false"$' "$fragment" >/dev/null -grep -E '^ExecStart=:' "$fragment" >/dev/null -grep -E '^ExecStart=\|' "$fragment" >/dev/null -rm -f /tmp/varlink-transient-execflags-* - -# Now the privilege related ones, in a unit running as an unprivileged user: -# - privileged/noSetuid: the command runs with full privileges, i.e. can write where the user cannot -# - (neither): the command runs as the user, and fails to write there (tolerated via ignoreFailure) -rm -f /run/varlink-transient-execflags-* -defer_transient_cleanup varlink-transient-execflags-priv.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-execflags-priv.service","Exec":{"User":"nobody"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[ - {"path":"/usr/bin/touch","arguments":["touch","/run/varlink-transient-execflags-plain"],"ignoreFailure":true}, - {"path":"/usr/bin/touch","arguments":["touch","/run/varlink-transient-execflags-privileged"],"privileged":true}, - {"path":"/usr/bin/touch","arguments":["touch","/run/varlink-transient-execflags-nosetuid"],"noSetuid":true}]}}}') -echo "$result" | jq -e '.context.Service.ExecStart[0].privileged == false' -echo "$result" | jq -e '.context.Service.ExecStart[1].privileged == true' -echo "$result" | jq -e '.context.Service.ExecStart[2].noSetuid == true' -timeout 30 bash -c 'until systemctl is-active varlink-transient-execflags-priv.service; do sleep 0.5; done' -test ! -e /run/varlink-transient-execflags-plain -test -e /run/varlink-transient-execflags-privileged -test -e /run/varlink-transient-execflags-nosetuid -fragment=$(systemctl show -P FragmentPath varlink-transient-execflags-priv.service) -grep -E '^ExecStart=\+' "$fragment" >/dev/null -grep -E '^ExecStart=!' "$fragment" >/dev/null -rm -f /run/varlink-transient-execflags-* - -# 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 - -# Exec.OOMScoreAdjust, Exec.UMask, Exec.NoNewPrivileges, Exec.MemoryDenyWriteExecute -# (int+validator, mode_t, two tristate-bool shapes) -defer_transient_cleanup varlink-transient-procctl.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-procctl.service","Exec":{"OOMScoreAdjust":250,"UMask":18,"NoNewPrivileges":true,"MemoryDenyWriteExecute":true},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.Exec.OOMScoreAdjust == 250' -echo "$result" | jq -e '.context.Exec.UMask == 18' -echo "$result" | jq -e '.context.Exec.NoNewPrivileges == true' -timeout 30 bash -c 'until systemctl is-active varlink-transient-procctl.service; do sleep 0.5; done' -systemctl show -P OOMScoreAdjust varlink-transient-procctl.service | grep '^250$' >/dev/null -systemctl show -P UMask varlink-transient-procctl.service | grep '^0022$' >/dev/null -systemctl show -P NoNewPrivileges varlink-transient-procctl.service | grep '^yes$' >/dev/null -systemctl show -P MemoryDenyWriteExecute varlink-transient-procctl.service | grep '^yes$' >/dev/null - -# Exec.RootHashPath / Exec.RootHashSignaturePath: the unit-file directive must be -# "RootHash=" / "RootHashSignature=" (not the JSON name), otherwise the next daemon-reload -# would drop the setting as an unknown key. -defer_transient_cleanup varlink-transient-roothash.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-roothash.service","Exec":{"RootHashPath":"/etc/hostname","RootHashSignaturePath":"/etc/machine-id"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' >/dev/null -fragment=$(systemctl show -P FragmentPath varlink-transient-roothash.service) -test -n "$fragment" -grep '^RootHash=/etc/hostname$' "$fragment" >/dev/null -grep '^RootHashSignature=/etc/machine-id$' "$fragment" >/dev/null - -# Service.Standard{Output,Error}FileDescriptor: connect passed fds (by push order) -# to the unit's stdout/stderr; regular files, so output can be checked after exit. -transient_out=$(mktemp) -transient_err=$(mktemp) -exec {transient_out_fd}>"$transient_out" -exec {transient_err_fd}>"$transient_err" -defer_transient_cleanup varlink-transient-fdpass.service -varlinkctl --push-fd="$transient_out_fd" --push-fd="$transient_err_fd" \ - call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-fdpass.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/sh","arguments":["/bin/sh","-c","echo to-stdout; echo to-stderr >&2"]}],"StandardOutputFileDescriptor":0,"StandardErrorFileDescriptor":1}}}' >/dev/null -# Close our copies so the unit holds the only remaining write ends. -exec {transient_out_fd}>&- -exec {transient_err_fd}>&- -timeout 30 bash -c 'until systemctl show -P ActiveState varlink-transient-fdpass.service | grep inactive >/dev/null; do sleep 0.5; done' -systemctl show -P Result varlink-transient-fdpass.service | grep success >/dev/null -grep -x to-stdout "$transient_out" >/dev/null -grep -x to-stderr "$transient_err" >/dev/null -rm -f "$transient_out" "$transient_err" - -# Starting a transient scope -defer_transient_cleanup varlink-transient-scope.scope -sleep infinity & -scope_child="$!" -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - "{\"context\":{\"ID\":\"varlink-transient-scope.scope\",\"Scope\":{\"PIDs\":[{\"pid\":${scope_child}}],\"OOMPolicy\":\"kill\", \"TimeoutStopUSec\":12000000}}}" -timeout 30 bash -c 'until systemctl is-active varlink-transient-scope.scope; do sleep 0.5; done' -systemctl whoami "$scope_child" | grep "^varlink-transient-scope.scope$" >/dev/null -systemctl show -P TimeoutStopUSec varlink-transient-scope.scope | grep 12s >/dev/null -systemctl show -P OOMPolicy varlink-transient-scope.scope | grep kill >/dev/null - -# Unit dependencies -defer_transient_cleanup varlink-transient-deps.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-deps.service","After":["varlink-transient-test.service"],"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' -timeout 30 bash -c 'until systemctl is-active varlink-transient-deps.service; do sleep 0.5; done' -systemctl show -P After varlink-transient-deps.service | grep varlink-transient-test.service >/dev/null - -# CGroup context: scalar resource-control properties round-trip into the reply -# context, systemctl show and the transient fragment -defer_transient_cleanup varlink-transient-cgroup.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-cgroup.service","CGroup":{"CPUWeight":200,"StartupCPUWeight":150,"CPUQuotaPerSecUSec":500000,"CPUQuotaPeriodUSec":20000,"MemoryAccounting":true,"MemoryLow":8388608,"StartupMemoryLow":4194304,"MemoryHigh":33554432,"MemoryMax":67108864,"TasksAccounting":true,"TasksMax":{"value":16},"IOAccounting":true,"IOWeight":100},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.CGroup.CPUWeight == 200' -echo "$result" | jq -e '.context.CGroup.StartupCPUWeight == 150' -echo "$result" | jq -e '.context.CGroup.CPUQuotaPerSecUSec == 500000' -echo "$result" | jq -e '.context.CGroup.CPUQuotaPeriodUSec == 20000' -echo "$result" | jq -e '.context.CGroup.StartupMemoryLow == 4194304' -echo "$result" | jq -e '.context.CGroup.MemoryMax == 67108864' -echo "$result" | jq -e '.context.CGroup.TasksMax == {"value":16,"scale":0}' -timeout 30 bash -c 'until systemctl is-active varlink-transient-cgroup.service; do sleep 0.5; done' -systemctl show -P CPUWeight varlink-transient-cgroup.service | grep -Fx 200 >/dev/null -systemctl show -P CPUQuotaPerSecUSec varlink-transient-cgroup.service | grep -Fx 500ms >/dev/null -systemctl show -P CPUQuotaPeriodUSec varlink-transient-cgroup.service | grep -Fx 20ms >/dev/null -systemctl show -P MemoryLow varlink-transient-cgroup.service | grep -Fx 8388608 >/dev/null -systemctl show -P MemoryHigh varlink-transient-cgroup.service | grep -Fx 33554432 >/dev/null -systemctl show -P MemoryMax varlink-transient-cgroup.service | grep -Fx 67108864 >/dev/null -systemctl show -P TasksMax varlink-transient-cgroup.service | grep -Fx 16 >/dev/null -systemctl show -P IOWeight varlink-transient-cgroup.service | grep -Fx 100 >/dev/null -# The fragment must use the unit-file directive spellings (CPUQuota=, not the JSON -# property name), otherwise the next daemon-reload would drop the setting. -fragment=$(systemctl show -P FragmentPath varlink-transient-cgroup.service) -test -n "$fragment" -grep -Fx 'CPUQuota=50.00%' "$fragment" >/dev/null -grep -Fx 'CPUQuotaPeriodSec=20ms' "$fragment" >/dev/null -grep -Fx 'MemoryMax=67108864' "$fragment" >/dev/null -grep -Fx 'TasksMax=16' "$fragment" >/dev/null - -# The special values are written as TasksMax=infinity, CPUWeight=idle and an empty -# StartupCPUWeight= (UINT64_MAX resets a weight to the default) -defer_transient_cleanup varlink-transient-cgroup-special.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-cgroup-special.service","CGroup":{"TasksMax":{"value":18446744073709551615},"CPUWeight":0,"StartupCPUWeight":18446744073709551615},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' >/dev/null -timeout 30 bash -c 'until systemctl is-active varlink-transient-cgroup-special.service; do sleep 0.5; done' -systemctl show -P TasksMax varlink-transient-cgroup-special.service | grep -Fx infinity >/dev/null -fragment=$(systemctl show -P FragmentPath varlink-transient-cgroup-special.service) -test -n "$fragment" -grep -Fx 'TasksMax=infinity' "$fragment" >/dev/null -grep -Fx 'CPUWeight=idle' "$fragment" >/dev/null -grep -Fx 'StartupCPUWeight=' "$fragment" >/dev/null - -# Cgroup limits materialize on the live cgroup -defer_transient_cleanup varlink-transient-cgroup-live.service -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-cgroup-live.service","CGroup":{"MemoryMax":67108864,"TasksMax":{"value":16}},"Service":{"ExecStart":[{"path":"/usr/bin/sleep","arguments":["/usr/bin/sleep","infinity"]}]}}}' >/dev/null -timeout 10 bash -c 'until systemctl is-active varlink-transient-cgroup-live.service; do sleep 0.5; done' -cgroup_path=$(systemctl show -P ControlGroup varlink-transient-cgroup-live.service) -test -n "$cgroup_path" -grep -Fx 67108864 "/sys/fs/cgroup${cgroup_path}/memory.max" >/dev/null -grep -Fx 16 "/sys/fs/cgroup${cgroup_path}/pids.max" >/dev/null - -# Slice= places the unit under an implicitly created slice -defer_transient_cleanup varlink-transient-slice.service -defer_transient_cleanup varlink-test.slice -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-slice.service","CGroup":{"Slice":"varlink-test.slice"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.CGroup.Slice == "varlink-test.slice"' -timeout 30 bash -c 'until systemctl is-active varlink-transient-slice.service; do sleep 0.5; done' -systemctl show -P Slice varlink-transient-slice.service | grep -Fx varlink-test.slice >/dev/null - -# Exec sandbox scalars: enum and yes/no-string properties round-trip into the reply -# context (Varlink spelling), systemctl show and the transient fragment (unit-file -# spelling, so the next daemon-reload keeps the setting) -defer_transient_cleanup varlink-transient-sandbox.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-sandbox.service","Exec":{"ProtectSystem":"strict","ProtectHome":"read_only","PrivateTmp":"disconnected","PrivateNetwork":"yes","PrivateUsers":"self"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.Exec.ProtectSystem == "strict"' -echo "$result" | jq -e '.context.Exec.ProtectHome == "read_only"' -echo "$result" | jq -e '.context.Exec.PrivateTmp == "disconnected"' -echo "$result" | jq -e '.context.Exec.PrivateNetwork == "yes"' -echo "$result" | jq -e '.context.Exec.PrivateUsers == "self"' -timeout 30 bash -c 'until systemctl is-active varlink-transient-sandbox.service; do sleep 0.5; done' -systemctl show -P ProtectSystem varlink-transient-sandbox.service | grep '^strict$' >/dev/null -systemctl show -P ProtectHome varlink-transient-sandbox.service | grep '^read-only$' >/dev/null -systemctl show -P PrivateTmpEx varlink-transient-sandbox.service | grep '^disconnected$' >/dev/null -systemctl show -P PrivateNetwork varlink-transient-sandbox.service | grep '^yes$' >/dev/null -systemctl show -P PrivateUsersEx varlink-transient-sandbox.service | grep '^self$' >/dev/null -fragment=$(systemctl show -P FragmentPath varlink-transient-sandbox.service) -test -n "$fragment" -grep '^ProtectSystem=strict$' "$fragment" >/dev/null -grep '^ProtectHome=read-only$' "$fragment" >/dev/null -grep '^PrivateTmp=disconnected$' "$fragment" >/dev/null -grep '^PrivateNetwork=yes$' "$fragment" >/dev/null -grep '^PrivateUsers=self$' "$fragment" >/dev/null - -# The sandbox is live, not just recorded: the unit only succeeds if /usr is read-only -# and its network namespace differs from PID 1's. Both readlinks are captured first, so -# an unreadable link fails the unit rather than passing it. Comparing namespaces beats -# listing interfaces here: the harness runs with --runtime-network=none, and the netns -# fallback devices come and go with the loaded modules. -defer_transient_cleanup varlink-transient-sandbox-live.service -# shellcheck disable=SC2016 -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-sandbox-live.service","Exec":{"ProtectSystem":"strict","PrivateNetwork":"yes"},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/sh","arguments":["/bin/sh","-c","p=$(readlink /proc/1/ns/net) && s=$(readlink /proc/self/ns/net) && test \"$s\" != \"$p\" && ! touch /usr/varlink-sandbox-canary"]}]}}}' >/dev/null -timeout 30 bash -c 'until systemctl show -P ActiveState varlink-transient-sandbox-live.service | grep -Ex "active|failed" >/dev/null; do sleep 0.5; done' -systemctl show -P Result varlink-transient-sandbox-live.service | grep '^success$' >/dev/null -test ! -e /usr/varlink-sandbox-canary - -# Relative forms: TasksMaxScale/MemoryMaxScale (floats, 1.0 == 100%) are input only, -# converted internally and written to the fragment as permyriad percentages. An -# explicit null is treated as absent (so MemoryMax=null does not conflict with -# MemoryMaxScale), and a zero scale is accepted for the min-0 memory properties, -# as over D-Bus -defer_transient_cleanup varlink-transient-cgroup-scale.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-cgroup-scale.service","CGroup":{"TasksMaxScale":0.5,"MemoryMax":null,"MemoryMaxScale":0.125,"MemoryMinScale":0.0,"TasksAccounting":null},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.CGroup.TasksMax == {"value":5000,"scale":10000}' -echo "$result" | jq -e '.context.CGroup.TasksMaxScale == null' -timeout 30 bash -c 'until systemctl is-active varlink-transient-cgroup-scale.service; do sleep 0.5; done' -fragment=$(systemctl show -P FragmentPath varlink-transient-cgroup-scale.service) -test -n "$fragment" -grep -Fx 'TasksMax=50.00%' "$fragment" >/dev/null -grep -Fx 'MemoryMax=12.50%' "$fragment" >/dev/null -grep -Fx 'MemoryMin=0.00%' "$fragment" >/dev/null -(! grep -F 'TasksAccounting=' "$fragment") - -# A null scale member of TasksMax is absent, not a request for the relative form -defer_transient_cleanup varlink-transient-cgroup-nullscale.service -result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-cgroup-nullscale.service","CGroup":{"TasksMax":{"value":16,"scale":null}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') -echo "$result" | jq -e '.context.CGroup.TasksMax == {"value":16,"scale":0}' - -# 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 +testcase_set_properties() { + set +o pipefail + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties '{"runtime": true, "name": "non-existent.service", "properties": {"Markers": ["needs-restart"]}}' |& grep "io.systemd.Unit.NoSuchUnit" + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties '{"runtime": true, "name": "systemd-journald.service", "properties": {"LoadState": "foobar"}}' |& grep "io.systemd.Unit.PropertyNotSupported" + set -o pipefail } -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" -# The 'privileged' and 'noSetuid' execution flags are mutually exclusive -defer_transient_cleanup varlink-transient-badflags.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-badflags.service","Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true","privileged":true,"noSetuid":true}]}}}' \ - "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" -# Out-of-range OOMScoreAdjust= value is rejected -defer_transient_cleanup varlink-transient-bad-oom.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-oom.service","Exec":{"OOMScoreAdjust":9999},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "Exec.OOMScoreAdjust" -# Relative RootDirectory path is rejected -defer_transient_cleanup varlink-transient-bad-rd.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-rd.service","Exec":{"RootDirectory":"relative/path"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "Exec.RootDirectory" -# Note that an unparsable enum value (e.g. StandardOutput=bogus) is rejected by the Varlink IDL -# validation layer before the JSON dispatch even runs, hence there's no point in testing pid1's -# dispatch-level enum handling here — and doing so would trip post.sh's "didn't pass validation" -# journal check, since the IDL layer logs about the rejection. -# Relative TTYPath is rejected -defer_transient_cleanup varlink-transient-bad-ttypath.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-ttypath.service","Exec":{"TTYPath":"relative/tty"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "Exec.TTYPath" -# The file based stdio types need a companion path, and StandardInput=data a companion payload, neither of -# which can be passed yet, hence they are rejected -for t in file append truncate; do - defer_transient_cleanup "varlink-transient-bad-stdout-$t.service" - expect_invalid_parameter \ - "{\"context\":{\"ID\":\"varlink-transient-bad-stdout-$t.service\",\"Exec\":{\"StandardOutput\":\"$t\"},\"Service\":{\"Type\":\"oneshot\",\"ExecStart\":[{\"path\":\"/bin/true\"}]}}}" \ - "Exec.StandardOutput" -done -defer_transient_cleanup varlink-transient-bad-stderr.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-stderr.service","Exec":{"StandardError":"file"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "Exec.StandardError" -for t in file data; do - defer_transient_cleanup "varlink-transient-bad-stdin-$t.service" - expect_invalid_parameter \ - "{\"context\":{\"ID\":\"varlink-transient-bad-stdin-$t.service\",\"Exec\":{\"StandardInput\":\"$t\"},\"Service\":{\"Type\":\"oneshot\",\"ExecStart\":[{\"path\":\"/bin/true\"}]}}}" \ - "Exec.StandardInput" -done -# 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" -# Bad PID on a scope -defer_transient_cleanup varlink-transient-badpid.scope -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-badpid.scope","Scope":{"PIDs":[{"pid": 1}]}}}' \ - "Scope.PIDs" -# Dependency on invalid unit (rejected at dispatch time) -defer_transient_cleanup varlink-invalid-dependency.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-invalid-dependency.service","Requires":["invalid@.service"],"Service":{"ExecStart":[{"path":"/usr/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" -# Same for Kill on a unit type without a kill context -varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-kill.slice","Kill":{"KillMode":"process"}}}' |& 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":{"AmbientCapabilities":["cap_net_raw"]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true) -echo "$unsupported_exec" | grep "io.systemd.Unit.PropertyNotSupported" -echo "$unsupported_exec" | grep "Exec.AmbientCapabilities" -# 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" -# Same for a Kill field -defer_transient_cleanup varlink-transient-unknown-kill.service -unsupported_kill=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-unknown-kill.service","Kill":{"KillSignal":"SIGINT"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true) -echo "$unsupported_kill" | grep "io.systemd.Unit.PropertyNotSupported" -echo "$unsupported_kill" | grep "Kill.KillSignal" -# Same for non-transient dependency types -defer_transient_cleanup varlink-non-transient-dependency.service -non_transient_dep=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-non-transient-dependency.service","Triggers":["varlink-transient-exists.service"],"Service":{"ExecStart":[{"path":"/usr/bin/true"}]}}}' 2>&1 || true) -echo "$non_transient_dep" | grep "io.systemd.Unit.PropertyNotSupported" -echo "$non_transient_dep" | grep "Triggers" -# Out-of-range CPUWeight= is rejected -defer_transient_cleanup varlink-transient-bad-weight.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-weight.service","CGroup":{"CPUWeight":123456},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "CGroup.CPUWeight" -# IOWeight=0 is rejected, only the CPU weights accept idle -defer_transient_cleanup varlink-transient-bad-ioweight.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-ioweight.service","CGroup":{"IOWeight":0},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "CGroup.IOWeight" -# MemoryMax=0 is rejected (minimum 1, matching the D-Bus setter) -defer_transient_cleanup varlink-transient-bad-memmax.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-memmax.service","CGroup":{"MemoryMax":0},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "CGroup.MemoryMax" -# The scale member of TasksMax is output only, TasksMaxScale is the input form -defer_transient_cleanup varlink-transient-bad-taskscale-member.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-taskscale-member.service","CGroup":{"TasksMax":{"value":5000,"scale":10000}},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "context" -# Out-of-range TasksMaxScale= (>= 100%) is rejected -defer_transient_cleanup varlink-transient-bad-tasksscale.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-tasksscale.service","CGroup":{"TasksMaxScale":1.5},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "CGroup.TasksMaxScale" -# Absolute and relative forms of the same limit may not be combined -defer_transient_cleanup varlink-transient-bad-taskscombo.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-taskscombo.service","CGroup":{"TasksMax":{"value":16},"TasksMaxScale":0.5},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "CGroup.TasksMaxScale" -# Zero MemoryMaxScale= is rejected -defer_transient_cleanup varlink-transient-bad-memscale.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-memscale.service","CGroup":{"MemoryMaxScale":0.0},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "CGroup.MemoryMaxScale" -# Slice= pointing at a non-slice unit is rejected -defer_transient_cleanup varlink-transient-bad-slice.service -expect_invalid_parameter \ - '{"context":{"ID":"varlink-transient-bad-slice.service","CGroup":{"Slice":"not-a-slice.service"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' \ - "CGroup.Slice" -# CGroup 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-cgroup.service -unsupported_cgroup=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ - '{"context":{"ID":"varlink-transient-unknown-cgroup.service","CGroup":{"DeviceAllow":[]},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' 2>&1 || true) -echo "$unsupported_cgroup" | grep "io.systemd.Unit.PropertyNotSupported" -echo "$unsupported_cgroup" | grep "CGroup.DeviceAllow" -set -o pipefail + +testcase_contexts() { + # test for KillContext + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"pid": {"pid": 0}}' | jq -e '.context.Kill' + # test for AutomountContext/Runtime + automount_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "automount" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1) + test -n "$automount_id" + # Use jq to JSON-encode the unit name as it may contain backslash escapes (e.g. \x2d) that + # are not valid JSON escape sequences and would be rejected by varlinkctl's JSON parser. + automount_params=$(jq -cn --arg name "$automount_id" '{name: $name}') + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$automount_params" | jq -e '.context.Automount' + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$automount_params" | jq -e '.runtime.Automount' + # test for MountContext/Runtime (skip volatile run-user-*.mount to avoid GC race) + mount_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "mount" and .runtime.LoadState == "loaded" and (.context.ID | startswith("run-user-") | not)) .context.ID // empty' | tail -n 1) + test -n "$mount_id" + mount_params=$(jq -cn --arg name "$mount_id" '{name: $name}') + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$mount_params" | jq -e '.context.Mount' + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$mount_params" | jq -e '.runtime.Mount' + # test for PathContext/Runtime + path_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "path" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1) + test -n "$path_id" + path_params=$(jq -cn --arg name "$path_id" '{name: $name}') + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$path_params" | jq -e '.context.Path' + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$path_params" | jq -e '.runtime.Path' + # test for ServiceContext/Runtime (skip volatile user@*/user-runtime-dir@* to avoid GC race) + service_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "service" and .runtime.LoadState == "loaded" and (.context.ID | test("^(user|user-runtime-dir)@") | not)) .context.ID // empty' | tail -n 1) + test -n "$service_id" + service_params=$(jq -cn --arg name "$service_id" '{name: $name}') + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$service_params" | jq -e '.context.Service' + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$service_params" | jq -e '.runtime.Service' + # test for ScopeContext/Runtime (skip volatile session-*.scope to avoid GC race) + scope_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "scope" and .runtime.LoadState == "loaded" and (.context.ID | startswith("session-") | not)) .context.ID // empty' | tail -n 1) + test -n "$scope_id" + scope_params=$(jq -cn --arg name "$scope_id" '{name: $name}') + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$scope_params" | jq -e '.context.Scope' + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$scope_params" | jq -e '.runtime.Scope' + # test for SocketContext/Runtime + socket_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "socket" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1) + test -n "$socket_id" + socket_params=$(jq -cn --arg name "$socket_id" '{name: $name}') + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$socket_params" | jq -e '.context.Socket' + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$socket_params" | jq -e '.runtime.Socket' + # test for SwapContext/Runtime (swap units may not be present on all systems) + swap_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "swap" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1) + if test -n "$swap_id"; then + swap_params=$(jq -cn --arg name "$swap_id" '{name: $name}') + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$swap_params" | jq -e '.context.Swap' + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$swap_params" | jq -e '.runtime.Swap' + fi + # test for TimerContext/Runtime + timer_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "timer" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1) + test -n "$timer_id" + timer_params=$(jq -cn --arg name "$timer_id" '{name: $name}') + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$timer_params" | jq -e '.context.Timer' + varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$timer_params" | jq -e '.runtime.Timer' +} + +testcase_user_manager() { + # test io.systemd.Unit in user manager + 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 '{}' +} + +run_testcases