python312Packages.pyoverkiz: 1.15.2 -> 1.15.3 (#366663)
[NixPkgs.git] / nixos / tests / rtkit.nix
blobd884a588a16e248a3929c833c60b63a92ca454d4
1 { lib, ... }:
4   name = "rtkit";
6   meta.maintainers = with lib.maintainers; [ rvl ];
8   nodes.machine =
9     { config, pkgs, ... }:
10     {
11       assertions = [
12         {
13           assertion = config.security.polkit.enable;
14           message = "rtkit needs polkit to handle authorization";
15         }
16       ];
18       imports = [ ./common/user-account.nix ];
19       services.getty.autologinUser = "alice";
21       security.rtkit.enable = true;
23       # Modified configuration with higher maximum realtime priority.
24       specialisation.withHigherPrio.configuration = {
25         security.rtkit.args = [
26           "--max-realtime-priority=89"
27           "--our-realtime-priority=90"
28         ];
29       };
31       # Target process for testing - belongs to a logind session.
32       systemd.user.services.sleeper = {
33         description = "Guinea pig service";
34         serviceConfig = {
35           ExecStart = "@${pkgs.coreutils}/bin/sleep sleep inf";
36           # rtkit-daemon won't grant real-time to threads unless they have a rttime limit.
37           LimitRTTIME = 200000;
38         };
39         wantedBy = [ "default.target" ];
40       };
42       # Target process for testing - doesn't belong to a session.
43       systemd.services."sleeper@" = {
44         description = "Guinea pig system service for %I";
45         serviceConfig = {
46           ExecStart = "@${pkgs.coreutils}/bin/sleep sleep inf";
47           LimitRTTIME = 200000;
48           User = "%I";
49         };
50       };
51       systemd.targets.multi-user.wants = [ "sleeper@alice.service" ];
53       # Install chrt to check outcomes of rtkit calls
54       environment.systemPackages = [ pkgs.util-linux ];
56       # Provide a little logging of polkit checks - otherwise it's
57       # impossible to know what's going on.
58       security.polkit.debug = true;
59       security.polkit.extraConfig = ''
60         polkit.addRule(function(action, subject) {
61           const ns = "org.freedesktop.RealtimeKit1.";
62           const acquireHighPrio = ns + "acquire-high-priority";
63           const acquireRT = ns + "acquire-real-time";
64           if (action.id == acquireHighPrio || action.id == acquireRT) {
65             polkit.log("rtkit: Checking " + action.id + " for " + subject.user + "\n  " + subject);
66           }
67         });
68       '';
69     };
71   interactive.nodes.machine =
72     { pkgs, ... }:
73     {
74       security.rtkit.args = [ "--debug" ];
75       systemd.services.strace-rtkit =
76         let
77           target = "rtkit-daemon.service";
78         in
79         {
80           bindsTo = [ target ];
81           after = [ target ];
82           scriptArgs = target;
83           script = ''
84             pid=$(systemctl show -P MainPID $1)
85             strace -tt -s 100 -e trace=all -p $pid
86           '';
87           path = [ pkgs.strace ];
88         };
89     };
91   testScript =
92     { nodes, ... }:
93     let
94       specialisations = "${nodes.machine.system.build.toplevel}/specialisation";
95       uid = toString nodes.machine.users.users.alice.uid;
96     in
97     ''
98       import json
99       import shlex
100       from collections import namedtuple
101       from typing import Any, Optional
103       Result = namedtuple("Result", ["command", "machine", "status", "out", "value"])
104       Value = namedtuple("Value", ["type", "data"])
106       def busctl(node: Machine, *args: Any, user: Optional[str] = None) -> Result:
107           command = f"busctl --json=short {shlex.join(map(str, args))}"
108           if user is not None:
109               command = f"su - {user} -c {shlex.quote(command)}"
110           (status, out) = node.execute(command)
111           out = out.strip()
112           value = json.loads(out, object_hook=lambda x: Value(**x)) if status == 0 and out else None
113           return Result(command, node, status, out, value)
115       def assert_result_success(result: Result):
116           if result.status != 0:
117               result.machine.log(f"output: {result.out}")
118               raise Exception(f"command `{result.command}` failed (exit code {result.status})")
120       def assert_result_fail(result: Result):
121           if result.status == 0:
122               raise Exception(f"command `{result.command}` unexpectedly succeeded")
124       def rtkit_make_process_realtime(node: Machine, pid: int, priority: int, user: Optional[str] = None) -> Result:
125           return busctl(node, "call", "org.freedesktop.RealtimeKit1", "/org/freedesktop/RealtimeKit1", "org.freedesktop.RealtimeKit1", "MakeThreadRealtimeWithPID", "ttu", pid, 0, priority, user=user)
127       def get_max_realtime_priority() -> int:
128           result = busctl(machine, "get-property", "org.freedesktop.RealtimeKit1", "/org/freedesktop/RealtimeKit1", "org.freedesktop.RealtimeKit1", "MaxRealtimePriority")
129           assert_result_success(result)
130           assert result.value.type == "i", f"""Unexpected MaxRealtimePriority property type ({result.value})"""
131           return int(result.value.data)
133       def parse_chrt(out: str, field: str) -> str:
134           return next(map(lambda l: l.split(": ")[1], filter(lambda l: field in l, out.splitlines())))
136       def get_pid(node: Machine, unit: str, user: Optional[str] = None) -> int:
137           node.wait_for_unit(unit, user=user)
138           (status, out) = node.systemctl(f"show -P MainPID {unit}", user=user)
139           if status == 0:
140               return int(out.strip())
141           else:
142               node.log(out)
143               raise Exception(f"unable to determine MainPID of {unit} (systemctl exit code {status})")
145       def assert_sched(node: Machine, pid: int, policy: Optional[str] = None, priority: Optional[int] = None):
146           out = node.succeed(f"chrt -p {pid}")
147           node.log(out)
148           if policy is not None:
149               thread_policy = parse_chrt(out, "policy")
150               assert policy in thread_policy, f"Expected {policy} scheduling policy, but got: {thread_policy}"
151           if priority is not None:
152               thread_priority = parse_chrt(out, "priority")
153               assert str(priority) == thread_priority, f"Expected scheduling priority {priority}, but got: {thread_priority}"
155       machine.wait_for_unit("basic.target")
157       rtprio = 20
158       higher_rtprio = 42
159       max_rtprio = get_max_realtime_priority()
161       with subtest("maximum sched_rr priority"):
162           assert max_rtprio >= rtprio, f"""MaxRealtimePriority ({max_rtprio}) too low"""
163           assert higher_rtprio > max_rtprio, f"""Test value higher_rtprio ({higher_rtprio}) insufficient compared to MaxRealtimePriority ({max_rtprio})"""
165       # wait for autologin and systemd user service manager
166       machine.wait_for_unit("multi-user.target")
167       machine.wait_for_unit("user@${uid}.service")
169       with subtest("polkit sanity check"):
170           pid = get_pid(machine, "sleeper.service", user="alice")
171           machine.succeed(f"pkcheck -p {pid} -a org.freedesktop.RealtimeKit1.acquire-real-time")
173       with subtest("chrt sanity check"):
174           print(machine.succeed("chrt --rr --max"))
175           pid = get_pid(machine, "sleeper.service", user="alice")
176           machine.succeed(f"chrt --rr --pid {rtprio} {pid}")
177           assert_sched(machine, pid, policy="SCHED_RR", priority=rtprio)
178           machine.stop_job("sleeper.service", user="alice")
179           machine.start_job("sleeper.service", user="alice")
181       # Permission granted by policy from rtkit package.
182       with subtest("local user process can acquire real-time scheduling"):
183           pid = get_pid(machine, "sleeper.service", user="alice")
184           result = rtkit_make_process_realtime(machine, pid, rtprio, user="alice")
185           assert_result_success(result)
186           assert_sched(machine, pid, policy="SCHED_RR", priority=rtprio)
188       # User must not get higher priority than the maximum
189       with subtest("real-time scheduling priority is limited"):
190           machine.stop_job("sleeper.service", user="alice")
191           machine.start_job("sleeper.service", user="alice")
192           pid = get_pid(machine, "sleeper.service", user="alice")
193           with machine.nested("rtkit call must fail"):
194               result = rtkit_make_process_realtime(machine, pid, max_rtprio + 1, user="alice")
195               assert_result_fail(result)
196           assert_sched(machine, pid, policy="SCHED_OTHER")
198       # This is a local shop for local people - we'll have no trouble here.
199       # In this test, the target process belongs to alice, but doesn't
200       # have a user session, so it's considered non-local.
201       with subtest("non-local user process cannot acquire real-time scheduling"):
202           pid = get_pid(machine, "sleeper@alice.service")
203           with machine.nested("rtkit call must fail"):
204               result = rtkit_make_process_realtime(machine, pid, rtprio, "alice")
205               assert_result_fail(result)
206           assert_sched(machine, pid, policy="SCHED_OTHER")
208       # Switch to alternate configuration then ask for higher priority.
209       with subtest("command-line arguments allow increasing maximum rtprio"):
210           machine.succeed("${specialisations}/withHigherPrio/bin/switch-to-configuration test")
211           pid = get_pid(machine, "sleeper.service", user="alice")
212           result = rtkit_make_process_realtime(machine, pid, higher_rtprio, user="alice")
213           assert_result_success(result)
214           assert_sched(machine, pid, policy="SCHED_RR", priority=higher_rtprio)
215     '';