about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/lib/test-driver/test-driver.py205
-rw-r--r--nixos/lib/testing-python.nix5
-rw-r--r--nixos/modules/config/xdg/sounds.nix6
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix12
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix2
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-kde.nix9
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image.nix6
-rw-r--r--nixos/modules/misc/ids.nix4
-rw-r--r--nixos/modules/profiles/graphical.nix1
-rw-r--r--nixos/modules/services/audio/mpd.nix1
-rw-r--r--nixos/modules/services/computing/slurm/slurm.nix51
-rw-r--r--nixos/modules/services/databases/redis.nix8
-rw-r--r--nixos/modules/services/mail/opensmtpd.nix18
-rw-r--r--nixos/modules/services/misc/zoneminder.nix2
-rw-r--r--nixos/modules/services/network-filesystems/ceph.nix72
-rw-r--r--nixos/modules/services/network-filesystems/samba.nix21
-rw-r--r--nixos/modules/services/networking/dnsdist.nix5
-rw-r--r--nixos/modules/services/networking/haproxy.nix36
-rw-r--r--nixos/modules/services/networking/tinydns.nix1
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix3
-rw-r--r--nixos/modules/services/web-servers/phpfpm/default.nix5
-rw-r--r--nixos/modules/services/web-servers/varnish/default.nix6
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome3.nix33
-rw-r--r--nixos/modules/system/boot/plymouth.nix10
-rw-r--r--nixos/modules/system/boot/systemd-unit-options.nix2
-rw-r--r--nixos/modules/virtualisation/libvirtd.nix3
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/caddy.nix57
-rw-r--r--nixos/tests/cadvisor.nix23
-rw-r--r--nixos/tests/cassandra.nix134
-rw-r--r--nixos/tests/ceph-multi-node.nix58
-rw-r--r--nixos/tests/ceph-single-node.nix38
-rw-r--r--nixos/tests/certmgr.nix28
-rw-r--r--nixos/tests/cfssl.nix8
-rw-r--r--nixos/tests/cjdns.nix47
-rw-r--r--nixos/tests/cloud-init.nix13
-rw-r--r--nixos/tests/couchdb.nix56
-rw-r--r--nixos/tests/dnscrypt-proxy.nix12
-rw-r--r--nixos/tests/docker-edge.nix22
-rw-r--r--nixos/tests/docker.nix22
-rw-r--r--nixos/tests/documize.nix56
-rw-r--r--nixos/tests/haproxy.nix4
-rw-r--r--nixos/tests/lightdm.nix16
-rw-r--r--nixos/tests/mpd.nix70
-rw-r--r--nixos/tests/opensmtpd.nix26
-rw-r--r--nixos/tests/powerdns.nix7
-rw-r--r--nixos/tests/pppd.nix12
-rw-r--r--nixos/tests/prometheus-exporters.nix4
-rw-r--r--nixos/tests/samba.nix14
-rw-r--r--nixos/tests/sddm.nix28
-rw-r--r--nixos/tests/shiori.nix76
-rw-r--r--nixos/tests/slurm.nix87
-rw-r--r--nixos/tests/smokeping.nix16
-rw-r--r--nixos/tests/tinydns.nix8
-rw-r--r--nixos/tests/xmonad.nix32
55 files changed, 859 insertions, 643 deletions
diff --git a/nixos/lib/test-driver/test-driver.py b/nixos/lib/test-driver/test-driver.py
index 93b8eebd2e12..9d89960876eb 100644
--- a/nixos/lib/test-driver/test-driver.py
+++ b/nixos/lib/test-driver/test-driver.py
@@ -1,6 +1,5 @@
 #! /somewhere/python3
-
-from contextlib import contextmanager
+from contextlib import contextmanager, _GeneratorContextManager
 from xml.sax.saxutils import XMLGenerator
 import _thread
 import atexit
@@ -8,7 +7,7 @@ import json
 import os
 import ptpython.repl
 import pty
-import queue
+from queue import Queue, Empty
 import re
 import shutil
 import socket
@@ -17,6 +16,7 @@ import sys
 import tempfile
 import time
 import unicodedata
+from typing import Tuple, TextIO, Any, Callable, Dict, Iterator, Optional, List
 
 CHAR_TO_KEY = {
     "A": "shift-a",
@@ -81,12 +81,18 @@ CHAR_TO_KEY = {
     ")": "shift-0x0B",
 }
 
+# Forward references
+nr_tests: int
+nr_succeeded: int
+log: "Logger"
+machines: "List[Machine]"
+
 
-def eprint(*args, **kwargs):
+def eprint(*args: object, **kwargs: Any) -> None:
     print(*args, file=sys.stderr, **kwargs)
 
 
-def create_vlan(vlan_nr):
+def create_vlan(vlan_nr: str) -> Tuple[str, str, "subprocess.Popen[bytes]", Any]:
     global log
     log.log("starting VDE switch for network {}".format(vlan_nr))
     vde_socket = os.path.abspath("./vde{}.ctl".format(vlan_nr))
@@ -110,7 +116,7 @@ def create_vlan(vlan_nr):
     return (vlan_nr, vde_socket, vde_process, fd)
 
 
-def retry(fn):
+def retry(fn: Callable) -> None:
     """Call the given function repeatedly, with 1 second intervals,
     until it returns True or a timeout is reached.
     """
@@ -125,52 +131,52 @@ def retry(fn):
 
 
 class Logger:
-    def __init__(self):
+    def __init__(self) -> None:
         self.logfile = os.environ.get("LOGFILE", "/dev/null")
         self.logfile_handle = open(self.logfile, "wb")
         self.xml = XMLGenerator(self.logfile_handle, encoding="utf-8")
-        self.queue = queue.Queue(1000)
+        self.queue: "Queue[Dict[str, str]]" = Queue(1000)
 
         self.xml.startDocument()
         self.xml.startElement("logfile", attrs={})
 
-    def close(self):
+    def close(self) -> None:
         self.xml.endElement("logfile")
         self.xml.endDocument()
         self.logfile_handle.close()
 
-    def sanitise(self, message):
+    def sanitise(self, message: str) -> str:
         return "".join(ch for ch in message if unicodedata.category(ch)[0] != "C")
 
-    def maybe_prefix(self, message, attributes):
+    def maybe_prefix(self, message: str, attributes: Dict[str, str]) -> str:
         if "machine" in attributes:
             return "{}: {}".format(attributes["machine"], message)
         return message
 
-    def log_line(self, message, attributes):
+    def log_line(self, message: str, attributes: Dict[str, str]) -> None:
         self.xml.startElement("line", attributes)
         self.xml.characters(message)
         self.xml.endElement("line")
 
-    def log(self, message, attributes={}):
+    def log(self, message: str, attributes: Dict[str, str] = {}) -> None:
         eprint(self.maybe_prefix(message, attributes))
         self.drain_log_queue()
         self.log_line(message, attributes)
 
-    def enqueue(self, message):
+    def enqueue(self, message: Dict[str, str]) -> None:
         self.queue.put(message)
 
-    def drain_log_queue(self):
+    def drain_log_queue(self) -> None:
         try:
             while True:
                 item = self.queue.get_nowait()
                 attributes = {"machine": item["machine"], "type": "serial"}
                 self.log_line(self.sanitise(item["msg"]), attributes)
-        except queue.Empty:
+        except Empty:
             pass
 
     @contextmanager
-    def nested(self, message, attributes={}):
+    def nested(self, message: str, attributes: Dict[str, str] = {}) -> Iterator[None]:
         eprint(self.maybe_prefix(message, attributes))
 
         self.xml.startElement("nest", attrs={})
@@ -189,24 +195,22 @@ class Logger:
 
 
 class Machine:
-    def __init__(self, args):
+    def __init__(self, args: Dict[str, Any]) -> None:
         if "name" in args:
             self.name = args["name"]
         else:
             self.name = "machine"
-            try:
-                cmd = args["startCommand"]
-                self.name = re.search("run-(.+)-vm$", cmd).group(1)
-            except KeyError:
-                pass
-            except AttributeError:
-                pass
+            cmd = args.get("startCommand", None)
+            if cmd:
+                match = re.search("run-(.+)-vm$", cmd)
+                if match:
+                    self.name = match.group(1)
 
         self.script = args.get("startCommand", self.create_startcommand(args))
 
         tmp_dir = os.environ.get("TMPDIR", tempfile.gettempdir())
 
-        def create_dir(name):
+        def create_dir(name: str) -> str:
             path = os.path.join(tmp_dir, name)
             os.makedirs(path, mode=0o700, exist_ok=True)
             return path
@@ -216,14 +220,14 @@ class Machine:
 
         self.booted = False
         self.connected = False
-        self.pid = None
+        self.pid: Optional[int] = None
         self.socket = None
-        self.monitor = None
-        self.logger = args["log"]
+        self.monitor: Optional[socket.socket] = None
+        self.logger: Logger = args["log"]
         self.allow_reboot = args.get("allowReboot", False)
 
     @staticmethod
-    def create_startcommand(args):
+    def create_startcommand(args: Dict[str, str]) -> str:
         net_backend = "-netdev user,id=net0"
         net_frontend = "-device virtio-net-pci,netdev=net0"
 
@@ -273,30 +277,32 @@ class Machine:
 
         return start_command
 
-    def is_up(self):
+    def is_up(self) -> bool:
         return self.booted and self.connected
 
-    def log(self, msg):
+    def log(self, msg: str) -> None:
         self.logger.log(msg, {"machine": self.name})
 
-    def nested(self, msg, attrs={}):
+    def nested(self, msg: str, attrs: Dict[str, str] = {}) -> _GeneratorContextManager:
         my_attrs = {"machine": self.name}
         my_attrs.update(attrs)
         return self.logger.nested(msg, my_attrs)
 
-    def wait_for_monitor_prompt(self):
+    def wait_for_monitor_prompt(self) -> str:
+        assert self.monitor is not None
         while True:
             answer = self.monitor.recv(1024).decode()
             if answer.endswith("(qemu) "):
                 return answer
 
-    def send_monitor_command(self, command):
+    def send_monitor_command(self, command: str) -> str:
         message = ("{}\n".format(command)).encode()
         self.log("sending monitor command: {}".format(command))
+        assert self.monitor is not None
         self.monitor.send(message)
         return self.wait_for_monitor_prompt()
 
-    def wait_for_unit(self, unit, user=None):
+    def wait_for_unit(self, unit: str, user: Optional[str] = None) -> bool:
         while True:
             info = self.get_unit_info(unit, user)
             state = info["ActiveState"]
@@ -306,7 +312,7 @@ class Machine:
             if state == "inactive":
                 status, jobs = self.systemctl("list-jobs --full 2>&1", user)
                 if "No jobs" in jobs:
-                    info = self.get_unit_info(unit)
+                    info = self.get_unit_info(unit, user)
                     if info["ActiveState"] == state:
                         raise Exception(
                             (
@@ -316,15 +322,20 @@ class Machine:
             if state == "active":
                 return True
 
-    def get_unit_info(self, unit, user=None):
+    def get_unit_info(self, unit: str, user: Optional[str] = None) -> Dict[str, str]:
         status, lines = self.systemctl('--no-pager show "{}"'.format(unit), user)
         if status != 0:
-            return None
+            raise Exception(
+                'retrieving systemctl info for unit "{}" {} failed with exit code {}'.format(
+                    unit, "" if user is None else 'under user "{}"'.format(user), status
+                )
+            )
 
         line_pattern = re.compile(r"^([^=]+)=(.*)$")
 
-        def tuple_from_line(line):
+        def tuple_from_line(line: str) -> Tuple[str, str]:
             match = line_pattern.match(line)
+            assert match is not None
             return match[1], match[2]
 
         return dict(
@@ -333,7 +344,7 @@ class Machine:
             if line_pattern.match(line)
         )
 
-    def systemctl(self, q, user=None):
+    def systemctl(self, q: str, user: Optional[str] = None) -> Tuple[int, str]:
         if user is not None:
             q = q.replace("'", "\\'")
             return self.execute(
@@ -345,7 +356,7 @@ class Machine:
             )
         return self.execute("systemctl {}".format(q))
 
-    def require_unit_state(self, unit, require_state="active"):
+    def require_unit_state(self, unit: str, require_state: str = "active") -> None:
         with self.nested(
             "checking if unit ‘{}’ has reached state '{}'".format(unit, require_state)
         ):
@@ -357,7 +368,7 @@ class Machine:
                     + "'active' but it is in state ‘{}’".format(state)
                 )
 
-    def execute(self, command):
+    def execute(self, command: str) -> Tuple[int, str]:
         self.connect()
 
         out_command = "( {} ); echo '|!EOF' $?\n".format(command)
@@ -375,19 +386,21 @@ class Machine:
                 return (status_code, output)
             output += chunk
 
-    def succeed(self, *commands):
+    def succeed(self, *commands: str) -> str:
         """Execute each command and check that it succeeds."""
+        output = ""
         for command in commands:
             with self.nested("must succeed: {}".format(command)):
-                status, output = self.execute(command)
+                (status, out) = self.execute(command)
                 if status != 0:
-                    self.log("output: {}".format(output))
+                    self.log("output: {}".format(out))
                     raise Exception(
                         "command `{}` failed (exit code {})".format(command, status)
                     )
-                return output
+                output += out
+        return output
 
-    def fail(self, *commands):
+    def fail(self, *commands: str) -> None:
         """Execute each command and check that it fails."""
         for command in commands:
             with self.nested("must fail: {}".format(command)):
@@ -397,21 +410,21 @@ class Machine:
                         "command `{}` unexpectedly succeeded".format(command)
                     )
 
-    def wait_until_succeeds(self, command):
+    def wait_until_succeeds(self, command: str) -> str:
         with self.nested("waiting for success: {}".format(command)):
             while True:
                 status, output = self.execute(command)
                 if status == 0:
                     return output
 
-    def wait_until_fails(self, command):
+    def wait_until_fails(self, command: str) -> str:
         with self.nested("waiting for failure: {}".format(command)):
             while True:
                 status, output = self.execute(command)
                 if status != 0:
                     return output
 
-    def wait_for_shutdown(self):
+    def wait_for_shutdown(self) -> None:
         if not self.booted:
             return
 
@@ -423,14 +436,14 @@ class Machine:
             self.booted = False
             self.connected = False
 
-    def get_tty_text(self, tty):
+    def get_tty_text(self, tty: str) -> str:
         status, output = self.execute(
             "fold -w$(stty -F /dev/tty{0} size | "
             "awk '{{print $2}}') /dev/vcs{0}".format(tty)
         )
         return output
 
-    def wait_until_tty_matches(self, tty, regexp):
+    def wait_until_tty_matches(self, tty: str, regexp: str) -> bool:
         matcher = re.compile(regexp)
         with self.nested("waiting for {} to appear on tty {}".format(regexp, tty)):
             while True:
@@ -438,43 +451,43 @@ class Machine:
                 if len(matcher.findall(text)) > 0:
                     return True
 
-    def send_chars(self, chars):
+    def send_chars(self, chars: List[str]) -> None:
         with self.nested("sending keys ‘{}‘".format(chars)):
             for char in chars:
                 self.send_key(char)
 
-    def wait_for_file(self, filename):
+    def wait_for_file(self, filename: str) -> bool:
         with self.nested("waiting for file ‘{}‘".format(filename)):
             while True:
                 status, _ = self.execute("test -e {}".format(filename))
                 if status == 0:
                     return True
 
-    def wait_for_open_port(self, port):
-        def port_is_open(_):
+    def wait_for_open_port(self, port: int) -> None:
+        def port_is_open(_: Any) -> bool:
             status, _ = self.execute("nc -z localhost {}".format(port))
             return status == 0
 
         with self.nested("waiting for TCP port {}".format(port)):
             retry(port_is_open)
 
-    def wait_for_closed_port(self, port):
-        def port_is_closed(_):
+    def wait_for_closed_port(self, port: int) -> None:
+        def port_is_closed(_: Any) -> bool:
             status, _ = self.execute("nc -z localhost {}".format(port))
             return status != 0
 
         retry(port_is_closed)
 
-    def start_job(self, jobname, user=None):
+    def start_job(self, jobname: str, user: Optional[str] = None) -> Tuple[int, str]:
         return self.systemctl("start {}".format(jobname), user)
 
-    def stop_job(self, jobname, user=None):
+    def stop_job(self, jobname: str, user: Optional[str] = None) -> Tuple[int, str]:
         return self.systemctl("stop {}".format(jobname), user)
 
-    def wait_for_job(self, jobname):
+    def wait_for_job(self, jobname: str) -> bool:
         return self.wait_for_unit(jobname)
 
-    def connect(self):
+    def connect(self) -> None:
         if self.connected:
             return
 
@@ -490,7 +503,7 @@ class Machine:
             self.log("(connecting took {:.2f} seconds)".format(toc - tic))
             self.connected = True
 
-    def screenshot(self, filename):
+    def screenshot(self, filename: str) -> None:
         out_dir = os.environ.get("out", os.getcwd())
         word_pattern = re.compile(r"^\w+$")
         if word_pattern.match(filename):
@@ -507,12 +520,12 @@ class Machine:
             if ret.returncode != 0:
                 raise Exception("Cannot convert screenshot")
 
-    def dump_tty_contents(self, tty):
+    def dump_tty_contents(self, tty: str) -> None:
         """Debugging: Dump the contents of the TTY<n>
         """
         self.execute("fold -w 80 /dev/vcs{} | systemd-cat".format(tty))
 
-    def get_screen_text(self):
+    def get_screen_text(self) -> str:
         if shutil.which("tesseract") is None:
             raise Exception("get_screen_text used but enableOCR is false")
 
@@ -540,30 +553,30 @@ class Machine:
 
                 return ret.stdout.decode("utf-8")
 
-    def wait_for_text(self, regex):
-        def screen_matches(last):
+    def wait_for_text(self, regex: str) -> None:
+        def screen_matches(last: bool) -> bool:
             text = self.get_screen_text()
-            m = re.search(regex, text)
+            matches = re.search(regex, text) is not None
 
-            if last and not m:
+            if last and not matches:
                 self.log("Last OCR attempt failed. Text was: {}".format(text))
 
-            return m
+            return matches
 
         with self.nested("waiting for {} to appear on screen".format(regex)):
             retry(screen_matches)
 
-    def send_key(self, key):
+    def send_key(self, key: str) -> None:
         key = CHAR_TO_KEY.get(key, key)
         self.send_monitor_command("sendkey {}".format(key))
 
-    def start(self):
+    def start(self) -> None:
         if self.booted:
             return
 
         self.log("starting vm")
 
-        def create_socket(path):
+        def create_socket(path: str) -> socket.socket:
             if os.path.exists(path):
                 os.unlink(path)
             s = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM)
