1# Test configuration switching.
2
3import ./make-test-python.nix ({ pkgs, ...} : {
4 name = "switch-test";
5 meta = with pkgs.lib.maintainers; {
6 maintainers = [ gleber ];
7 };
8
9 nodes = {
10 machine = { config, pkgs, lib, ... }: {
11 environment.systemPackages = [ pkgs.socat ]; # for the socket activation stuff
12 users.mutableUsers = false;
13
14 specialisation = {
15 # A system with a simple socket-activated unit
16 simple-socket.configuration = {
17 systemd.services.socket-activated.serviceConfig = {
18 ExecStart = pkgs.writeScript "socket-test.py" /* python */ ''
19 #!${pkgs.python3}/bin/python3
20
21 from socketserver import TCPServer, StreamRequestHandler
22 import socket
23
24 class Handler(StreamRequestHandler):
25 def handle(self):
26 self.wfile.write("hello".encode("utf-8"))
27
28 class Server(TCPServer):
29 def __init__(self, server_address, handler_cls):
30 # Invoke base but omit bind/listen steps (performed by systemd activation!)
31 TCPServer.__init__(
32 self, server_address, handler_cls, bind_and_activate=False)
33 # Override socket
34 self.socket = socket.fromfd(3, self.address_family, self.socket_type)
35
36 if __name__ == "__main__":
37 server = Server(("localhost", 1234), Handler)
38 server.serve_forever()
39 '';
40 };
41 systemd.sockets.socket-activated = {
42 wantedBy = [ "sockets.target" ];
43 listenStreams = [ "/run/test.sock" ];
44 socketConfig.SocketMode = lib.mkDefault "0777";
45 };
46 };
47
48 # The same system but the socket is modified
49 modified-socket.configuration = {
50 imports = [ config.specialisation.simple-socket.configuration ];
51 systemd.sockets.socket-activated.socketConfig.SocketMode = "0666";
52 };
53
54 # The same system but the service is modified
55 modified-service.configuration = {
56 imports = [ config.specialisation.simple-socket.configuration ];
57 systemd.services.socket-activated.serviceConfig.X-Test = "test";
58 };
59
60 # The same system but both service and socket are modified
61 modified-service-and-socket.configuration = {
62 imports = [ config.specialisation.simple-socket.configuration ];
63 systemd.services.socket-activated.serviceConfig.X-Test = "some_value";
64 systemd.sockets.socket-activated.socketConfig.SocketMode = "0444";
65 };
66
67 # A system with a socket-activated service and some simple services
68 service-and-socket.configuration = {
69 imports = [ config.specialisation.simple-socket.configuration ];
70 systemd.services.simple-service = {
71 wantedBy = [ "multi-user.target" ];
72 serviceConfig = {
73 Type = "oneshot";
74 RemainAfterExit = true;
75 ExecStart = "${pkgs.coreutils}/bin/true";
76 };
77 };
78
79 systemd.services.simple-restart-service = {
80 stopIfChanged = false;
81 wantedBy = [ "multi-user.target" ];
82 serviceConfig = {
83 Type = "oneshot";
84 RemainAfterExit = true;
85 ExecStart = "${pkgs.coreutils}/bin/true";
86 };
87 };
88
89 systemd.services.simple-reload-service = {
90 reloadIfChanged = true;
91 wantedBy = [ "multi-user.target" ];
92 serviceConfig = {
93 Type = "oneshot";
94 RemainAfterExit = true;
95 ExecStart = "${pkgs.coreutils}/bin/true";
96 ExecReload = "${pkgs.coreutils}/bin/true";
97 };
98 };
99
100 systemd.services.no-restart-service = {
101 restartIfChanged = false;
102 wantedBy = [ "multi-user.target" ];
103 serviceConfig = {
104 Type = "oneshot";
105 RemainAfterExit = true;
106 ExecStart = "${pkgs.coreutils}/bin/true";
107 };
108 };
109 };
110
111 # The same system but with an activation script that restarts all services
112 restart-and-reload-by-activation-script.configuration = {
113 imports = [ config.specialisation.service-and-socket.configuration ];
114 system.activationScripts.restart-and-reload-test = {
115 supportsDryActivation = true;
116 deps = [];
117 text = ''
118 if [ "$NIXOS_ACTION" = dry-activate ]; then
119 f=/run/nixos/dry-activation-restart-list
120 else
121 f=/run/nixos/activation-restart-list
122 fi
123 cat <<EOF >> "$f"
124 simple-service.service
125 simple-restart-service.service
126 simple-reload-service.service
127 no-restart-service.service
128 socket-activated.service
129 EOF
130 '';
131 };
132 };
133
134 # A system with a timer
135 with-timer.configuration = {
136 systemd.timers.test-timer = {
137 wantedBy = [ "timers.target" ];
138 timerConfig.OnCalendar = "@1395716396"; # chosen by fair dice roll
139 };
140 systemd.services.test-timer = {
141 serviceConfig = {
142 Type = "oneshot";
143 ExecStart = "${pkgs.coreutils}/bin/true";
144 };
145 };
146 };
147
148 # The same system but with another time
149 with-timer-modified.configuration = {
150 imports = [ config.specialisation.with-timer.configuration ];
151 systemd.timers.test-timer.timerConfig.OnCalendar = lib.mkForce "Fri 2012-11-23 16:00:00";
152 };
153
154 # A system with a systemd mount
155 with-mount.configuration = {
156 systemd.mounts = [
157 {
158 description = "Testmount";
159 what = "tmpfs";
160 type = "tmpfs";
161 where = "/testmount";
162 options = "size=1M";
163 wantedBy = [ "local-fs.target" ];
164 }
165 ];
166 };
167
168 # The same system but with another time
169 with-mount-modified.configuration = {
170 systemd.mounts = [
171 {
172 description = "Testmount";
173 what = "tmpfs";
174 type = "tmpfs";
175 where = "/testmount";
176 options = "size=10M";
177 wantedBy = [ "local-fs.target" ];
178 }
179 ];
180 };
181
182 # A system with a path unit
183 with-path.configuration = {
184 systemd.paths.test-watch = {
185 wantedBy = [ "paths.target" ];
186 pathConfig.PathExists = "/testpath";
187 };
188 systemd.services.test-watch = {
189 serviceConfig = {
190 Type = "oneshot";
191 ExecStart = "${pkgs.coreutils}/bin/touch /testpath-modified";
192 };
193 };
194 };
195
196 # The same system but watching another file
197 with-path-modified.configuration = {
198 imports = [ config.specialisation.with-path.configuration ];
199 systemd.paths.test-watch.pathConfig.PathExists = lib.mkForce "/testpath2";
200 };
201
202 # A system with a slice
203 with-slice.configuration = {
204 systemd.slices.testslice.sliceConfig.MemoryMax = "1"; # don't allow memory allocation
205 systemd.services.testservice = {
206 serviceConfig = {
207 Type = "oneshot";
208 RemainAfterExit = true;
209 ExecStart = "${pkgs.coreutils}/bin/true";
210 Slice = "testslice.slice";
211 };
212 };
213 };
214
215 # The same system but the slice allows to allocate memory
216 with-slice-non-crashing.configuration = {
217 imports = [ config.specialisation.with-slice.configuration ];
218 systemd.slices.testslice.sliceConfig.MemoryMax = lib.mkForce null;
219 };
220 };
221 };
222 other = { ... }: {
223 users.mutableUsers = true;
224 };
225 };
226
227 testScript = { nodes, ... }: let
228 originalSystem = nodes.machine.config.system.build.toplevel;
229 otherSystem = nodes.other.config.system.build.toplevel;
230
231 # Ensures failures pass through using pipefail, otherwise failing to
232 # switch-to-configuration is hidden by the success of `tee`.
233 stderrRunner = pkgs.writeScript "stderr-runner" ''
234 #! ${pkgs.runtimeShell}
235 set -e
236 set -o pipefail
237 exec env -i "$@" | tee /dev/stderr
238 '';
239 in /* python */ ''
240 def switch_to_specialisation(name, action="test"):
241 out = machine.succeed(f"${originalSystem}/specialisation/{name}/bin/switch-to-configuration {action} 2>&1")
242 assert_lacks(out, "switch-to-configuration line") # Perl warnings
243 return out
244
245 def assert_contains(haystack, needle):
246 if needle not in haystack:
247 print("The haystack that will cause the following exception is:")
248 print("---")
249 print(haystack)
250 print("---")
251 raise Exception(f"Expected string '{needle}' was not found")
252
253 def assert_lacks(haystack, needle):
254 if needle in haystack:
255 print("The haystack that will cause the following exception is:")
256 print("---")
257 print(haystack, end="")
258 print("---")
259 raise Exception(f"Unexpected string '{needle}' was found")
260
261
262 machine.succeed(
263 "${stderrRunner} ${originalSystem}/bin/switch-to-configuration test"
264 )
265 machine.succeed(
266 "${stderrRunner} ${otherSystem}/bin/switch-to-configuration test"
267 )
268
269 with subtest("systemd sockets"):
270 machine.succeed("${originalSystem}/bin/switch-to-configuration test")
271
272 # Simple socket is created
273 out = switch_to_specialisation("simple-socket")
274 assert_lacks(out, "stopping the following units:")
275 # not checking for reload because dbus gets reloaded
276 assert_lacks(out, "restarting the following units:")
277 assert_lacks(out, "\nstarting the following units:")
278 assert_contains(out, "the following new units were started: socket-activated.socket\n")
279 assert_lacks(out, "as well:")
280 machine.succeed("[ $(stat -c%a /run/test.sock) = 777 ]")
281
282 # Changing the socket restarts it
283 out = switch_to_specialisation("modified-socket")
284 assert_lacks(out, "stopping the following units:")
285 #assert_lacks(out, "reloading the following units:")
286 assert_contains(out, "restarting the following units: socket-activated.socket\n")
287 assert_lacks(out, "\nstarting the following units:")
288 assert_lacks(out, "the following new units were started:")
289 assert_lacks(out, "as well:")
290 machine.succeed("[ $(stat -c%a /run/test.sock) = 666 ]") # change was applied
291
292 # The unit is properly activated when the socket is accessed
293 if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello":
294 raise Exception("Socket was not properly activated")
295
296 # Changing the socket restarts it and ignores the active service
297 out = switch_to_specialisation("simple-socket")
298 assert_contains(out, "stopping the following units: socket-activated.service\n")
299 assert_lacks(out, "reloading the following units:")
300 assert_contains(out, "restarting the following units: socket-activated.socket\n")
301 assert_lacks(out, "\nstarting the following units:")
302 assert_lacks(out, "the following new units were started:")
303 assert_lacks(out, "as well:")
304 machine.succeed("[ $(stat -c%a /run/test.sock) = 777 ]") # change was applied
305
306 # Changing the service does nothing when the service is not active
307 out = switch_to_specialisation("modified-service")
308 assert_lacks(out, "stopping the following units:")
309 assert_lacks(out, "reloading the following units:")
310 assert_lacks(out, "restarting the following units:")
311 assert_lacks(out, "\nstarting the following units:")
312 assert_lacks(out, "the following new units were started:")
313 assert_lacks(out, "as well:")
314
315 # Activating the service and modifying it stops it but leaves the socket untouched
316 machine.succeed("socat - UNIX-CONNECT:/run/test.sock")
317 out = switch_to_specialisation("simple-socket")
318 assert_contains(out, "stopping the following units: socket-activated.service\n")
319 assert_lacks(out, "reloading the following units:")
320 assert_lacks(out, "restarting the following units:")
321 assert_lacks(out, "\nstarting the following units:")
322 assert_lacks(out, "the following new units were started:")
323 assert_lacks(out, "as well:")
324
325 # Activating the service and both the service and the socket stops the service and restarts the socket
326 machine.succeed("socat - UNIX-CONNECT:/run/test.sock")
327 out = switch_to_specialisation("modified-service-and-socket")
328 assert_contains(out, "stopping the following units: socket-activated.service\n")
329 assert_lacks(out, "reloading the following units:")
330 assert_contains(out, "restarting the following units: socket-activated.socket\n")
331 assert_lacks(out, "\nstarting the following units:")
332 assert_lacks(out, "the following new units were started:")
333 assert_lacks(out, "as well:")
334
335 with subtest("restart and reload by activation file"):
336 out = switch_to_specialisation("service-and-socket")
337 # Switch to a system where the example services get restarted
338 # by the activation script
339 out = switch_to_specialisation("restart-and-reload-by-activation-script")
340 assert_lacks(out, "stopping the following units:")
341 assert_contains(out, "stopping the following units as well: simple-service.service, socket-activated.service\n")
342 assert_contains(out, "reloading the following units: simple-reload-service.service\n")
343 assert_contains(out, "restarting the following units: simple-restart-service.service\n")
344 assert_contains(out, "\nstarting the following units: simple-service.service")
345
346 # The same, but in dry mode
347 switch_to_specialisation("service-and-socket")
348 out = switch_to_specialisation("restart-and-reload-by-activation-script", action="dry-activate")
349 assert_lacks(out, "would stop the following units:")
350 assert_contains(out, "would stop the following units as well: simple-service.service, socket-activated.service\n")
351 assert_contains(out, "would reload the following units: simple-reload-service.service\n")
352 assert_contains(out, "would restart the following units: simple-restart-service.service\n")
353 assert_contains(out, "\nwould start the following units: simple-service.service")
354
355 with subtest("mounts"):
356 switch_to_specialisation("with-mount")
357 out = machine.succeed("mount | grep 'on /testmount'")
358 assert_contains(out, "size=1024k")
359
360 out = switch_to_specialisation("with-mount-modified")
361 assert_lacks(out, "stopping the following units:")
362 assert_contains(out, "reloading the following units: testmount.mount\n")
363 assert_lacks(out, "restarting the following units:")
364 assert_lacks(out, "\nstarting the following units:")
365 assert_lacks(out, "the following new units were started:")
366 assert_lacks(out, "as well:")
367 # It changed
368 out = machine.succeed("mount | grep 'on /testmount'")
369 assert_contains(out, "size=10240k")
370
371 with subtest("timers"):
372 switch_to_specialisation("with-timer")
373 out = machine.succeed("systemctl show test-timer.timer")
374 assert_contains(out, "OnCalendar=2014-03-25 02:59:56 UTC")
375
376 out = switch_to_specialisation("with-timer-modified")
377 assert_lacks(out, "stopping the following units:")
378 assert_lacks(out, "reloading the following units:")
379 assert_contains(out, "restarting the following units: test-timer.timer\n")
380 assert_lacks(out, "\nstarting the following units:")
381 assert_lacks(out, "the following new units were started:")
382 assert_lacks(out, "as well:")
383 # It changed
384 out = machine.succeed("systemctl show test-timer.timer")
385 assert_contains(out, "OnCalendar=Fri 2012-11-23 16:00:00")
386
387 with subtest("paths"):
388 switch_to_specialisation("with-path")
389 machine.fail("test -f /testpath-modified")
390
391 # touch the file, unit should be triggered
392 machine.succeed("touch /testpath")
393 machine.wait_until_succeeds("test -f /testpath-modified")
394
395 machine.succeed("rm /testpath")
396 machine.succeed("rm /testpath-modified")
397 switch_to_specialisation("with-path-modified")
398
399 machine.succeed("touch /testpath")
400 machine.fail("test -f /testpath-modified")
401 machine.succeed("touch /testpath2")
402 machine.wait_until_succeeds("test -f /testpath-modified")
403
404 # This test ensures that changes to slice configuration get applied.
405 # We test this by having a slice that allows no memory allocation at
406 # all and starting a service within it. If the service crashes, the slice
407 # is applied and if we modify the slice to allow memory allocation, the
408 # service should successfully start.
409 with subtest("slices"):
410 machine.succeed("echo 0 > /proc/sys/vm/panic_on_oom") # allow OOMing
411 out = switch_to_specialisation("with-slice")
412 machine.fail("systemctl start testservice.service")
413 out = switch_to_specialisation("with-slice-non-crashing")
414 machine.succeed("systemctl start testservice.service")
415 machine.succeed("echo 1 > /proc/sys/vm/panic_on_oom") # disallow OOMing
416
417 '';
418})