nixpkgs/nixos/tests/switch-test.nix
2021-10-17 14:35:47 +02:00

417 lines
17 KiB
Nix

# Test configuration switching.
import ./make-test-python.nix ({ pkgs, ...} : {
name = "switch-test";
meta = with pkgs.lib.maintainers; {
maintainers = [ gleber ];
};
nodes = {
machine = { config, pkgs, lib, ... }: {
environment.systemPackages = [ pkgs.socat ]; # for the socket activation stuff
users.mutableUsers = false;
specialisation = {
# A system with a simple socket-activated unit
simple-socket.configuration = {
systemd.services.socket-activated.serviceConfig = {
ExecStart = pkgs.writeScript "socket-test.py" /* python */ ''
#!${pkgs.python3}/bin/python3
from socketserver import TCPServer, StreamRequestHandler
import socket
class Handler(StreamRequestHandler):
def handle(self):
self.wfile.write("hello".encode("utf-8"))
class Server(TCPServer):
def __init__(self, server_address, handler_cls):
# Invoke base but omit bind/listen steps (performed by systemd activation!)
TCPServer.__init__(
self, server_address, handler_cls, bind_and_activate=False)
# Override socket
self.socket = socket.fromfd(3, self.address_family, self.socket_type)
if __name__ == "__main__":
server = Server(("localhost", 1234), Handler)
server.serve_forever()
'';
};
systemd.sockets.socket-activated = {
wantedBy = [ "sockets.target" ];
listenStreams = [ "/run/test.sock" ];
socketConfig.SocketMode = lib.mkDefault "0777";
};
};
# The same system but the socket is modified
modified-socket.configuration = {
imports = [ config.specialisation.simple-socket.configuration ];
systemd.sockets.socket-activated.socketConfig.SocketMode = "0666";
};
# The same system but the service is modified
modified-service.configuration = {
imports = [ config.specialisation.simple-socket.configuration ];
systemd.services.socket-activated.serviceConfig.X-Test = "test";
};
# The same system but both service and socket are modified
modified-service-and-socket.configuration = {
imports = [ config.specialisation.simple-socket.configuration ];
systemd.services.socket-activated.serviceConfig.X-Test = "some_value";
systemd.sockets.socket-activated.socketConfig.SocketMode = "0444";
};
# A system with a socket-activated service and some simple services
service-and-socket.configuration = {
imports = [ config.specialisation.simple-socket.configuration ];
systemd.services.simple-service = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.coreutils}/bin/true";
};
};
systemd.services.simple-restart-service = {
stopIfChanged = false;
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.coreutils}/bin/true";
};
};
systemd.services.simple-reload-service = {
reloadIfChanged = true;
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.coreutils}/bin/true";
ExecReload = "${pkgs.coreutils}/bin/true";
};
};
systemd.services.no-restart-service = {
restartIfChanged = false;
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.coreutils}/bin/true";
};
};
};
# The same system but with an activation script that restarts all services
restart-and-reload-by-activation-script.configuration = {
imports = [ config.specialisation.service-and-socket.configuration ];
system.activationScripts.restart-and-reload-test = {
supportsDryActivation = true;
deps = [];
text = ''
if [ "$NIXOS_ACTION" = dry-activate ]; then
f=/run/nixos/dry-activation-restart-list
else
f=/run/nixos/activation-restart-list
fi
cat <<EOF >> "$f"
simple-service.service
simple-restart-service.service
simple-reload-service.service
no-restart-service.service
socket-activated.service
EOF
'';
};
};
# A system with a timer
with-timer.configuration = {
systemd.timers.test-timer = {
wantedBy = [ "timers.target" ];
timerConfig.OnCalendar = "@1395716396"; # chosen by fair dice roll
};
systemd.services.test-timer = {
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.coreutils}/bin/true";
};
};
};
# The same system but with another time
with-timer-modified.configuration = {
imports = [ config.specialisation.with-timer.configuration ];
systemd.timers.test-timer.timerConfig.OnCalendar = lib.mkForce "Fri 2012-11-23 16:00:00";
};
# A system with a systemd mount
with-mount.configuration = {
systemd.mounts = [
{
description = "Testmount";
what = "tmpfs";
type = "tmpfs";
where = "/testmount";
options = "size=1M";
wantedBy = [ "local-fs.target" ];
}
];
};
# The same system but with another time
with-mount-modified.configuration = {
systemd.mounts = [
{
description = "Testmount";
what = "tmpfs";
type = "tmpfs";
where = "/testmount";
options = "size=10M";
wantedBy = [ "local-fs.target" ];
}
];
};
# A system with a path unit
with-path.configuration = {
systemd.paths.test-watch = {
wantedBy = [ "paths.target" ];
pathConfig.PathExists = "/testpath";
};
systemd.services.test-watch = {
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.coreutils}/bin/touch /testpath-modified";
};
};
};
# The same system but watching another file
with-path-modified.configuration = {
imports = [ config.specialisation.with-path.configuration ];
systemd.paths.test-watch.pathConfig.PathExists = lib.mkForce "/testpath2";
};
# A system with a slice
with-slice.configuration = {
systemd.slices.testslice.sliceConfig.MemoryMax = "1"; # don't allow memory allocation
systemd.services.testservice = {
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.coreutils}/bin/true";
Slice = "testslice.slice";
};
};
};
# The same system but the slice allows to allocate memory
with-slice-non-crashing.configuration = {
imports = [ config.specialisation.with-slice.configuration ];
systemd.slices.testslice.sliceConfig.MemoryMax = lib.mkForce null;
};
};
};
other = { ... }: {
users.mutableUsers = true;
};
};
testScript = { nodes, ... }: let
originalSystem = nodes.machine.config.system.build.toplevel;
otherSystem = nodes.other.config.system.build.toplevel;
# Ensures failures pass through using pipefail, otherwise failing to
# switch-to-configuration is hidden by the success of `tee`.
stderrRunner = pkgs.writeScript "stderr-runner" ''
#! ${pkgs.runtimeShell}
set -e
set -o pipefail
exec env -i "$@" | tee /dev/stderr
'';
in /* python */ ''
def switch_to_specialisation(name, action="test"):
out = machine.succeed(f"${originalSystem}/specialisation/{name}/bin/switch-to-configuration {action} 2>&1")
assert_lacks(out, "switch-to-configuration line") # Perl warnings
return out
def assert_contains(haystack, needle):
if needle not in haystack:
print("The haystack that will cause the following exception is:")
print("---")
print(haystack)
print("---")
raise Exception(f"Expected string '{needle}' was not found")
def assert_lacks(haystack, needle):
if needle in haystack:
print("The haystack that will cause the following exception is:")
print("---")
print(haystack, end="")
print("---")
raise Exception(f"Unexpected string '{needle}' was found")
machine.succeed(
"${stderrRunner} ${originalSystem}/bin/switch-to-configuration test"
)
machine.succeed(
"${stderrRunner} ${otherSystem}/bin/switch-to-configuration test"
)
with subtest("systemd sockets"):
machine.succeed("${originalSystem}/bin/switch-to-configuration test")
# Simple socket is created
out = switch_to_specialisation("simple-socket")
assert_lacks(out, "stopping the following units:")
# not checking for reload because dbus gets reloaded
assert_lacks(out, "restarting the following units:")
assert_lacks(out, "\nstarting the following units:")
assert_contains(out, "the following new units were started: socket-activated.socket\n")
assert_lacks(out, "as well:")
machine.succeed("[ $(stat -c%a /run/test.sock) = 777 ]")
# Changing the socket restarts it
out = switch_to_specialisation("modified-socket")
assert_lacks(out, "stopping the following units:")
#assert_lacks(out, "reloading the following units:")
assert_contains(out, "restarting the following units: socket-activated.socket\n")
assert_lacks(out, "\nstarting the following units:")
assert_lacks(out, "the following new units were started:")
assert_lacks(out, "as well:")
machine.succeed("[ $(stat -c%a /run/test.sock) = 666 ]") # change was applied
# The unit is properly activated when the socket is accessed
if machine.succeed("socat - UNIX-CONNECT:/run/test.sock") != "hello":
raise Exception("Socket was not properly activated")
# Changing the socket restarts it and ignores the active service
out = switch_to_specialisation("simple-socket")
assert_contains(out, "stopping the following units: socket-activated.service\n")
assert_lacks(out, "reloading the following units:")
assert_contains(out, "restarting the following units: socket-activated.socket\n")
assert_lacks(out, "\nstarting the following units:")
assert_lacks(out, "the following new units were started:")
assert_lacks(out, "as well:")
machine.succeed("[ $(stat -c%a /run/test.sock) = 777 ]") # change was applied
# Changing the service does nothing when the service is not active
out = switch_to_specialisation("modified-service")
assert_lacks(out, "stopping the following units:")
assert_lacks(out, "reloading the following units:")
assert_lacks(out, "restarting the following units:")
assert_lacks(out, "\nstarting the following units:")
assert_lacks(out, "the following new units were started:")
assert_lacks(out, "as well:")
# Activating the service and modifying it stops it but leaves the socket untouched
machine.succeed("socat - UNIX-CONNECT:/run/test.sock")
out = switch_to_specialisation("simple-socket")
assert_contains(out, "stopping the following units: socket-activated.service\n")
assert_lacks(out, "reloading the following units:")
assert_lacks(out, "restarting the following units:")
assert_lacks(out, "\nstarting the following units:")
assert_lacks(out, "the following new units were started:")
assert_lacks(out, "as well:")
# Activating the service and both the service and the socket stops the service and restarts the socket
machine.succeed("socat - UNIX-CONNECT:/run/test.sock")
out = switch_to_specialisation("modified-service-and-socket")
assert_contains(out, "stopping the following units: socket-activated.service\n")
assert_lacks(out, "reloading the following units:")
assert_contains(out, "restarting the following units: socket-activated.socket\n")
assert_lacks(out, "\nstarting the following units:")
assert_lacks(out, "the following new units were started:")
assert_lacks(out, "as well:")
with subtest("restart and reload by activation file"):
out = switch_to_specialisation("service-and-socket")
# Switch to a system where the example services get restarted
# by the activation script
out = switch_to_specialisation("restart-and-reload-by-activation-script")
assert_lacks(out, "stopping the following units:")
assert_contains(out, "stopping the following units as well: simple-service.service, socket-activated.service\n")
assert_contains(out, "reloading the following units: simple-reload-service.service\n")
assert_contains(out, "restarting the following units: simple-restart-service.service\n")
assert_contains(out, "\nstarting the following units: simple-service.service")
# The same, but in dry mode
switch_to_specialisation("service-and-socket")
out = switch_to_specialisation("restart-and-reload-by-activation-script", action="dry-activate")
assert_lacks(out, "would stop the following units:")
assert_contains(out, "would stop the following units as well: simple-service.service, socket-activated.service\n")
assert_contains(out, "would reload the following units: simple-reload-service.service\n")
assert_contains(out, "would restart the following units: simple-restart-service.service\n")
assert_contains(out, "\nwould start the following units: simple-service.service")
with subtest("mounts"):
switch_to_specialisation("with-mount")
out = machine.succeed("mount | grep 'on /testmount'")
assert_contains(out, "size=1024k")
out = switch_to_specialisation("with-mount-modified")
assert_lacks(out, "stopping the following units:")
assert_contains(out, "reloading the following units: testmount.mount\n")
assert_lacks(out, "restarting the following units:")
assert_lacks(out, "\nstarting the following units:")
assert_lacks(out, "the following new units were started:")
assert_lacks(out, "as well:")
# It changed
out = machine.succeed("mount | grep 'on /testmount'")
assert_contains(out, "size=10240k")
with subtest("timers"):
switch_to_specialisation("with-timer")
out = machine.succeed("systemctl show test-timer.timer")
assert_contains(out, "OnCalendar=2014-03-25 02:59:56 UTC")
out = switch_to_specialisation("with-timer-modified")
assert_lacks(out, "stopping the following units:")
assert_lacks(out, "reloading the following units:")
assert_contains(out, "restarting the following units: test-timer.timer\n")
assert_lacks(out, "\nstarting the following units:")
assert_lacks(out, "the following new units were started:")
assert_lacks(out, "as well:")
# It changed
out = machine.succeed("systemctl show test-timer.timer")
assert_contains(out, "OnCalendar=Fri 2012-11-23 16:00:00")
with subtest("paths"):
switch_to_specialisation("with-path")
machine.fail("test -f /testpath-modified")
# touch the file, unit should be triggered
machine.succeed("touch /testpath")
machine.wait_until_succeeds("test -f /testpath-modified")
machine.succeed("rm /testpath /testpath-modified")
switch_to_specialisation("with-path-modified")
machine.succeed("touch /testpath")
machine.fail("test -f /testpath-modified")
machine.succeed("touch /testpath2")
machine.wait_until_succeeds("test -f /testpath-modified")
# This test ensures that changes to slice configuration get applied.
# We test this by having a slice that allows no memory allocation at
# all and starting a service within it. If the service crashes, the slice
# is applied and if we modify the slice to allow memory allocation, the
# service should successfully start.
with subtest("slices"):
machine.succeed("echo 0 > /proc/sys/vm/panic_on_oom") # allow OOMing
out = switch_to_specialisation("with-slice")
machine.fail("systemctl start testservice.service")
out = switch_to_specialisation("with-slice-non-crashing")
machine.succeed("systemctl start testservice.service")
machine.succeed("echo 1 > /proc/sys/vm/panic_on_oom") # disallow OOMing
'';
})