@@ -613,9 +626,9 @@ class Machine:
         self.monitor, _ = self.monitor_socket.accept()
         self.shell, _ = self.shell_socket.accept()
 
-        def process_serial_output():
-            for line in self.process.stdout:
-                line = line.decode("unicode_escape").replace("\r", "").rstrip()
+        def process_serial_output() -> None:
+            for _line in self.process.stdout:
+                line = _line.decode("unicode_escape").replace("\r", "").rstrip()
                 eprint("{} # {}".format(self.name, line))
                 self.logger.enqueue({"msg": line, "machine": self.name})
 
@@ -628,14 +641,14 @@ class Machine:
 
         self.log("QEMU running (pid {})".format(self.pid))
 
-    def shutdown(self):
+    def shutdown(self) -> None:
         if not self.booted:
             return
 
         self.shell.send("poweroff\n".encode())
         self.wait_for_shutdown()
 
-    def crash(self):
+    def crash(self) -> None:
         if not self.booted:
             return
 
@@ -643,7 +656,7 @@ class Machine:
         self.send_monitor_command("quit")
         self.wait_for_shutdown()
 
-    def wait_for_x(self):
+    def wait_for_x(self) -> None:
         """Wait until it is possible to connect to the X server.  Note that
         testing the existence of /tmp/.X11-unix/X0 is insufficient.
         """
@@ -660,15 +673,15 @@ class Machine:
                 if status == 0:
                     return
 
-    def get_window_names(self):
+    def get_window_names(self) -> List[str]:
         return self.succeed(
             r"xwininfo -root -tree | sed 's/.*0x[0-9a-f]* \"\([^\"]*\)\".*/\1/; t; d'"
         ).splitlines()
 
-    def wait_for_window(self, regexp):
+    def wait_for_window(self, regexp: str) -> None:
         pattern = re.compile(regexp)
 
-        def window_is_visible(last_try):
+        def window_is_visible(last_try: bool) -> bool:
             names = self.get_window_names()
             if last_try:
                 self.log(
@@ -681,10 +694,10 @@ class Machine:
         with self.nested("Waiting for a window to appear"):
             retry(window_is_visible)
 
-    def sleep(self, secs):
+    def sleep(self, secs: int) -> None:
         time.sleep(secs)
 
-    def forward_port(self, host_port=8080, guest_port=80):
+    def forward_port(self, host_port: int = 8080, guest_port: int = 80) -> None:
         """Forward a TCP port on the host to a TCP port on the guest.
         Useful during interactive testing.
         """
@@ -692,43 +705,46 @@ class Machine:
             "hostfwd_add tcp::{}-:{}".format(host_port, guest_port)
         )
 
-    def block(self):
+    def block(self) -> None:
         """Make the machine unreachable by shutting down eth1 (the multicast
         interface used to talk to the other VMs).  We keep eth0 up so that
         the test driver can continue to talk to the machine.
         """
         self.send_monitor_command("set_link virtio-net-pci.1 off")
 
-    def unblock(self):
+    def unblock(self) -> None:
         """Make the machine reachable.
         """
         self.send_monitor_command("set_link virtio-net-pci.1 on")
 
 
-def create_machine(args):
+def create_machine(args: Dict[str, Any]) -> Machine:
     global log
     args["log"] = log
     args["redirectSerial"] = os.environ.get("USE_SERIAL", "0") == "1"
     return Machine(args)
 
 
-def start_all():
+def start_all() -> None:
+    global machines
     with log.nested("starting all VMs"):
         for machine in machines:
             machine.start()
 
 
-def join_all():
+def join_all() -> None:
+    global machines
     with log.nested("waiting for all VMs to finish"):
         for machine in machines:
             machine.wait_for_shutdown()
 
 
-def test_script():
+def test_script() -> None:
     exec(os.environ["testScript"])
 
 
-def run_tests():
+def run_tests() -> None:
+    global machines
     tests = os.environ.get("tests", None)
     if tests is not None:
         with log.nested("running the VM test script"):
@@ -751,7 +767,7 @@ def run_tests():
 
 
 @contextmanager
-def subtest(name):
+def subtest(name: str) -> Iterator[None]:
     global nr_tests
     global nr_succeeded
 
@@ -768,7 +784,6 @@ def subtest(name):
 
 
 if __name__ == "__main__":
-    global log
     log = Logger()
 
     vlan_nrs = list(dict.fromkeys(os.environ["VLANS"].split()))
@@ -787,7 +802,7 @@ if __name__ == "__main__":
     nr_succeeded = 0
 
     @atexit.register
-    def clean_up():
+    def clean_up() -> None:
         with log.nested("cleaning up"):
             for machine in machines:
                 if machine.pid is None:
diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix
index 21f6172e9671..4d990cf551f3 100644
--- a/nixos/lib/testing-python.nix
+++ b/nixos/lib/testing-python.nix
@@ -26,7 +26,7 @@ in rec {
 
     nativeBuildInputs = [ makeWrapper ];
     buildInputs = [ (python3.withPackages (p: [ p.ptpython ])) ];
-    checkInputs = with python3Packages; [ pylint black ];
+    checkInputs = with python3Packages; [ pylint black mypy ];
 
     dontUnpack = true;
 
@@ -34,6 +34,9 @@ in rec {
 
     doCheck = true;
     checkPhase = ''
+      mypy --disallow-untyped-defs \
+           --no-implicit-optional \
+           --ignore-missing-imports ${testDriverScript}
       pylint --errors-only ${testDriverScript}
       black --check --diff ${testDriverScript}
     '';
diff --git a/nixos/modules/config/xdg/sounds.nix b/nixos/modules/config/xdg/sounds.nix
index 148240d631cf..14d6340fc33b 100644
--- a/nixos/modules/config/xdg/sounds.nix
+++ b/nixos/modules/config/xdg/sounds.nix
@@ -1,4 +1,4 @@
-{ config, lib, ... }:
+{ config, lib, pkgs, ... }:
 
 with lib;
 {
@@ -14,6 +14,10 @@ with lib;
   };
 
   config = mkIf config.xdg.sounds.enable {
+    environment.systemPackages = [
+      pkgs.sound-theme-freedesktop
+    ];
+
     environment.pathsToLink = [
       "/share/sounds"
     ];
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix
index 719ba5ffb127..e0b558dcb0d8 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-base.nix
@@ -22,17 +22,7 @@ with lib;
     });
   '';
 
-  services.xserver = {
-    enable = true;
-
-    # Automatically login as nixos.
-    displayManager.slim = {
-      enable = true;
-      defaultUser = "nixos";
-      autoLogin = true;
-    };
-
-  };
+  services.xserver.enable = true;
 
   # Provide networkmanager for easy wireless configuration.
   networking.networkmanager.enable = true;
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
index 0b813bbf37b4..23c3426bff08 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix
@@ -10,8 +10,6 @@ with lib;
 
   services.xserver.desktopManager.gnome3.enable = true;
 
-  services.xserver.displayManager.slim.enable = mkForce false;
-
   # Auto-login as root.
   services.xserver.displayManager.gdm.autoLogin = {
     enable = true;
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-kde.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-kde.nix
index 1dc7920ff640..6a10a6404e67 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-kde.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-kde.nix
@@ -13,6 +13,15 @@ with lib;
       enable = true;
       enableQt4Support = false;
     };
+
+    # Automatically login as nixos.
+    displayManager.sddm = {
+      enable = true;
+      autoLogin = {
+        enable = true;
+        user = "nixos";
+      };
+    };
   };
 
   environment.systemPackages = with pkgs; [
diff --git a/nixos/modules/installer/cd-dvd/sd-image.nix b/nixos/modules/installer/cd-dvd/sd-image.nix
index d510f3b2daf2..7865b767f0b7 100644
--- a/nixos/modules/installer/cd-dvd/sd-image.nix
+++ b/nixos/modules/installer/cd-dvd/sd-image.nix
@@ -140,7 +140,11 @@ in
         export img=$out/sd-image/${config.sdImage.imageName}
 
         echo "${pkgs.stdenv.buildPlatform.system}" > $out/nix-support/system
-        echo "file sd-image $img" >> $out/nix-support/hydra-build-products
+        if test -n "$compressImage"; then
+          echo "file sd-image $img.bz2" >> $out/nix-support/hydra-build-products
+        else
+          echo "file sd-image $img" >> $out/nix-support/hydra-build-products
+        fi
 
         # Gap in front of the first partition, in MiB
         gap=8
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index 3e8a5b07a5ed..a4db2c9d1d87 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -128,7 +128,7 @@
       tcpcryptd = 93; # tcpcryptd uses a hard-coded uid. We patch it in Nixpkgs to match this choice.
       firebird = 95;
       #keys = 96; # unused
-      haproxy = 97;
+      #haproxy = 97; # DynamicUser as of 2019-11-08
       mongodb = 98;
       openldap = 99;
       #users = 100; # unused
@@ -443,7 +443,7 @@
       #tcpcryptd = 93; # unused
       firebird = 95;
       keys = 96;
-      haproxy = 97;
+      #haproxy = 97; # DynamicUser as of 2019-11-08
       #mongodb = 98; # unused
       openldap = 99;
       munin = 102;
diff --git a/nixos/modules/profiles/graphical.nix b/nixos/modules/profiles/graphical.nix
index 649f5564ac61..ac3c228b3c65 100644
--- a/nixos/modules/profiles/graphical.nix
+++ b/nixos/modules/profiles/graphical.nix
@@ -16,7 +16,6 @@
 
   # Enable sound in virtualbox appliances.
   hardware.pulseaudio.enable = true;
-  hardware.pulseaudio.systemWide = true; # Needed since we run plasma as root.
 
   environment.systemPackages = [ pkgs.glxinfo pkgs.firefox ];
 }
diff --git a/nixos/modules/services/audio/mpd.nix b/nixos/modules/services/audio/mpd.nix
index 56dc858b6405..7932d094197b 100644
--- a/nixos/modules/services/audio/mpd.nix
+++ b/nixos/modules/services/audio/mpd.nix
@@ -18,7 +18,6 @@ let
     ''}
     state_file          "${cfg.dataDir}/state"
     sticker_file        "${cfg.dataDir}/sticker.sql"
-    log_file            "syslog"
     user                "${cfg.user}"
     group               "${cfg.group}"
 
diff --git a/nixos/modules/services/computing/slurm/slurm.nix b/nixos/modules/services/computing/slurm/slurm.nix
index d1a1383e45b0..c70d999ca96d 100644
--- a/nixos/modules/services/computing/slurm/slurm.nix
+++ b/nixos/modules/services/computing/slurm/slurm.nix
@@ -18,7 +18,7 @@ let
       ${optionalString (cfg.controlAddr != null) ''controlAddr=${cfg.controlAddr}''}
       ${toString (map (x: "NodeName=${x}\n") cfg.nodeName)}
       ${toString (map (x: "PartitionName=${x}\n") cfg.partitionName)}
-      PlugStackConfig=${plugStackConfig}
+      PlugStackConfig=${plugStackConfig}/plugstack.conf
       ProctrackType=${cfg.procTrackType}
       ${cfg.extraConfig}
     '';
@@ -39,6 +39,8 @@ let
      DbdHost=${cfg.dbdserver.dbdHost}
      SlurmUser=${cfg.user}
      StorageType=accounting_storage/mysql
+     StorageUser=${cfg.dbdserver.storageUser}
+     ${optionalString (cfg.dbdserver.storagePass != null) "StoragePass=${cfg.dbdserver.storagePass}"}
      ${cfg.dbdserver.extraConfig}
    '';
 
@@ -48,7 +50,6 @@ let
     name = "etc-slurm";
     paths = [ configFile cgroupConfig plugStackConfig ] ++ cfg.extraConfigPaths;
   };
-
 in
 
 {
@@ -86,6 +87,37 @@ in
           '';
         };
 
+        storageUser = mkOption {
+          type = types.str;
+          default = cfg.user;
+          description = ''
+            Database user name.
+          '';
+        };
+
+        storagePass = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            Database password. Note that this password will be publicable
+            readable in the nix store. Use <option>configFile</option>
+            to store the and config file and password outside the nix store.
+          '';
+        };
+
+        configFile = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            Path to <literal>slurmdbd.conf</literal>. The password for the database connection
+            is stored in the config file. Use this option to specfify a path
+            outside the nix store. If this option is unset a configuration file
+            will be generated. See also:
+            <citerefentry><refentrytitle>slurmdbd.conf</refentrytitle>
+            <manvolnum>8</manvolnum></citerefentry>.
+          '';
+        };
+
         extraConfig = mkOption {
           type = types.lines;
           default = "";
@@ -112,7 +144,7 @@ in
 
       package = mkOption {
         type = types.package;
-        default = pkgs.slurm;
+        default = pkgs.slurm.override { enableX11 = ! cfg.enableSrunX11; };
         defaultText = "pkgs.slurm";
         example = literalExample "pkgs.slurm-full";
         description = ''
@@ -178,9 +210,14 @@ in
           If enabled srun will accept the option "--x11" to allow for X11 forwarding
           from within an interactive session or a batch job. This activates the
           slurm-spank-x11 module. Note that this option also enables
-          'services.openssh.forwardX11' on the client.
+          <option>services.openssh.forwardX11</option> on the client.
 
           This option requires slurm to be compiled without native X11 support.
+          The default behavior is to re-compile the slurm package with native X11
+          support disabled if this option is set to true.
+
+          To use the native X11 support add <literal>PrologFlags=X11</literal> in <option>extraConfig</option>.
+          Note that this method will only work RSA SSH host keys.
         '';
       };
 
@@ -356,7 +393,11 @@ in
       requires = [ "munged.service" "mysql.service" ];
 
       # slurm strips the last component off the path
-      environment.SLURM_CONF = "${slurmdbdConf}/slurm.conf";
+      environment.SLURM_CONF =
+        if (cfg.dbdserver.configFile == null) then
+          "${slurmdbdConf}/slurm.conf"
+        else
+          cfg.dbdserver.configFile;
 
       serviceConfig = {
         Type = "forking";
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index 5695eeaf74cb..95128a641d94 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -185,10 +185,10 @@ in
   ###### implementation
 
   config = mkIf config.services.redis.enable {
-
-    boot.kernel.sysctl = {
-      "vm.nr_hugepages" = "0";
-    } // mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; };
+    boot.kernel.sysctl = (mkMerge [
+      { "vm.nr_hugepages" = "0"; }
+      ( mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; } )
+    ]);
 
     networking.firewall = mkIf cfg.openFirewall {
       allowedTCPPorts = [ cfg.port ];
diff --git a/nixos/modules/services/mail/opensmtpd.nix b/nixos/modules/services/mail/opensmtpd.nix
index a870550ba50b..1fabe2da45c5 100644
--- a/nixos/modules/services/mail/opensmtpd.nix
+++ b/nixos/modules/services/mail/opensmtpd.nix
@@ -101,6 +101,12 @@ in {
       };
     };
 
+    systemd.tmpfiles.rules = [
+      "d /var/spool/smtpd 711 root - - -"
+      "d /var/spool/smtpd/offline 770 root smtpq - -"
+      "d /var/spool/smtpd/purge 700 smtpq root - -"
+    ];
+
     systemd.services.opensmtpd = let
       procEnv = pkgs.buildEnv {
         name = "opensmtpd-procs";
@@ -110,18 +116,6 @@ in {
     in {
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
-      preStart = ''
-        mkdir -p /var/spool/smtpd
-        chmod 711 /var/spool/smtpd
-
-        mkdir -p /var/spool/smtpd/offline
-        chown root.smtpq /var/spool/smtpd/offline
-        chmod 770 /var/spool/smtpd/offline
-
-        mkdir -p /var/spool/smtpd/purge
-        chown smtpq.root /var/spool/smtpd/purge
-        chmod 700 /var/spool/smtpd/purge
-      '';
       serviceConfig.ExecStart = "${cfg.package}/sbin/smtpd -d -f ${conf} ${args}";
       environment.OPENSMTPD_PROC_PATH = "${procEnv}/libexec/opensmtpd";
     };
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
index 3bff04e7127d..d7f7324580c0 100644
--- a/nixos/modules/services/misc/zoneminder.nix
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -265,7 +265,7 @@ in {
                 }
 
                 location /cache/ {
-                  alias /var/cache/${dirName};
+                  alias /var/cache/${dirName}/;
                 }
 
                 location ~ \.php$ {
diff --git a/nixos/modules/services/network-filesystems/ceph.nix b/nixos/modules/services/network-filesystems/ceph.nix
index 656a2d21b868..543a7b25d5d6 100644
--- a/nixos/modules/services/network-filesystems/ceph.nix
+++ b/nixos/modules/services/network-filesystems/ceph.nix
@@ -9,12 +9,14 @@ let
   expandCamelCase = replaceStrings upperChars (map (s: " ${s}") lowerChars);
   expandCamelCaseAttrs = mapAttrs' (name: value: nameValuePair (expandCamelCase name) value);
 
-  makeServices = (daemonType: daemonIds: extraServiceConfig:
+  makeServices = (daemonType: daemonIds:
     mkMerge (map (daemonId:
-      { "ceph-${daemonType}-${daemonId}" = makeService daemonType daemonId cfg.global.clusterName pkgs.ceph extraServiceConfig; })
+      { "ceph-${daemonType}-${daemonId}" = makeService daemonType daemonId cfg.global.clusterName pkgs.ceph; })
       daemonIds));
 
-  makeService = (daemonType: daemonId: clusterName: ceph: extraServiceConfig: {
+  makeService = (daemonType: daemonId: clusterName: ceph:
+    let
+      stateDirectory = "ceph/${if daemonType == "rgw" then "radosgw" else daemonType}/${clusterName}-${daemonId}"; in {
     enable = true;
     description = "Ceph ${builtins.replaceStrings lowerChars upperChars daemonType} daemon ${daemonId}";
     after = [ "network-online.target" "time-sync.target" ] ++ optional (daemonType == "osd") "ceph-mon.target";
@@ -22,6 +24,11 @@ let
     partOf = [ "ceph-${daemonType}.target" ];
     wantedBy = [ "ceph-${daemonType}.target" ];
 
+    path = [ pkgs.getopt ];
+
+    # Don't start services that are not yet initialized
+    unitConfig.ConditionPathExists = "/var/lib/${stateDirectory}/keyring";
+
     serviceConfig = {
       LimitNOFILE = 1048576;
       LimitNPROC = 1048576;
@@ -34,22 +41,22 @@ let
       Restart = "on-failure";
       StartLimitBurst = "5";
       StartLimitInterval = "30min";
+      StateDirectory = stateDirectory;
+      User = "ceph";
+      Group = if daemonType == "osd" then "disk" else "ceph";
       ExecStart = ''${ceph.out}/bin/${if daemonType == "rgw" then "radosgw" else "ceph-${daemonType}"} \
-                    -f --cluster ${clusterName} --id ${daemonId} --setuser ceph \
-                    --setgroup ${if daemonType == "osd" then "disk" else "ceph"}'';
-    } // extraServiceConfig
-      // optionalAttrs (daemonType == "osd") { ExecStartPre = ''${ceph.lib}/libexec/ceph/ceph-osd-prestart.sh \
-                                                              --id ${daemonId} --cluster ${clusterName}''; };
-    } // optionalAttrs (builtins.elem daemonType [ "mds" "mon" "rgw" "mgr" ]) {
-      preStart = ''
-        daemonPath="/var/lib/ceph/${if daemonType == "rgw" then "radosgw" else daemonType}/${clusterName}-${daemonId}"
-        if [ ! -d $daemonPath ]; then
-          mkdir -m 755 -p $daemonPath
-          chown -R ceph:ceph $daemonPath
-        fi
-      '';
-    } // optionalAttrs (daemonType == "osd") { path = [ pkgs.getopt ]; }
-  );
+                    -f --cluster ${clusterName} --id ${daemonId}'';
+    } // optionalAttrs (daemonType == "osd") {
+      ExecStartPre = ''${ceph.lib}/libexec/ceph/ceph-osd-prestart.sh --id ${daemonId} --cluster ${clusterName}'';
+      StartLimitBurst = "30";
+      RestartSec = "20s";
+      PrivateDevices = "no"; # osd needs disk access
+    } // optionalAttrs ( daemonType == "mon") {
+      RestartSec = "10";
+    } // optionalAttrs (lib.elem daemonType ["mgr" "mds"]) {
+      StartLimitBurst = "3";
+    };
+  });
 
   makeTarget = (daemonType:
     {
@@ -58,6 +65,7 @@ let
         partOf = [ "ceph.target" ];
         wantedBy = [ "ceph.target" ];
         before = [ "ceph.target" ];
+        unitConfig.StopWhenUnneeded = true;
       };
     }
   );
@@ -377,22 +385,22 @@ in
 
     systemd.services = let
       services = []
-        ++ optional cfg.mon.enable (makeServices "mon" cfg.mon.daemons { RestartSec = "10"; })
-        ++ optional cfg.mds.enable (makeServices "mds" cfg.mds.daemons { StartLimitBurst = "3"; })
-        ++ optional cfg.osd.enable (makeServices "osd" cfg.osd.daemons { StartLimitBurst = "30";
-                                                                         RestartSec = "20s";
-                                                                         PrivateDevices = "no"; # osd needs disk access
-                                                                       })
-        ++ optional cfg.rgw.enable (makeServices "rgw" cfg.rgw.daemons { })
-        ++ optional cfg.mgr.enable (makeServices "mgr" cfg.mgr.daemons { StartLimitBurst = "3"; });
+        ++ optional cfg.mon.enable (makeServices "mon" cfg.mon.daemons)
+        ++ optional cfg.mds.enable (makeServices "mds" cfg.mds.daemons)
+        ++ optional cfg.osd.enable (makeServices "osd" cfg.osd.daemons)
+        ++ optional cfg.rgw.enable (makeServices "rgw" cfg.rgw.daemons)
+        ++ optional cfg.mgr.enable (makeServices "mgr" cfg.mgr.daemons);
       in
         mkMerge services;
 
     systemd.targets = let
       targets = [
-        { ceph = { description = "Ceph target allowing to start/stop all ceph service instances at once";
-                     wantedBy = [ "multi-user.target" ]; }; }
-      ] ++ optional cfg.mon.enable (makeTarget "mon")
+        { ceph = {
+          description = "Ceph target allowing to start/stop all ceph service instances at once";
+          wantedBy = [ "multi-user.target" ];
+          unitConfig.StopWhenUnneeded = true;
+        }; } ]
+        ++ optional cfg.mon.enable (makeTarget "mon")
         ++ optional cfg.mds.enable (makeTarget "mds")
         ++ optional cfg.osd.enable (makeTarget "osd")
         ++ optional cfg.rgw.enable (makeTarget "rgw")
@@ -401,7 +409,11 @@ in
         mkMerge targets;
 
     systemd.tmpfiles.rules = [
+      "d /etc/ceph - ceph ceph - -"
       "d /run/ceph 0770 ceph ceph -"
-    ];
+      "d /var/lib/ceph - ceph ceph - -"]
+    ++ optionals cfg.mgr.enable [ "d /var/lib/ceph/mgr - ceph ceph - -"]
+    ++ optionals cfg.mon.enable [ "d /var/lib/ceph/mon - ceph ceph - -"]
+    ++ optionals cfg.osd.enable [ "d /var/lib/ceph/osd - ceph ceph - -"];
   };
 }
diff --git a/nixos/modules/services/network-filesystems/samba.nix b/nixos/modules/services/network-filesystems/samba.nix
index ce565dbaab81..0315c1c71ee3 100644
--- a/nixos/modules/services/network-filesystems/samba.nix
+++ b/nixos/modules/services/network-filesystems/samba.nix
@@ -12,11 +12,6 @@ let
 
   samba = cfg.package;
 
-  setupScript =
-    ''
-      mkdir -p /var/lock/samba /var/log/samba /var/cache/samba /var/lib/samba/private
-    '';
-
   shareConfig = name:
     let share = getAttr name cfg.shares; in
     "[${name}]\n " + (smbToString (
@@ -45,7 +40,6 @@ let
   daemonService = appName: args:
     { description = "Samba Service Daemon ${appName}";
 
-      after = [ "network.target" ];
       requiredBy = [ "samba.target" ];
       partOf = [ "samba.target" ];
 
@@ -62,6 +56,7 @@ let
         Type = "notify";
         NotifyAccess = "all"; #may not do anything...
       };
+      unitConfig.RequiresMountsFor = "/var/lib/samba";
 
       restartTriggers = [ configFile ];
     };
@@ -228,8 +223,7 @@ in
         systemd = {
           targets.samba = {
             description = "Samba Server";
-            requires = [ "samba-setup.service" ];
-            after = [ "samba-setup.service" "network.target" ];
+            after = [ "network.target" ];
             wantedBy = [ "multi-user.target" ];
           };
           # Refer to https://github.com/samba-team/samba/tree/master/packaging/systemd
@@ -238,12 +232,13 @@ in
             samba-smbd = daemonService "smbd" "";
             samba-nmbd = mkIf cfg.enableNmbd (daemonService "nmbd" "");
             samba-winbindd = mkIf cfg.enableWinbindd (daemonService "winbindd" "");
-            samba-setup = {
-              description = "Samba Setup Task";
-              script = setupScript;
-              unitConfig.RequiresMountsFor = "/var/lib/samba";
-            };
           };
+          tmpfiles.rules = [
+            "d /var/lock/samba - - - - -"
+            "d /var/log/samba - - - - -"
+            "d /var/cache/samba - - - - -"
+            "d /var/lib/samba/private - - - - -"
+          ];
         };
 
         security.pam.services.samba = {};
diff --git a/nixos/modules/services/networking/dnsdist.nix b/nixos/modules/services/networking/dnsdist.nix
index 12eee136e639..8249da69bc1a 100644
--- a/nixos/modules/services/networking/dnsdist.nix
+++ b/nixos/modules/services/networking/dnsdist.nix
@@ -46,11 +46,10 @@ in {
         RestartSec="1";
         DynamicUser = true;
         StartLimitInterval="0";
-        PrivateTmp=true;
         PrivateDevices=true;
-        CapabilityBoundingSet="CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID";
+        AmbientCapabilities="CAP_NET_BIND_SERVICE";
+        CapabilityBoundingSet="CAP_NET_BIND_SERVICE";
         ExecStart = "${pkgs.dnsdist}/bin/dnsdist --supervised --disable-syslog --config ${configFile}";
-        ProtectSystem="full";
         ProtectHome=true;
         RestrictAddressFamilies="AF_UNIX AF_INET AF_INET6";
         LimitNOFILE="16384";
diff --git a/nixos/modules/services/networking/haproxy.nix b/nixos/modules/services/networking/haproxy.nix
index 0438d0bf8d86..aff71e5e97da 100644
--- a/nixos/modules/services/networking/haproxy.nix
+++ b/nixos/modules/services/networking/haproxy.nix
@@ -1,7 +1,16 @@
 { config, lib, pkgs, ... }:
+
 let
   cfg = config.services.haproxy;
-  haproxyCfg = pkgs.writeText "haproxy.conf" cfg.config;
+
+  haproxyCfg = pkgs.writeText "haproxy.conf" ''
+    global
+      # needed for hot-reload to work without dropping packets in multi-worker mode
+      stats socket /run/haproxy/haproxy.sock mode 600 expose-fd listeners level user
+
+    ${cfg.config}
+  '';
+
 in
 with lib;
 {
@@ -25,9 +34,7 @@ with lib;
           <filename>haproxy.conf</filename>.
         '';
       };
-
     };
-
   };
 
   config = mkIf cfg.enable {
@@ -42,21 +49,16 @@ with lib;
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
-        Type = "forking";
-        PIDFile = "/run/haproxy.pid";
-        ExecStartPre = "${pkgs.haproxy}/sbin/haproxy -c -q -f ${haproxyCfg}";
-        ExecStart = "${pkgs.haproxy}/sbin/haproxy -D -f ${haproxyCfg} -p /run/haproxy.pid";
-        ExecReload = "-${pkgs.bash}/bin/bash -c \"exec ${pkgs.haproxy}/sbin/haproxy -D -f ${haproxyCfg} -p /run/haproxy.pid -sf $MAINPID\"";
+        DynamicUser = true;
+        Type = "notify";
+        # when running the config test, don't be quiet so we can see what goes wrong
+        ExecStartPre = "${pkgs.haproxy}/sbin/haproxy -c -f ${haproxyCfg}";
+        ExecStart = "${pkgs.haproxy}/sbin/haproxy -Ws -f ${haproxyCfg}";
+        Restart = "on-failure";
+        RuntimeDirectory = "haproxy";
+        # needed in case we bind to port < 1024
+        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
       };
     };
-
-    environment.systemPackages = [ pkgs.haproxy ];
-
-    users.users.haproxy = {
-      group = "haproxy";
-      uid = config.ids.uids.haproxy;
-    };
-
-    users.groups.haproxy.gid = config.ids.uids.haproxy;
   };
 }
diff --git a/nixos/modules/services/networking/tinydns.nix b/nixos/modules/services/networking/tinydns.nix
index 7b2c464ab46b..79507b2ebcdd 100644
--- a/nixos/modules/services/networking/tinydns.nix
+++ b/nixos/modules/services/networking/tinydns.nix
@@ -37,6 +37,7 @@ with lib;
     systemd.services.tinydns = {
       description = "djbdns tinydns server";
       wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
       path = with pkgs; [ daemontools djbdns ];
       preStart = ''
         rm -rf /var/lib/tinydns
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index 3c5918baa533..f5a6051b4b5f 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -33,6 +33,8 @@ let
 
   enableSSL = any (vhost: vhost.enableSSL) allHosts;
 
+  enableUserDir = any (vhost: vhost.enableUserDir) allHosts;
+
   # NOTE: generally speaking order of modules is very important
   modules =
     [ # required apache modules our httpd service cannot run without
@@ -45,6 +47,7 @@ let
     ]
     ++ (if mainCfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ])
     ++ optional enableSSL "ssl"
+    ++ optional enableUserDir "userdir"
     ++ optional mainCfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; }
     ++ optional mainCfg.enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; }
     ++ optional mainCfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; }
diff --git a/nixos/modules/services/web-servers/phpfpm/default.nix b/nixos/modules/services/web-servers/phpfpm/default.nix
index 4ab7e3f0c0a9..095de64dfb14 100644
--- a/nixos/modules/services/web-servers/phpfpm/default.nix
+++ b/nixos/modules/services/web-servers/phpfpm/default.nix
@@ -31,7 +31,7 @@ let
     '';
     passAsFile = [ "nixDefaults" "phpOptions" ];
   } ''
-    cat $phpPackage/etc/php.ini $nixDefaultsPath $phpOptionsPath > $out
+    cat ${poolOpts.phpPackage}/etc/php.ini $nixDefaultsPath $phpOptionsPath > $out
   '';
 
   poolOpts = { name, ... }:
@@ -69,8 +69,6 @@ let
 
         phpOptions = mkOption {
           type = types.lines;
-          default = cfg.phpOptions;
-          defaultText = "config.services.phpfpm.phpOptions";
           description = ''
             "Options appended to the PHP configuration file <filename>php.ini</filename> used for this PHP-FPM pool."
           '';
@@ -137,6 +135,7 @@ let
       config = {
         socket = if poolOpts.listen == "" then "${runtimeDir}/${name}.sock" else poolOpts.listen;
         group = mkDefault poolOpts.user;
+        phpOptions = mkBefore cfg.phpOptions;
 
         settings = mapAttrs (name: mkDefault){
           listen = poolOpts.socket;
diff --git a/nixos/modules/services/web-servers/varnish/default.nix b/nixos/modules/services/web-servers/varnish/default.nix
index 63f967185c2d..01fe3d12917a 100644
--- a/nixos/modules/services/web-servers/varnish/default.nix
+++ b/nixos/modules/services/web-servers/varnish/default.nix
@@ -15,8 +15,8 @@ in
 
       package = mkOption {
         type = types.package;
-        default = pkgs.varnish5;
-        defaultText = "pkgs.varnish5";
+        default = pkgs.varnish;
+        defaultText = "pkgs.varnish";
         description = ''
           The package to use
         '';
@@ -48,7 +48,7 @@ in
       extraModules = mkOption {
         type = types.listOf types.package;
         default = [];
-        example = literalExample "[ pkgs.varnish5Packages.geoip ]";
+        example = literalExample "[ pkgs.varnishPackages.geoip ]";
         description = "
           Varnish modules (except 'std').
         ";
diff --git a/nixos/modules/services/x11/desktop-managers/gnome3.nix b/nixos/modules/services/x11/desktop-managers/gnome3.nix
index 5ad31e5b9d00..0135ac19cbf7 100644
--- a/nixos/modules/services/x11/desktop-managers/gnome3.nix
+++ b/nixos/modules/services/x11/desktop-managers/gnome3.nix
@@ -184,6 +184,13 @@ in
         enableGnomeKeyring = true;
       };
 
+      systemd.packages = with pkgs.gnome3; [
+        gnome-flashback
+      ] ++ (map
+        (wm: gnome-flashback.mkSystemdTargetForWm {
+          inherit (wm) wmName;
+        }) cfg.flashback.customSessions);
+
       services.dbus.packages = [
         pkgs.gnome3.gnome-screensaver
       ];
@@ -217,6 +224,12 @@ in
 
       services.xserver.updateDbusEnvironment = true;
 
+      # gnome has a custom alert theme but it still
+      # inherits from the freedesktop theme.
+      environment.systemPackages = with pkgs; [
+        sound-theme-freedesktop
+      ];
+
       # Needed for themes and backgrounds
       environment.pathsToLink = [
         "/share" # TODO: https://github.com/NixOS/nixpkgs/issues/47173
@@ -265,6 +278,26 @@ in
         source-sans-pro
       ];
 
+      ## Enable soft realtime scheduling, only supported on wayland ##
+
+      security.wrappers.".gnome-shell-wrapped" = {
+        source = "${pkgs.gnome3.gnome-shell}/bin/.gnome-shell-wrapped";
+        capabilities = "cap_sys_nice=ep";
+      };
+
+      systemd.user.services.gnome-shell-wayland = let
+        gnomeShellRT = with pkgs.gnome3; pkgs.runCommand "gnome-shell-rt" {} ''
+          mkdir -p $out/bin/
+          cp ${gnome-shell}/bin/gnome-shell $out/bin
+          sed -i "s@${gnome-shell}/bin/@${config.security.wrapperDir}/@" $out/bin/gnome-shell
+        '';
+      in {
+        # Note we need to clear ExecStart before overriding it
+        serviceConfig.ExecStart = ["" "${gnomeShellRT}/bin/gnome-shell"];
+        # Do not use the default environment, it provides a broken PATH
+        environment = mkForce {};
+      };
+
       # Adapt from https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/gnome-3-32/elements/core/meta-gnome-core-shell.bst
       environment.systemPackages = with pkgs.gnome3; [
         adwaita-icon-theme
diff --git a/nixos/modules/system/boot/plymouth.nix b/nixos/modules/system/boot/plymouth.nix
index adca3c3f66e7..23fce22366d8 100644
--- a/nixos/modules/system/boot/plymouth.nix
+++ b/nixos/modules/system/boot/plymouth.nix
@@ -25,6 +25,7 @@ let
     [Daemon]
     ShowDelay=0
     Theme=${cfg.theme}
+    ${cfg.extraConfig}
   '';
 
 in
@@ -65,6 +66,15 @@ in
         '';
       };
 
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Literal string to append to <literal>configFile</literal>
+          and the config file generated by the plymouth module.
+        '';
+      };
+
     };
 
   };
diff --git a/nixos/modules/system/boot/systemd-unit-options.nix b/nixos/modules/system/boot/systemd-unit-options.nix
index c1f2c98afcd8..bee21f1a8f36 100644
--- a/nixos/modules/system/boot/systemd-unit-options.nix
+++ b/nixos/modules/system/boot/systemd-unit-options.nix
@@ -24,7 +24,7 @@ in rec {
       in
         if isList (head defs'')
         then concatLists defs''
-        else mergeOneOption loc defs';
+        else mergeEqualOption loc defs';
   };
 
   sharedOptions = {
diff --git a/nixos/modules/virtualisation/libvirtd.nix b/nixos/modules/virtualisation/libvirtd.nix
index 16b79d869193..edde7dde39ed 100644
--- a/nixos/modules/virtualisation/libvirtd.nix
+++ b/nixos/modules/virtualisation/libvirtd.nix
@@ -146,7 +146,8 @@ in {
       # this file is expected in /etc/qemu and not sysconfdir (/var/lib)
       etc."qemu/bridge.conf".text = lib.concatMapStringsSep "\n" (e:
         "allow ${e}") cfg.allowedBridges;
-      systemPackages = with pkgs; [ libvirt libressl.nc cfg.qemuPackage ];
+      systemPackages = with pkgs; [ libvirt libressl.nc iptables cfg.qemuPackage ];
+      etc.ethertypes.source = "${iptables}/etc/ethertypes";
     };
 
     boot.kernelModules = [ "tun" ];
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 636c4e93e9b3..9db505a27d41 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -244,6 +244,7 @@ in
   rxe = handleTest ./rxe.nix {};
   samba = handleTest ./samba.nix {};
   sddm = handleTest ./sddm.nix {};
+  shiori = handleTest ./shiori.nix {};
   signal-desktop = handleTest ./signal-desktop.nix {};
   simple = handleTest ./simple.nix {};
   slim = handleTest ./slim.nix {};
diff --git a/nixos/tests/caddy.nix b/nixos/tests/caddy.nix
index ab9d2fbf4d1d..fc10df0c79b5 100644
--- a/nixos/tests/caddy.nix
+++ b/nixos/tests/caddy.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "caddy";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ xfix ];
@@ -50,33 +50,38 @@ import ./make-test.nix ({ pkgs, ... }: {
     etagSystem = "${nodes.webserver.config.system.build.toplevel}/fine-tune/child-1";
     justReloadSystem = "${nodes.webserver.config.system.build.toplevel}/fine-tune/child-2";
   in ''
-    my $url = 'http://localhost/example.html';
-    $webserver->waitForUnit("caddy");
-    $webserver->waitForOpenPort("80");
+    url = "http://localhost/example.html"
+    webserver.wait_for_unit("caddy")
+    webserver.wait_for_open_port("80")
 
-    sub checkEtag {
-      my $etag = $webserver->succeed(
-        'curl -v '.$url.' 2>&1 | sed -n -e "s/^< [Ee][Tt][Aa][Gg]: *//p"'
-      );
-      $etag =~ s/\r?\n$//;
-      my $httpCode = $webserver->succeed(
-        'curl -w "%{http_code}" -X HEAD -H \'If-None-Match: '.$etag.'\' '.$url
-      );
-      die "HTTP code is not 304" unless $httpCode == 304;
-      return $etag;
-    }
 
-    subtest "check ETag if serving Nix store paths", sub {
-      my $oldEtag = checkEtag;
-      $webserver->succeed("${etagSystem}/bin/switch-to-configuration test >&2");
-      $webserver->sleep(1); # race condition
-      my $newEtag = checkEtag;
-      die "Old ETag $oldEtag is the same as $newEtag" if $oldEtag eq $newEtag;
-    };
+    def check_etag(url):
+        etag = webserver.succeed(
+            "curl -v '{}' 2>&1 | sed -n -e \"s/^< [Ee][Tt][Aa][Gg]: *//p\"".format(url)
+        )
+        etag = etag.replace("\r\n", " ")
+        http_code = webserver.succeed(
+            "curl -w \"%{{http_code}}\" -X HEAD -H 'If-None-Match: {}' {}".format(etag, url)
+        )
+        assert int(http_code) == 304, "HTTP code is not 304"
+        return etag
 
-    subtest "config is reloaded on nixos-rebuild switch", sub {
-      $webserver->succeed("${justReloadSystem}/bin/switch-to-configuration test >&2");
-      $webserver->waitForOpenPort("8080");
-    };
+
+    with subtest("check ETag if serving Nix store paths"):
+        old_etag = check_etag(url)
+        webserver.succeed(
+            "${etagSystem}/bin/switch-to-configuration test >&2"
+        )
+        webserver.sleep(1)
+        new_etag = check_etag(url)
+        assert old_etag != new_etag, "Old ETag {} is the same as {}".format(
+            old_etag, new_etag
+        )
+    
+    with subtest("config is reloaded on nixos-rebuild switch"):
+        webserver.succeed(
+            "${justReloadSystem}/bin/switch-to-configuration test >&2"
+        )
+        webserver.wait_for_open_port("8080")
   '';
 })
diff --git a/nixos/tests/cadvisor.nix b/nixos/tests/cadvisor.nix
index e60bae4b7003..60c04f147800 100644
--- a/nixos/tests/cadvisor.nix
+++ b/nixos/tests/cadvisor.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... } : {
+import ./make-test-python.nix ({ pkgs, ... } : {
   name = "cadvisor";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ offline ];
@@ -16,20 +16,19 @@ import ./make-test.nix ({ pkgs, ... } : {
     };
   };
 
-  testScript =
-    ''
-      startAll;
-      $machine->waitForUnit("cadvisor.service");
-      $machine->succeed("curl http://localhost:8080/containers/");
+  testScript =  ''
+      start_all()
+      machine.wait_for_unit("cadvisor.service")
+      machine.succeed("curl http://localhost:8080/containers/")
 
-      $influxdb->waitForUnit("influxdb.service");
+      influxdb.wait_for_unit("influxdb.service")
 
       # create influxdb database
-      $influxdb->succeed(q~
-        curl -XPOST http://localhost:8086/query --data-urlencode "q=CREATE DATABASE root"
-      ~);
+      influxdb.succeed(
+          'curl -XPOST http://localhost:8086/query --data-urlencode "q=CREATE DATABASE root"'
+      )
 
-      $influxdb->waitForUnit("cadvisor.service");
-      $influxdb->succeed("curl http://localhost:8080/containers/");
+      influxdb.wait_for_unit("cadvisor.service")
+      influxdb.succeed("curl http://localhost:8080/containers/")
     '';
 })
diff --git a/nixos/tests/cassandra.nix b/nixos/tests/cassandra.nix
index c55733c9be7b..05607956a9d6 100644
--- a/nixos/tests/cassandra.nix
+++ b/nixos/tests/cassandra.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, lib, ... }:
+import ./make-test-python.nix ({ pkgs, lib, ... }:
 let
   # Change this to test a different version of Cassandra:
   testPackage = pkgs.cassandra;
@@ -9,13 +9,16 @@ let
   jmxRolesFile = ./cassandra-jmx-roles;
   jmxAuthArgs = "-u ${(builtins.elemAt jmxRoles 0).username} -pw ${(builtins.elemAt jmxRoles 0).password}";
   jmxPort = 7200;  # Non-standard port so it doesn't accidentally work
+  jmxPortStr = toString jmxPort;
 
-  # Would usually be assigned to 512M
+  # Would usually be assigned to 512M.
+  # Set it to a different value, so that we can check whether our config
+  # actually changes it.
   numMaxHeapSize = "400";
   getHeapLimitCommand = ''
-    nodetool info -p ${toString jmxPort} | grep "^Heap Memory" | awk \'{print $NF}\'
+    nodetool info -p ${jmxPortStr} | grep "^Heap Memory" | awk '{print $NF}'
   '';
-  checkHeapLimitCommand = ''
+  checkHeapLimitCommand = pkgs.writeShellScript "check-heap-limit.sh" ''
     [ 1 -eq "$(echo "$(${getHeapLimitCommand}) < ${numMaxHeapSize}" | ${pkgs.bc}/bin/bc)" ]
   '';
 
@@ -44,7 +47,10 @@ let
   };
 in
 {
-  name = "cassandra-ci";
+  name = "cassandra";
+  meta = {
+    maintainers = with lib.maintainers; [ johnazoidberg ];
+  };
 
   nodes = {
     cass0 = nodeCfg "192.168.1.1" {};
@@ -52,66 +58,74 @@ in
     cass2 = nodeCfg "192.168.1.3" { jvmOpts = [ "-Dcassandra.replace_address=cass1" ]; };
   };
 
-  testScript = let
-    jmxPortS = toString jmxPort;
-  in ''
+  testScript = ''
     # Check configuration
-    subtest "Timers exist", sub {
-      $cass0->succeed("systemctl list-timers | grep cassandra-full-repair.timer");
-      $cass0->succeed("systemctl list-timers | grep cassandra-incremental-repair.timer");
-    };
-    subtest "Can connect via cqlsh", sub {
-      $cass0->waitForUnit("cassandra.service");
-      $cass0->waitUntilSucceeds("nc -z cass0 9042");
-      $cass0->succeed("echo 'show version;' | cqlsh cass0");
-    };
-    subtest "Nodetool is operational", sub {
-      $cass0->waitForUnit("cassandra.service");
-      $cass0->waitUntilSucceeds("nc -z localhost ${jmxPortS}");
-      $cass0->succeed("nodetool status -p ${jmxPortS} --resolve-ip | egrep '^UN[[:space:]]+cass0'");
-    };
-    subtest "Cluster name was set", sub {
-      $cass0->waitForUnit("cassandra.service");
-      $cass0->waitUntilSucceeds("nc -z localhost ${jmxPortS}");
-      $cass0->waitUntilSucceeds("nodetool describecluster -p ${jmxPortS} | grep 'Name: ${clusterName}'");
-    };
-    subtest "Heap limit set correctly", sub {
-      # Nodetool takes a while until it can display info
-      $cass0->waitUntilSucceeds('nodetool info -p ${jmxPortS}');
-      $cass0->succeed('${checkHeapLimitCommand}');
-    };
+    with subtest("Timers exist"):
+        cass0.succeed("systemctl list-timers | grep cassandra-full-repair.timer")
+        cass0.succeed("systemctl list-timers | grep cassandra-incremental-repair.timer")
+
+    with subtest("Can connect via cqlsh"):
+        cass0.wait_for_unit("cassandra.service")
+        cass0.wait_until_succeeds("nc -z cass0 9042")
+        cass0.succeed("echo 'show version;' | cqlsh cass0")
+
+    with subtest("Nodetool is operational"):
+        cass0.wait_for_unit("cassandra.service")
+        cass0.wait_until_succeeds("nc -z localhost ${jmxPortStr}")
+        cass0.succeed("nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass0'")
+
+    with subtest("Cluster name was set"):
+        cass0.wait_for_unit("cassandra.service")
+        cass0.wait_until_succeeds("nc -z localhost ${jmxPortStr}")
+        cass0.wait_until_succeeds(
+            "nodetool describecluster -p ${jmxPortStr} | grep 'Name: ${clusterName}'"
+        )
+
+    with subtest("Heap limit set correctly"):
+        # Nodetool takes a while until it can display info
+        cass0.wait_until_succeeds("nodetool info -p ${jmxPortStr}")
+        cass0.succeed("${checkHeapLimitCommand}")
 
     # Check cluster interaction
-    subtest "Bring up cluster", sub {
-      $cass1->waitForUnit("cassandra.service");
-      $cass1->waitUntilSucceeds("nodetool -p ${jmxPortS} ${jmxAuthArgs} status | egrep -c '^UN' | grep 2");
-      $cass0->succeed("nodetool status -p ${jmxPortS} --resolve-ip | egrep '^UN[[:space:]]+cass1'");
-    };
+    with subtest("Bring up cluster"):
+        cass1.wait_for_unit("cassandra.service")
+        cass1.wait_until_succeeds(
+            "nodetool -p ${jmxPortStr} ${jmxAuthArgs} status | egrep -c '^UN' | grep 2"
+        )
+        cass0.succeed("nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass1'")
   '' + lib.optionalString testRemoteAuth ''
-    subtest "Remote authenticated jmx", sub {
-      # Doesn't work if not enabled
-      $cass0->waitUntilSucceeds("nc -z localhost ${jmxPortS}");
-      $cass1->fail("nc -z 192.168.1.1 ${toString jmxPort}");
-      $cass1->fail("nodetool -p ${jmxPortS} -h 192.168.1.1 status");
+    with subtest("Remote authenticated jmx"):
+        # Doesn't work if not enabled
+        cass0.wait_until_succeeds("nc -z localhost ${jmxPortStr}")
+        cass1.fail("nc -z 192.168.1.1 ${jmxPortStr}")
+        cass1.fail("nodetool -p ${jmxPortStr} -h 192.168.1.1 status")
 
-      # Works if enabled
-      $cass1->waitUntilSucceeds("nc -z localhost ${toString jmxPort}");
-      $cass0->succeed("nodetool -p ${jmxPortS} -h 192.168.1.2 ${jmxAuthArgs} status");
-    };
+        # Works if enabled
+        cass1.wait_until_succeeds("nc -z localhost ${jmxPortStr}")
+        cass0.succeed("nodetool -p ${jmxPortStr} -h 192.168.1.2 ${jmxAuthArgs} status")
   '' + ''
-    subtest "Break and fix node", sub {
-      $cass1->block;
-      $cass0->waitUntilSucceeds("nodetool status -p ${jmxPortS} --resolve-ip | egrep -c '^DN[[:space:]]+cass1'");
-      $cass0->succeed("nodetool status -p ${jmxPortS} | egrep -c '^UN'  | grep 1");
-      $cass1->unblock;
-      $cass1->waitUntilSucceeds("nodetool -p ${jmxPortS} ${jmxAuthArgs} status | egrep -c '^UN'  | grep 2");
-      $cass0->succeed("nodetool status -p ${jmxPortS} | egrep -c '^UN'  | grep 2");
-    };
-    subtest "Replace crashed node", sub {
-      $cass1->crash;
-      $cass2->waitForUnit("cassandra.service");
-      $cass0->waitUntilFails("nodetool status -p ${jmxPortS} --resolve-ip | egrep '^UN[[:space:]]+cass1'");
-      $cass0->waitUntilSucceeds("nodetool status -p ${jmxPortS} --resolve-ip | egrep '^UN[[:space:]]+cass2'");
-    };
+    with subtest("Break and fix node"):
+        cass1.block()
+        cass0.wait_until_succeeds(
+            "nodetool status -p ${jmxPortStr} --resolve-ip | egrep -c '^DN[[:space:]]+cass1'"
+        )
+        cass0.succeed("nodetool status -p ${jmxPortStr} | egrep -c '^UN'  | grep 1")
+        cass1.unblock()
+        cass1.wait_until_succeeds(
+            "nodetool -p ${jmxPortStr} ${jmxAuthArgs} status | egrep -c '^UN'  | grep 2"
+        )
+        cass0.succeed("nodetool status -p ${jmxPortStr} | egrep -c '^UN'  | grep 2")
+
+    with subtest("Replace crashed node"):
+        cass1.block()  # .crash() waits until it's fully shutdown
+        cass2.start()
+        cass0.wait_until_fails(
+            "nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass1'"
+        )
+
+        cass2.wait_for_unit("cassandra.service")
+        cass0.wait_until_succeeds(
+            "nodetool status -p ${jmxPortStr} --resolve-ip | egrep '^UN[[:space:]]+cass2'"
+        )
   '';
 })
diff --git a/nixos/tests/ceph-multi-node.nix b/nixos/tests/ceph-multi-node.nix
index 6698aac3f271..ed493d6a1b34 100644
--- a/nixos/tests/ceph-multi-node.nix
+++ b/nixos/tests/ceph-multi-node.nix
@@ -49,9 +49,6 @@ let
     boot.kernelModules = [ "xfs" ];
 
     services.ceph = cephConfig;
-
-    # So that we don't have to battle systemd when bootstraping
-    systemd.targets.ceph.wantedBy = lib.mkForce [];
   };
 
   networkMonA = {
@@ -107,6 +104,10 @@ let
     };
   }; };
 
+  # Following deployment is based on the manual deployment described here:
+  # https://docs.ceph.com/docs/master/install/manual-deployment/
+  # For other ways to deploy a ceph cluster, look at the documentation at
+  # https://docs.ceph.com/docs/master/
   testscript = { ... }: ''
     startAll;
 
@@ -114,27 +115,6 @@ let
     $osd0->waitForUnit("network.target");
     $osd1->waitForUnit("network.target");
 
-    # Create the ceph-related directories
-    $monA->mustSucceed(
-      "mkdir -p /var/lib/ceph/mgr/ceph-${cfg.monA.name}",
-      "mkdir -p /var/lib/ceph/mon/ceph-${cfg.monA.name}",
-      "chown ceph:ceph -R /var/lib/ceph/",
-      "mkdir -p /etc/ceph",
-      "chown ceph:ceph -R /etc/ceph"
-    );
-    $osd0->mustSucceed(
-      "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
-      "chown ceph:ceph -R /var/lib/ceph/",
-      "mkdir -p /etc/ceph",
-      "chown ceph:ceph -R /etc/ceph"
-    );
-    $osd1->mustSucceed(
-      "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
-      "chown ceph:ceph -R /var/lib/ceph/",
-      "mkdir -p /etc/ceph",
-      "chown ceph:ceph -R /etc/ceph"
-    );
-
     # Bootstrap ceph-mon daemon
     $monA->mustSucceed(
       "sudo -u ceph ceph-authtool --create-keyring /tmp/ceph.mon.keyring --gen-key -n mon. --cap mon 'allow *'",
@@ -142,6 +122,7 @@ let
       "sudo -u ceph ceph-authtool /tmp/ceph.mon.keyring --import-keyring /etc/ceph/ceph.client.admin.keyring",
       "monmaptool --create --add ${cfg.monA.name} ${cfg.monA.ip} --fsid ${cfg.clusterId} /tmp/monmap",
       "sudo -u ceph ceph-mon --mkfs -i ${cfg.monA.name} --monmap /tmp/monmap --keyring /tmp/ceph.mon.keyring",
+      "sudo -u ceph mkdir -p /var/lib/ceph/mgr/ceph-${cfg.monA.name}/",
       "sudo -u ceph touch /var/lib/ceph/mon/ceph-${cfg.monA.name}/done",
       "systemctl start ceph-mon-${cfg.monA.name}"
     );
@@ -168,12 +149,14 @@ let
     # Bootstrap both OSDs
     $osd0->mustSucceed(
       "mkfs.xfs /dev/vdb",
+      "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
       "mount /dev/vdb /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
       "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd0.name}/keyring --name osd.${cfg.osd0.name} --add-key ${cfg.osd0.key}",
       "echo '{\"cephx_secret\": \"${cfg.osd0.key}\"}' | ceph osd new ${cfg.osd0.uuid} -i -",
     );
     $osd1->mustSucceed(
       "mkfs.xfs /dev/vdb",
+      "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
       "mount /dev/vdb /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
       "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd1.name}/keyring --name osd.${cfg.osd1.name} --add-key ${cfg.osd1.key}",
       "echo '{\"cephx_secret\": \"${cfg.osd1.key}\"}' | ceph osd new ${cfg.osd1.uuid} -i -"
@@ -209,22 +192,17 @@ let
       "ceph osd pool delete multi-node-other-test multi-node-other-test --yes-i-really-really-mean-it"
     );
 
-    # As we disable the target in the config, we still want to test that it works as intended
-    $osd0->mustSucceed("systemctl stop ceph-osd-${cfg.osd0.name}");
-    $osd1->mustSucceed("systemctl stop ceph-osd-${cfg.osd1.name}");
-    $monA->mustSucceed(
-      "systemctl stop ceph-mgr-${cfg.monA.name}",
-      "systemctl stop ceph-mon-${cfg.monA.name}"
-    );
-    
-    $monA->succeed("systemctl start ceph.target");
-    $monA->waitForUnit("ceph-mon-${cfg.monA.name}");
-    $monA->waitForUnit("ceph-mgr-${cfg.monA.name}");
-    $osd0->succeed("systemctl start ceph.target");
-    $osd0->waitForUnit("ceph-osd-${cfg.osd0.name}");
-    $osd1->succeed("systemctl start ceph.target");
-    $osd1->waitForUnit("ceph-osd-${cfg.osd1.name}");
-    
+    # Shut down ceph on all machines in a very unpolite way
+    $monA->crash;
+    $osd0->crash;
+    $osd1->crash;
+
+    # Start it up
+    $osd0->start;
+    $osd1->start;
+    $monA->start;
+
+    # Ensure the cluster comes back up again
     $monA->succeed("ceph -s | grep 'mon: 1 daemons'");
     $monA->waitUntilSucceeds("ceph -s | grep 'quorum ${cfg.monA.name}'");
     $monA->waitUntilSucceeds("ceph osd stat | grep -e '2 osds: 2 up[^,]*, 2 in'");
diff --git a/nixos/tests/ceph-single-node.nix b/nixos/tests/ceph-single-node.nix
index 10b77cff5a31..041fbd7e8e64 100644
--- a/nixos/tests/ceph-single-node.nix
+++ b/nixos/tests/ceph-single-node.nix
@@ -46,9 +46,6 @@ let
     boot.kernelModules = [ "xfs" ];
 
     services.ceph = cephConfig;
-
-    # So that we don't have to battle systemd when bootstraping
-    systemd.targets.ceph.wantedBy = lib.mkForce [];
   };
 
   networkMonA = {
@@ -72,22 +69,15 @@ let
     };
   }; };
 
+  # Following deployment is based on the manual deployment described here:
+  # https://docs.ceph.com/docs/master/install/manual-deployment/
+  # For other ways to deploy a ceph cluster, look at the documentation at
+  # https://docs.ceph.com/docs/master/
   testscript = { ... }: ''
     startAll;
 
     $monA->waitForUnit("network.target");
 
-    # Create the ceph-related directories
-    $monA->mustSucceed(
-      "mkdir -p /var/lib/ceph/mgr/ceph-${cfg.monA.name}",
-      "mkdir -p /var/lib/ceph/mon/ceph-${cfg.monA.name}",
-      "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
-      "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
-      "mkdir -p /etc/ceph",
-      "chown ceph:ceph -R /etc/ceph",
-      "chown ceph:ceph -R /var/lib/ceph/",
-    );
-
     # Bootstrap ceph-mon daemon
     $monA->mustSucceed(
       "sudo -u ceph ceph-authtool --create-keyring /tmp/ceph.mon.keyring --gen-key -n mon. --cap mon 'allow *'",
@@ -104,8 +94,9 @@ let
     # Can't check ceph status until a mon is up
     $monA->succeed("ceph -s | grep 'mon: 1 daemons'");
 
-    # Start the ceph-mgr daemon, it has no deps and hardly any setup
+    # Start the ceph-mgr daemon, after copying in the keyring
     $monA->mustSucceed(
+      "sudo -u ceph mkdir -p /var/lib/ceph/mgr/ceph-${cfg.monA.name}/",
       "ceph auth get-or-create mgr.${cfg.monA.name} mon 'allow profile mgr' osd 'allow *' mds 'allow *' > /var/lib/ceph/mgr/ceph-${cfg.monA.name}/keyring",
       "systemctl start ceph-mgr-${cfg.monA.name}"
     );
@@ -117,7 +108,9 @@ let
     $monA->mustSucceed(
       "mkfs.xfs /dev/vdb",
       "mkfs.xfs /dev/vdc",
+      "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
       "mount /dev/vdb /var/lib/ceph/osd/ceph-${cfg.osd0.name}",
+      "mkdir -p /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
       "mount /dev/vdc /var/lib/ceph/osd/ceph-${cfg.osd1.name}",
       "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd0.name}/keyring --name osd.${cfg.osd0.name} --add-key ${cfg.osd0.key}",
       "ceph-authtool --create-keyring /var/lib/ceph/osd/ceph-${cfg.osd1.name}/keyring --name osd.${cfg.osd1.name} --add-key ${cfg.osd1.key}",
@@ -159,20 +152,17 @@ let
       "ceph osd pool delete single-node-other-test single-node-other-test --yes-i-really-really-mean-it"
     );
 
-    # As we disable the target in the config, we still want to test that it works as intended
-    $monA->mustSucceed(
-      "systemctl stop ceph-osd-${cfg.osd0.name}",
-      "systemctl stop ceph-osd-${cfg.osd1.name}",
-      "systemctl stop ceph-mgr-${cfg.monA.name}",
-      "systemctl stop ceph-mon-${cfg.monA.name}"
-    );
-    
+    # Shut down ceph by stopping ceph.target.
+    $monA->mustSucceed("systemctl stop ceph.target");
+
+    # Start it up
     $monA->succeed("systemctl start ceph.target");
     $monA->waitForUnit("ceph-mon-${cfg.monA.name}");
     $monA->waitForUnit("ceph-mgr-${cfg.monA.name}");
     $monA->waitForUnit("ceph-osd-${cfg.osd0.name}");
     $monA->waitForUnit("ceph-osd-${cfg.osd1.name}");
-    
+
+    # Ensure the cluster comes back up again
     $monA->succeed("ceph -s | grep 'mon: 1 daemons'");
     $monA->waitUntilSucceeds("ceph -s | grep 'quorum ${cfg.monA.name}'");
     $monA->waitUntilSucceeds("ceph osd stat | grep -e '2 osds: 2 up[^,]*, 2 in'");
diff --git a/nixos/tests/certmgr.nix b/nixos/tests/certmgr.nix
index fe67833808ce..cb69f35e862f 100644
--- a/nixos/tests/certmgr.nix
+++ b/nixos/tests/certmgr.nix
@@ -3,7 +3,7 @@
   pkgs ? import ../.. { inherit system config; }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
+with import ../lib/testing-python.nix { inherit system pkgs; };
 let
   mkSpec = { host, service ? null, action }: {
     inherit action;
@@ -123,17 +123,17 @@ in
       )));
     };
     testScript = ''
-      $machine->waitForUnit('cfssl.service');
-      $machine->waitUntilSucceeds('ls /tmp/decl.example.org-ca.pem');
-      $machine->waitUntilSucceeds('ls /tmp/decl.example.org-key.pem');
-      $machine->waitUntilSucceeds('ls /tmp/decl.example.org-cert.pem');
-      $machine->waitUntilSucceeds('ls /tmp/imp.example.org-ca.pem');
-      $machine->waitUntilSucceeds('ls /tmp/imp.example.org-key.pem');
-      $machine->waitUntilSucceeds('ls /tmp/imp.example.org-cert.pem');
-      $machine->waitForUnit('nginx.service');
-      $machine->succeed('[ "1" -lt "$(journalctl -u nginx | grep "Starting Nginx" | wc -l)" ]');
-      $machine->succeed('curl --cacert /tmp/imp.example.org-ca.pem https://imp.example.org');
-      $machine->succeed('curl --cacert /tmp/decl.example.org-ca.pem https://decl.example.org');
+      machine.wait_for_unit("cfssl.service")
+      machine.wait_until_succeeds("ls /tmp/decl.example.org-ca.pem")
+      machine.wait_until_succeeds("ls /tmp/decl.example.org-key.pem")
+      machine.wait_until_succeeds("ls /tmp/decl.example.org-cert.pem")
+      machine.wait_until_succeeds("ls /tmp/imp.example.org-ca.pem")
+      machine.wait_until_succeeds("ls /tmp/imp.example.org-key.pem")
+      machine.wait_until_succeeds("ls /tmp/imp.example.org-cert.pem")
+      machine.wait_for_unit("nginx.service")
+      assert 1 < int(machine.succeed('journalctl -u nginx | grep "Starting Nginx" | wc -l'))
+      machine.succeed("curl --cacert /tmp/imp.example.org-ca.pem https://imp.example.org")
+      machine.succeed("curl --cacert /tmp/decl.example.org-ca.pem https://decl.example.org")
     '';
   };
 
@@ -143,8 +143,8 @@ in
       test = mkSpec { host = "command.example.org"; action = "touch /tmp/command.executed"; };
     };
     testScript = ''
-      $machine->waitForUnit('cfssl.service');
-      $machine->waitUntilSucceeds('stat /tmp/command.executed');
+      machine.wait_for_unit("cfssl.service")
+      machine.wait_until_succeeds("stat /tmp/command.executed")
     '';
   };
 
diff --git a/nixos/tests/cfssl.nix b/nixos/tests/cfssl.nix
index 513ed8c45741..e291fc285fba 100644
--- a/nixos/tests/cfssl.nix
+++ b/nixos/tests/cfssl.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "cfssl";
 
   machine = { config, lib, pkgs, ... }:
@@ -60,8 +60,8 @@ import ./make-test.nix ({ pkgs, ...} : {
     });
   in
     ''
-      $machine->waitForUnit('cfssl.service');
-      $machine->waitUntilSucceeds('${cfsslrequest}');
-      $machine->succeed('ls /tmp/certificate-key.pem');
+      machine.wait_for_unit("cfssl.service")
+      machine.wait_until_succeeds("${cfsslrequest}")
+      machine.succeed("ls /tmp/certificate-key.pem")
     '';
 })
diff --git a/nixos/tests/cjdns.nix b/nixos/tests/cjdns.nix
index 6660eecf05b9..d72236d415d4 100644
--- a/nixos/tests/cjdns.nix
+++ b/nixos/tests/cjdns.nix
@@ -17,7 +17,7 @@ let
 
 in
 
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "cjdns";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ ehmry ];
@@ -83,36 +83,39 @@ import ./make-test.nix ({ pkgs, ...} : {
 
   testScript =
     ''
-      startAll;
+      import re
 
-      $alice->waitForUnit("cjdns.service");
-      $bob->waitForUnit("cjdns.service");
-      $carol->waitForUnit("cjdns.service");
+      start_all()
 
-      sub cjdnsIp {
-          my ($machine) = @_;
-          my $ip = (split /[ \/]+/, $machine->succeed("ip -o -6 addr show dev tun0"))[3];
-          $machine->log("has ip $ip");
-          return $ip;
-      }
+      alice.wait_for_unit("cjdns.service")
+      bob.wait_for_unit("cjdns.service")
+      carol.wait_for_unit("cjdns.service")
 
-      my $aliceIp6 = cjdnsIp $alice;
-      my $bobIp6   = cjdnsIp $bob;
-      my $carolIp6 = cjdnsIp $carol;
+
+      def cjdns_ip(machine):
+          res = machine.succeed("ip -o -6 addr show dev tun0")
+          ip = re.split("\s+|/", res)[3]
+          machine.log("has ip {}".format(ip))
+          return ip
+
+
+      alice_ip6 = cjdns_ip(alice)
+      bob_ip6 = cjdns_ip(bob)
+      carol_ip6 = cjdns_ip(carol)
 
       # ping a few times each to let the routing table establish itself
 
-      $alice->succeed("ping -c 4 $carolIp6");
-      $bob->succeed("ping -c 4 $carolIp6");
+      alice.succeed("ping -c 4 {}".format(carol_ip6))
+      bob.succeed("ping -c 4 {}".format(carol_ip6))
 
-      $carol->succeed("ping -c 4 $aliceIp6");
-      $carol->succeed("ping -c 4 $bobIp6");
+      carol.succeed("ping -c 4 {}".format(alice_ip6))
+      carol.succeed("ping -c 4 {}".format(bob_ip6))
 
-      $alice->succeed("ping -c 4 $bobIp6");
-      $bob->succeed("ping -c 4 $aliceIp6");
+      alice.succeed("ping -c 4 {}".format(bob_ip6))
+      bob.succeed("ping -c 4 {}".format(alice_ip6))
 
-      $alice->waitForUnit("httpd.service");
+      alice.wait_for_unit("httpd.service")
 
-      $bob->succeed("curl --fail -g http://[$aliceIp6]");
+      bob.succeed("curl --fail -g http://[{}]".format(alice_ip6))
     '';
 })
diff --git a/nixos/tests/cloud-init.nix b/nixos/tests/cloud-init.nix
index 516d29c9036b..aafa6e24e84a 100644
--- a/nixos/tests/cloud-init.nix
+++ b/nixos/tests/cloud-init.nix
@@ -3,7 +3,7 @@
   pkgs ? import ../.. { inherit system config; }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
+with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
 let
@@ -30,6 +30,7 @@ let
       '';
   };
 in makeTest {
+  name = "cloud-init";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ lewo ];
   };
@@ -40,10 +41,12 @@ in makeTest {
       services.cloud-init.enable = true;
     };
   testScript = ''
-     $machine->start;
-     $machine->waitForUnit("cloud-init.service");
-     $machine->succeed("cat /tmp/cloudinit-write-file | grep -q 'cloudinit'");
+      machine.start()
+      machine.wait_for_unit("cloud-init.service")
+      machine.succeed("cat /tmp/cloudinit-write-file | grep -q 'cloudinit'")
 
-     $machine->waitUntilSucceeds("cat /root/.ssh/authorized_keys | grep -q 'should be a key!'");
+      machine.wait_until_succeeds(
+          "cat /root/.ssh/authorized_keys | grep -q 'should be a key!'"
+      )
   '';
 }
diff --git a/nixos/tests/couchdb.nix b/nixos/tests/couchdb.nix
index 48ea48eebbb3..10e95701acdb 100644
--- a/nixos/tests/couchdb.nix
+++ b/nixos/tests/couchdb.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, lib, ...}:
+import ./make-test-python.nix ({ pkgs, lib, ...}:
 
 with lib;
 
@@ -35,22 +35,42 @@ with lib;
         fi
       '';
   in ''
-    startAll;
-
-    $couchdb1->waitForUnit("couchdb.service");
-    $couchdb1->waitUntilSucceeds("${curlJqCheck "GET" "" ".couchdb" "Welcome"}");
-    $couchdb1->waitUntilSucceeds("${curlJqCheck "GET" "_all_dbs" ". | length" "2"}");
-    $couchdb1->succeed("${curlJqCheck "PUT" "foo" ".ok" "true"}");
-    $couchdb1->succeed("${curlJqCheck "GET" "_all_dbs" ". | length" "3"}");
-    $couchdb1->succeed("${curlJqCheck "DELETE" "foo" ".ok" "true"}");
-    $couchdb1->succeed("${curlJqCheck "GET" "_all_dbs" ". | length" "2"}");
-
-    $couchdb2->waitForUnit("couchdb.service");
-    $couchdb2->waitUntilSucceeds("${curlJqCheck "GET" "" ".couchdb" "Welcome"}");
-    $couchdb2->waitUntilSucceeds("${curlJqCheck "GET" "_all_dbs" ". | length" "0"}");
-    $couchdb2->succeed("${curlJqCheck "PUT" "foo" ".ok" "true"}");
-    $couchdb2->succeed("${curlJqCheck "GET" "_all_dbs" ". | length" "1"}");
-    $couchdb2->succeed("${curlJqCheck "DELETE" "foo" ".ok" "true"}");
-    $couchdb2->succeed("${curlJqCheck "GET" "_all_dbs" ". | length" "0"}");
+    start_all()
+
+    couchdb1.wait_for_unit("couchdb.service")
+    couchdb1.wait_until_succeeds(
+        "${curlJqCheck "GET" "" ".couchdb" "Welcome"}"
+    )
+    couchdb1.wait_until_succeeds(
+        "${curlJqCheck "GET" "_all_dbs" ". | length" "2"}"
+    )
+    couchdb1.succeed("${curlJqCheck "PUT" "foo" ".ok" "true"}")
+    couchdb1.succeed(
+        "${curlJqCheck "GET" "_all_dbs" ". | length" "3"}"
+    )
+    couchdb1.succeed(
+        "${curlJqCheck "DELETE" "foo" ".ok" "true"}"
+    )
+    couchdb1.succeed(
+        "${curlJqCheck "GET" "_all_dbs" ". | length" "2"}"
+    )
+
+    couchdb2.wait_for_unit("couchdb.service")
+    couchdb2.wait_until_succeeds(
+        "${curlJqCheck "GET" "" ".couchdb" "Welcome"}"
+    )
+    couchdb2.wait_until_succeeds(
+        "${curlJqCheck "GET" "_all_dbs" ". | length" "0"}"
+    )
+    couchdb2.succeed("${curlJqCheck "PUT" "foo" ".ok" "true"}")
+    couchdb2.succeed(
+        "${curlJqCheck "GET" "_all_dbs" ". | length" "1"}"
+    )
+    couchdb2.succeed(
+        "${curlJqCheck "DELETE" "foo" ".ok" "true"}"
+    )
+    couchdb2.succeed(
+        "${curlJqCheck "GET" "_all_dbs" ". | length" "0"}"
+    )
   '';
 })
diff --git a/nixos/tests/dnscrypt-proxy.nix b/nixos/tests/dnscrypt-proxy.nix
index 13bc9d3d9168..98153d5c9047 100644
--- a/nixos/tests/dnscrypt-proxy.nix
+++ b/nixos/tests/dnscrypt-proxy.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "dnscrypt-proxy";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ joachifm ];
@@ -23,11 +23,13 @@ import ./make-test.nix ({ pkgs, ... }: {
   };
 
   testScript = ''
-    $client->waitForUnit("dnsmasq");
+    client.wait_for_unit("dnsmasq")
 
     # The daemon is socket activated; sending a single ping should activate it.
-    $client->fail("systemctl is-active dnscrypt-proxy");
-    $client->execute("${pkgs.iputils}/bin/ping -c1 example.com");
-    $client->waitUntilSucceeds("systemctl is-active dnscrypt-proxy");
+    client.fail("systemctl is-active dnscrypt-proxy")
+    client.execute(
+        "${pkgs.iputils}/bin/ping -c1 example.com"
+    )
+    client.wait_until_succeeds("systemctl is-active dnscrypt-proxy")
   '';
 })
diff --git a/nixos/tests/docker-edge.nix b/nixos/tests/docker-edge.nix
index b306c149be91..96de885a554a 100644
--- a/nixos/tests/docker-edge.nix
+++ b/nixos/tests/docker-edge.nix
@@ -1,6 +1,6 @@
 # This test runs docker and checks if simple container starts
 
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "docker";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ nequissimus offline ];
@@ -31,17 +31,19 @@ import ./make-test.nix ({ pkgs, ...} : {
     };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $docker->waitForUnit("sockets.target");
-    $docker->succeed("tar cv --files-from /dev/null | docker import - scratchimg");
-    $docker->succeed("docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10");
-    $docker->succeed("docker ps | grep sleeping");
-    $docker->succeed("sudo -u hasprivs docker ps");
-    $docker->fail("sudo -u noprivs docker ps");
-    $docker->succeed("docker stop sleeping");
+    docker.wait_for_unit("sockets.target")
+    docker.succeed("tar cv --files-from /dev/null | docker import - scratchimg")
+    docker.succeed(
+        "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+    )
+    docker.succeed("docker ps | grep sleeping")
+    docker.succeed("sudo -u hasprivs docker ps")
+    docker.fail("sudo -u noprivs docker ps")
+    docker.succeed("docker stop sleeping")
 
     # Must match version twice to ensure client and server versions are correct
-    $docker->succeed('[ $(docker version | grep ${pkgs.docker-edge.version} | wc -l) = "2" ]');
+    docker.succeed('[ $(docker version | grep ${pkgs.docker-edge.version} | wc -l) = "2" ]')
   '';
 })
diff --git a/nixos/tests/docker.nix b/nixos/tests/docker.nix
index d67b2f8743d8..8fda7c1395ef 100644
--- a/nixos/tests/docker.nix
+++ b/nixos/tests/docker.nix
@@ -1,6 +1,6 @@
 # This test runs docker and checks if simple container starts
 
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "docker";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ nequissimus offline ];
@@ -31,17 +31,19 @@ import ./make-test.nix ({ pkgs, ...} : {
     };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $docker->waitForUnit("sockets.target");
-    $docker->succeed("tar cv --files-from /dev/null | docker import - scratchimg");
-    $docker->succeed("docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10");
-    $docker->succeed("docker ps | grep sleeping");
-    $docker->succeed("sudo -u hasprivs docker ps");
-    $docker->fail("sudo -u noprivs docker ps");
-    $docker->succeed("docker stop sleeping");
+    docker.wait_for_unit("sockets.target")
+    docker.succeed("tar cv --files-from /dev/null | docker import - scratchimg")
+    docker.succeed(
+        "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
+    )
+    docker.succeed("docker ps | grep sleeping")
+    docker.succeed("sudo -u hasprivs docker ps")
+    docker.fail("sudo -u noprivs docker ps")
+    docker.succeed("docker stop sleeping")
 
     # Must match version twice to ensure client and server versions are correct
-    $docker->succeed('[ $(docker version | grep ${pkgs.docker.version} | wc -l) = "2" ]');
+    docker.succeed('[ $(docker version | grep ${pkgs.docker.version} | wc -l) = "2" ]')
   '';
 })
diff --git a/nixos/tests/documize.nix b/nixos/tests/documize.nix
index 8b852a4f7795..3be20a780d31 100644
--- a/nixos/tests/documize.nix
+++ b/nixos/tests/documize.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, lib, ...} : {
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
   name = "documize";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ ma27 ];
@@ -29,30 +29,34 @@ import ./make-test.nix ({ pkgs, lib, ...} : {
   };
 
   testScript = ''
-    startAll;
-
-    $machine->waitForUnit("documize-server.service");
-    $machine->waitForOpenPort(3000);
-
-    my $dbhash = $machine->succeed("curl -f localhost:3000 "
-                                  . " | grep 'property=\"dbhash' "
-                                  . " | grep -Po 'content=\"\\K[^\"]*'"
-                                  );
-
-    chomp($dbhash);
-
-    $machine->succeed("curl -X POST "
-                      . "--data 'dbname=documize' "
-                      . "--data 'dbhash=$dbhash' "
-                      . "--data 'title=NixOS' "
-                      . "--data 'message=Docs' "
-                      . "--data 'firstname=John' "
-                      . "--data 'lastname=Doe' "
-                      . "--data 'email=john.doe\@nixos.org' "
-                      . "--data 'password=verysafe' "
-                      . "-f localhost:3000/api/setup"
-                    );
-
-    $machine->succeed('test "$(curl -f localhost:3000/api/public/meta | jq ".title" | xargs echo)" = "NixOS"');
+    start_all()
+
+    machine.wait_for_unit("documize-server.service")
+    machine.wait_for_open_port(3000)
+
+    dbhash = machine.succeed(
+        "curl -f localhost:3000 | grep 'property=\"dbhash' | grep -Po 'content=\"\\K[^\"]*'"
+    )
+
+    dbhash = dbhash.strip()
+
+    machine.succeed(
+        (
+            "curl -X POST"
+            " --data 'dbname=documize'"
+            " --data 'dbhash={}'"
+            " --data 'title=NixOS'"
+            " --data 'message=Docs'"
+            " --data 'firstname=John'"
+            " --data 'lastname=Doe'"
+            " --data 'email=john.doe@nixos.org'"
+            " --data 'password=verysafe'"
+            " -f localhost:3000/api/setup"
+        ).format(dbhash)
+    )
+
+    machine.succeed(
+        'test "$(curl -f localhost:3000/api/public/meta | jq ".title" | xargs echo)" = "NixOS"'
+    )
   '';
 })
diff --git a/nixos/tests/haproxy.nix b/nixos/tests/haproxy.nix
index 22a83e9d1eab..72e77a68193e 100644
--- a/nixos/tests/haproxy.nix
+++ b/nixos/tests/haproxy.nix
@@ -16,6 +16,8 @@ import ./make-test.nix ({ pkgs, ...}: {
           frontend http
             bind *:80
             mode http
+            option http-use-htx
+            http-request use-service prometheus-exporter if { path /metrics }
             use_backend http_server
         '';
       };
@@ -36,6 +38,6 @@ import ./make-test.nix ({ pkgs, ...}: {
     $machine->waitForUnit('haproxy.service');
     $machine->waitForUnit('httpd.service');
     $machine->succeed('curl -k http://localhost:80/index.txt | grep "We are all good!"');
-
+    $machine->succeed('curl -k http://localhost:80/metrics | grep haproxy_process_pool_allocated_bytes');
   '';
 })
diff --git a/nixos/tests/lightdm.nix b/nixos/tests/lightdm.nix
index c805f1ed9f3c..ef30f7741e23 100644
--- a/nixos/tests/lightdm.nix
+++ b/nixos/tests/lightdm.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "lightdm";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ aszlig worldofpeace ];
@@ -18,12 +18,12 @@ import ./make-test.nix ({ pkgs, ...} : {
   testScript = { nodes, ... }: let
     user = nodes.machine.config.users.users.alice;
   in ''
-    startAll;
-    $machine->waitForText(qr/${user.description}/);
-    $machine->screenshot("lightdm");
-    $machine->sendChars("${user.password}\n");
-    $machine->waitForFile("/home/alice/.Xauthority");
-    $machine->succeed("xauth merge ~alice/.Xauthority");
-    $machine->waitForWindow("^IceWM ");
+    start_all()
+    machine.wait_for_text("${user.description}")
+    machine.screenshot("lightdm")
+    machine.send_chars("${user.password}\n")
+    machine.wait_for_file("${user.home}/.Xauthority")
+    machine.succeed("xauth merge ${user.home}/.Xauthority")
+    machine.wait_for_window("^IceWM ")
   '';
 })
diff --git a/nixos/tests/mpd.nix b/nixos/tests/mpd.nix
index ac2b810defe3..a992576808dc 100644
--- a/nixos/tests/mpd.nix
+++ b/nixos/tests/mpd.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }:
+import ./make-test.nix ({ pkgs, lib, ... }:
   let
     track = pkgs.fetchurl {
       # Sourced from http://freemusicarchive.org/music/Blue_Wave_Theory/Surf_Music_Month_Challenge/Skyhawk_Beach_fade_in
@@ -46,38 +46,51 @@ import ./make-test.nix ({ pkgs, ... }:
     };
 
   nodes =
-    { client = 
+    { client =
       { ... }: { };
 
       serverALSA =
-        { ... }: (mkServer {
-          mpd = defaultMpdCfg // {
-            network.listenAddress = "any";
-            extraConfig = ''
-              audio_output {
-                type "alsa"
-                name "ALSA"
-                mixer_type "null"
-              }
-            '';
-          };
-
-          musicService = with defaultMpdCfg; musicService { inherit user group musicDirectory; };
-        }) // { networking.firewall.allowedTCPPorts = [ 6600 ]; };
+        { ... }: lib.mkMerge [
+          (mkServer {
+            mpd = defaultMpdCfg // {
+              network.listenAddress = "any";
+              extraConfig = ''
+                audio_output {
+                  type "alsa"
+                  name "ALSA"
+                  mixer_type "null"
+                }
+              '';
+            };
+            musicService = with defaultMpdCfg; musicService { inherit user group musicDirectory; };
+          })
+          { networking.firewall.allowedTCPPorts = [ 6600 ]; }
+        ];
 
       serverPulseAudio =
-        { ... }: (mkServer {
-          mpd = defaultMpdCfg // {
-            extraConfig = ''
-              audio_output {
-                type "pulse"
-                name "The Pulse"
-              }
-            '';
-          };
-
-          musicService = with defaultCfg; musicService { inherit user group musicDirectory; };
-        }) // { hardware.pulseaudio.enable = true; };
+        { ... }: lib.mkMerge [
+          (mkServer {
+            mpd = defaultMpdCfg // {
+              extraConfig = ''
+                audio_output {
+                  type "pulse"
+                  name "The Pulse"
+                }
+              '';
+            };
+
+            musicService = with defaultCfg; musicService { inherit user group musicDirectory; };
+          })
+          {
+            hardware.pulseaudio = {
+              enable = true;
+              systemWide = true;
+              tcp.enable = true;
+              tcp.anonymousClients.allowAll = true;
+            };
+            systemd.services.mpd.environment.PULSE_SERVER = "localhost";
+          }
+        ];
     };
 
   testScript = ''
@@ -110,6 +123,7 @@ import ./make-test.nix ({ pkgs, ... }:
     play_some_music($serverALSA);
     play_some_music($serverPulseAudio);
 
+    $client->waitForUnit("multi-user.target");
     $client->succeed("$mpc -h serverALSA status");
 
     # The PulseAudio-based server is configured not to accept external client connections
diff --git a/nixos/tests/opensmtpd.nix b/nixos/tests/opensmtpd.nix
index 883ad7604941..e6f52db1d984 100644
--- a/nixos/tests/opensmtpd.nix
+++ b/nixos/tests/opensmtpd.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix {
+import ./make-test-python.nix {
   name = "opensmtpd";
 
   nodes = {
@@ -102,23 +102,23 @@ import ./make-test.nix {
   };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $client->waitForUnit("network-online.target");
-    $smtp1->waitForUnit('opensmtpd');
-    $smtp2->waitForUnit('opensmtpd');
-    $smtp2->waitForUnit('dovecot2');
+    client.wait_for_unit("network-online.target")
+    smtp1.wait_for_unit("opensmtpd")
+    smtp2.wait_for_unit("opensmtpd")
+    smtp2.wait_for_unit("dovecot2")
 
     # To prevent sporadic failures during daemon startup, make sure
     # services are listening on their ports before sending requests
-    $smtp1->waitForOpenPort(25);
-    $smtp2->waitForOpenPort(25);
-    $smtp2->waitForOpenPort(143);
+    smtp1.wait_for_open_port(25)
+    smtp2.wait_for_open_port(25)
+    smtp2.wait_for_open_port(143)
 
-    $client->succeed('send-a-test-mail');
-    $smtp1->waitUntilFails('smtpctl show queue | egrep .');
-    $smtp2->waitUntilFails('smtpctl show queue | egrep .');
-    $client->succeed('check-mail-landed >&2');
+    client.succeed("send-a-test-mail")
+    smtp1.wait_until_fails("smtpctl show queue | egrep .")
+    smtp2.wait_until_fails("smtpctl show queue | egrep .")
+    client.succeed("check-mail-landed >&2")
   '';
 
   meta.timeout = 30;
diff --git a/nixos/tests/powerdns.nix b/nixos/tests/powerdns.nix
index 8addcc784012..75d71315e644 100644
--- a/nixos/tests/powerdns.nix
+++ b/nixos/tests/powerdns.nix
@@ -1,12 +1,13 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "powerdns";
 
   nodes.server = { ... }: {
     services.powerdns.enable = true;
+    environment.systemPackages = [ pkgs.dnsutils ];
   };
 
   testScript = ''
-    $server->waitForUnit("pdns");
-    $server->succeed("${pkgs.dnsutils}/bin/dig version.bind txt chaos \@127.0.0.1");
+    server.wait_for_unit("pdns")
+    server.succeed("dig version.bind txt chaos \@127.0.0.1")
   '';
 })
diff --git a/nixos/tests/pppd.nix b/nixos/tests/pppd.nix
index 91f811859093..bda0aa75bb50 100644
--- a/nixos/tests/pppd.nix
+++ b/nixos/tests/pppd.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix (
+import ./make-test-python.nix (
   let
     chap-secrets = {
       text = ''"flynn" * "reindeerflotilla" *'';
@@ -53,10 +53,10 @@ import ./make-test.nix (
         environment.etc."ppp/chap-secrets" = chap-secrets;
       };
     };
-  
+
     testScript = ''
-      startAll;
-      $client->waitUntilSucceeds("ping -c1 -W1 192.0.2.1");
-      $server->waitUntilSucceeds("ping -c1 -W1 192.0.2.2");
+      start_all()
+      client.wait_until_succeeds("ping -c1 -W1 192.0.2.1")
+      server.wait_until_succeeds("ping -c1 -W1 192.0.2.2")
     '';
-  })  
+  })
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
index 76cecb7433a9..563f24726477 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -457,9 +457,7 @@ let
         wait_for_unit("prometheus-varnish-exporter.service")
         wait_for_open_port(6081)
         wait_for_open_port(9131)
-        succeed(
-            "curl -sSf http://localhost:9131/metrics | grep -q 'varnish_up 1'"
-        )
+        succeed("curl -sSf http://localhost:9131/metrics | grep -q 'varnish_up 1'")
       '';
     };
 
diff --git a/nixos/tests/samba.nix b/nixos/tests/samba.nix
index 2802e00a5b1a..142269752b34 100644
--- a/nixos/tests/samba.nix
+++ b/nixos/tests/samba.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }:
+import ./make-test-python.nix ({ pkgs, ... }:
 
 {
   name = "samba";
@@ -36,12 +36,12 @@ import ./make-test.nix ({ pkgs, ... }:
 
   testScript =
     ''
-      $server->start;
-      $server->waitForUnit("samba.target");
-      $server->succeed("mkdir -p /public; echo bar > /public/foo");
+      server.start()
+      server.wait_for_unit("samba.target")
+      server.succeed("mkdir -p /public; echo bar > /public/foo")
 
-      $client->start;
-      $client->waitForUnit("remote-fs.target");
-      $client->succeed("[[ \$(cat /public/foo) = bar ]]");
+      client.start()
+      client.wait_for_unit("remote-fs.target")
+      client.succeed("[[ $(cat /public/foo) = bar ]]")
     '';
 })
diff --git a/nixos/tests/sddm.nix b/nixos/tests/sddm.nix
index 678bcbeab20a..4bdcd701dcf1 100644
--- a/nixos/tests/sddm.nix
+++ b/nixos/tests/sddm.nix
@@ -3,7 +3,7 @@
   pkgs ? import ../.. { inherit system config; }
 }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
+with import ../lib/testing-python.nix { inherit system pkgs; };
 
 let
   inherit (pkgs) lib;
@@ -26,13 +26,13 @@ let
       testScript = { nodes, ... }: let
         user = nodes.machine.config.users.users.alice;
       in ''
-        startAll;
-        $machine->waitForText(qr/select your user/i);
-        $machine->screenshot("sddm");
-        $machine->sendChars("${user.password}\n");
-        $machine->waitForFile("/home/alice/.Xauthority");
-        $machine->succeed("xauth merge ~alice/.Xauthority");
-        $machine->waitForWindow("^IceWM ");
+        start_all()
+        machine.wait_for_text("(?i)select your user")
+        machine.screenshot("sddm")
+        machine.send_chars("${user.password}\n")
+        machine.wait_for_file("${user.home}/.Xauthority")
+        machine.succeed("xauth merge ${user.home}/.Xauthority")
+        machine.wait_for_window("^IceWM ")
       '';
     };
 
@@ -57,11 +57,13 @@ let
         services.xserver.desktopManager.default = "none";
       };
 
-      testScript = { ... }: ''
-        startAll;
-        $machine->waitForFile("/home/alice/.Xauthority");
-        $machine->succeed("xauth merge ~alice/.Xauthority");
-        $machine->waitForWindow("^IceWM ");
+      testScript = { nodes, ... }: let
+        user = nodes.machine.config.users.users.alice;
+      in ''
+        start_all()
+        machine.wait_for_file("${user.home}/.Xauthority")
+        machine.succeed("xauth merge ${user.home}/.Xauthority")
+        machine.wait_for_window("^IceWM ")
       '';
     };
   };
diff --git a/nixos/tests/shiori.nix b/nixos/tests/shiori.nix
index 0022a7220fe2..a5771262c6f2 100644
--- a/nixos/tests/shiori.nix
+++ b/nixos/tests/shiori.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ lib, ...}:
+import ./make-test-python.nix ({ pkgs, lib, ...}:
 
 {
   name = "shiori";
@@ -8,10 +8,74 @@ import ./make-test.nix ({ lib, ...}:
     { ... }:
     { services.shiori.enable = true; };
 
-  testScript = ''
-    $machine->waitForUnit('shiori.service');
-    $machine->waitForOpenPort('8080');
-    $machine->succeed("curl --fail http://localhost:8080/");
-    $machine->succeed("curl --fail --location http://localhost:8080/ | grep -qi shiori");
+  testScript = let
+    authJSON = pkgs.writeText "auth.json" (builtins.toJSON {
+      username = "shiori";
+      password = "gopher";
+      remember = 1; # hour
+      owner = true;
+    });
+
+  insertBookmark = {
+    url = "http://example.org";
+    title = "Example Bookmark";
+  };
+
+  insertBookmarkJSON = pkgs.writeText "insertBookmark.json" (builtins.toJSON insertBookmark);
+  in ''
+    import json
+
+    machine.wait_for_unit("shiori.service")
+    machine.wait_for_open_port(8080)
+    machine.succeed("curl --fail http://localhost:8080/")
+    machine.succeed("curl --fail --location http://localhost:8080/ | grep -qi shiori")
+
+    with subtest("login"):
+        auth_json = machine.succeed(
+            "curl --fail --location http://localhost:8080/api/login "
+            "-X POST -H 'Content-Type:application/json' -d @${authJSON}"
+        )
+        auth_ret = json.loads(auth_json)
+        session_id = auth_ret["session"]
+
+    with subtest("bookmarks"):
+        with subtest("first use no bookmarks"):
+            bookmarks_json = machine.succeed(
+                (
+                    "curl --fail --location http://localhost:8080/api/bookmarks "
+                    "-H 'X-Session-Id:{}'"
+                ).format(session_id)
+            )
+
+            if json.loads(bookmarks_json)["bookmarks"] != []:
+                raise Exception("Shiori have a bookmark on first use")
+
+        with subtest("insert bookmark"):
+            machine.succeed(
+                (
+                    "curl --fail --location http://localhost:8080/api/bookmarks "
+                    "-X POST -H 'X-Session-Id:{}' "
+                    "-H 'Content-Type:application/json' -d @${insertBookmarkJSON}"
+                ).format(session_id)
+            )
+
+        with subtest("get inserted bookmark"):
+            bookmarks_json = machine.succeed(
+                (
+                    "curl --fail --location http://localhost:8080/api/bookmarks "
+                    "-H 'X-Session-Id:{}'"
+                ).format(session_id)
+            )
+
+            bookmarks = json.loads(bookmarks_json)["bookmarks"]
+            if len(bookmarks) != 1:
+                raise Exception("Shiori didn't save the bookmark")
+
+            bookmark = bookmarks[0]
+            if (
+                bookmark["url"] != "${insertBookmark.url}"
+                or bookmark["title"] != "${insertBookmark.title}"
+            ):
+                raise Exception("Inserted bookmark doesn't have same URL or title")
   '';
 })
diff --git a/nixos/tests/slurm.nix b/nixos/tests/slurm.nix
index 4c2cd3c3d264..17527378cf0a 100644
--- a/nixos/tests/slurm.nix
+++ b/nixos/tests/slurm.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ lib, ... }:
+import ./make-test-python.nix ({ lib, ... }:
 let
     mungekey = "mungeverryweakkeybuteasytointegratoinatest";
 
@@ -54,10 +54,15 @@ in {
         networking.firewall.enable = false;
         services.slurm.dbdserver = {
           enable = true;
+          storagePass = "password123";
         };
         services.mysql = {
           enable = true;
-          package = pkgs.mysql;
+          package = pkgs.mariadb;
+          initialScript = pkgs.writeText "mysql-init.sql" ''
+            CREATE USER 'slurm'@'localhost' IDENTIFIED BY 'password123';
+            GRANT ALL PRIVILEGES ON slurm_acct_db.* TO 'slurm'@'localhost';
+          '';
           ensureDatabases = [ "slurm_acct_db" ];
           ensureUsers = [{
             ensurePermissions = { "slurm_acct_db.*" = "ALL PRIVILEGES"; };
@@ -80,63 +85,57 @@ in {
 
   testScript =
   ''
-  startAll;
+  start_all()
 
   # Set up authentification across the cluster
-  foreach my $node (($submit,$control,$dbd,$node1,$node2,$node3))
-  {
-    $node->waitForUnit("default.target");
+  for node in [submit, control, dbd, node1, node2, node3]:
 
-    $node->succeed("mkdir /etc/munge");
-    $node->succeed("echo '${mungekey}' > /etc/munge/munge.key");
-    $node->succeed("chmod 0400 /etc/munge/munge.key");
-    $node->succeed("chown munge:munge /etc/munge/munge.key");
-    $node->succeed("systemctl restart munged");
+      node.wait_for_unit("default.target")
+
+      node.succeed("mkdir /etc/munge")
+      node.succeed(
+          "echo '${mungekey}' > /etc/munge/munge.key"
+      )
+      node.succeed("chmod 0400 /etc/munge/munge.key")
+      node.succeed("chown munge:munge /etc/munge/munge.key")
+      node.succeed("systemctl restart munged")
+
+      node.wait_for_unit("munged")
 
-    $node->waitForUnit("munged");
-  };
 
   # Restart the services since they have probably failed due to the munge init
   # failure
-  subtest "can_start_slurmdbd", sub {
-    $dbd->succeed("systemctl restart slurmdbd");
-    $dbd->waitForUnit("slurmdbd.service");
-    $dbd->waitForOpenPort(6819);
-  };
+  with subtest("can_start_slurmdbd"):
+      dbd.succeed("systemctl restart slurmdbd")
+      dbd.wait_for_unit("slurmdbd.service")
+      dbd.wait_for_open_port(6819)
 
   # there needs to be an entry for the current
   # cluster in the database before slurmctld is restarted
-  subtest "add_account", sub {
-    $control->succeed("sacctmgr -i add cluster default");
-    # check for cluster entry
-    $control->succeed("sacctmgr list cluster | awk '{ print \$1 }' | grep default");
-  };
+  with subtest("add_account"):
+      control.succeed("sacctmgr -i add cluster default")
+      # check for cluster entry
+      control.succeed("sacctmgr list cluster | awk '{ print $1 }' | grep default")
 
-  subtest "can_start_slurmctld", sub {
-    $control->succeed("systemctl restart slurmctld");
-    $control->waitForUnit("slurmctld.service");
-  };
+  with subtest("can_start_slurmctld"):
+      control.succeed("systemctl restart slurmctld")
+      control.waitForUnit("slurmctld.service")
 
-  subtest "can_start_slurmd", sub {
-    foreach my $node (($node1,$node2,$node3))
-    {
-      $node->succeed("systemctl restart slurmd.service");
-      $node->waitForUnit("slurmd");
-    }
-  };
+  with subtest("can_start_slurmd"):
+      for node in [node1, node2, node3]:
+          node.succeed("systemctl restart slurmd.service")
+          node.wait_for_unit("slurmd")
 
   # Test that the cluster works and can distribute jobs;
 
-  subtest "run_distributed_command", sub {
-    # Run `hostname` on 3 nodes of the partition (so on all the 3 nodes).
-    # The output must contain the 3 different names
-    $submit->succeed("srun -N 3 hostname | sort | uniq | wc -l | xargs test 3 -eq");
-  };
+  with subtest("run_distributed_command"):
+      # Run `hostname` on 3 nodes of the partition (so on all the 3 nodes).
+      # The output must contain the 3 different names
+      submit.succeed("srun -N 3 hostname | sort | uniq | wc -l | xargs test 3 -eq")
 
-  subtest "check_slurm_dbd", sub {
-    # find the srun job from above in the database
-    sleep 5;
-    $control->succeed("sacct | grep hostname");
-  };
+      with subtest("check_slurm_dbd"):
+          # find the srun job from above in the database
+          control.succeed("sleep 5")
+          control.succeed("sacct | grep hostname")
   '';
 })
diff --git a/nixos/tests/smokeping.nix b/nixos/tests/smokeping.nix
index 07d228051127..4f8f0fcc9fe2 100644
--- a/nixos/tests/smokeping.nix
+++ b/nixos/tests/smokeping.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "smokeping";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ cransom ];
@@ -22,12 +22,12 @@ import ./make-test.nix ({ pkgs, ...} : {
   };
 
   testScript = ''
-    startAll;
-    $sm->waitForUnit("smokeping");
-    $sm->waitForUnit("thttpd");
-    $sm->waitForFile("/var/lib/smokeping/data/Local/LocalMachine.rrd");
-    $sm->succeed("curl -s -f localhost:8081/smokeping.fcgi?target=Local");
-    $sm->succeed("ls /var/lib/smokeping/cache/Local/LocalMachine_mini.png");
-    $sm->succeed("ls /var/lib/smokeping/cache/index.html");
+    start_all()
+    sm.wait_for_unit("smokeping")
+    sm.wait_for_unit("thttpd")
+    sm.wait_for_file("/var/lib/smokeping/data/Local/LocalMachine.rrd")
+    sm.succeed("curl -s -f localhost:8081/smokeping.fcgi?target=Local")
+    sm.succeed("ls /var/lib/smokeping/cache/Local/LocalMachine_mini.png")
+    sm.succeed("ls /var/lib/smokeping/cache/index.html")
   '';
 })
diff --git a/nixos/tests/tinydns.nix b/nixos/tests/tinydns.nix
index cb7ee0c5fb5e..c7740d5ade35 100644
--- a/nixos/tests/tinydns.nix
+++ b/nixos/tests/tinydns.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ lib, ...} : {
+import ./make-test-python.nix ({ lib, ...} : {
   name = "tinydns";
   meta = {
     maintainers = with lib.maintainers; [ basvandijk ];
@@ -19,8 +19,8 @@ import ./make-test.nix ({ lib, ...} : {
     };
   };
   testScript = ''
-    $nameserver->start;
-    $nameserver->waitForUnit("tinydns.service");
-    $nameserver->succeed("host bla.foo.bar | grep '1\.2\.3\.4'");
+    nameserver.start()
+    nameserver.wait_for_unit("tinydns.service")
+    nameserver.succeed("host bla.foo.bar | grep '1\.2\.3\.4'")
   '';
 })
diff --git a/nixos/tests/xmonad.nix b/nixos/tests/xmonad.nix
index 79c15ccffecd..c2e5ba60d7bc 100644
--- a/nixos/tests/xmonad.nix
+++ b/nixos/tests/xmonad.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "xmonad";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ nequissimus ];
@@ -21,19 +21,21 @@ import ./make-test.nix ({ pkgs, ...} : {
     };
   };
 
-  testScript = { ... }: ''
-    $machine->waitForX;
-    $machine->waitForFile("/home/alice/.Xauthority");
-    $machine->succeed("xauth merge ~alice/.Xauthority");
-    $machine->sendKeys("alt-ctrl-x");
-    $machine->waitForWindow(qr/alice.*machine/);
-    $machine->sleep(1);
-    $machine->screenshot("terminal");
-    $machine->waitUntilSucceeds("xmonad --restart");
-    $machine->sleep(3);
-    $machine->sendKeys("alt-shift-ret");
-    $machine->waitForWindow(qr/alice.*machine/);
-    $machine->sleep(1);
-    $machine->screenshot("terminal");
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+  in ''
+    machine.wait_for_x()
+    machine.wait_for_file("${user.home}/.Xauthority")
+    machine.succeed("xauth merge ${user.home}/.Xauthority")
+    machine.send_chars("alt-ctrl-x")
+    machine.wait_for_window("${user.name}.*machine")
+    machine.sleep(1)
+    machine.screenshot("terminal")
+    machine.wait_until_succeeds("xmonad --restart")
+    machine.sleep(3)
+    machine.send_chars("alt-shift-ret")
+    machine.wait_for_window("${user.name}.*machine")
+    machine.sleep(1)
+    machine.screenshot("terminal")
   '';
 })