summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/configuration/x-windows.xml3
-rw-r--r--nixos/doc/manual/configuration/xfce.xml5
-rw-r--r--nixos/doc/manual/installation/installing.xml2
-rw-r--r--nixos/doc/manual/man-nixos-option.xml9
-rw-r--r--nixos/doc/manual/release-notes/rl-2003.xml122
-rw-r--r--nixos/lib/make-ext4-fs.nix44
-rw-r--r--nixos/lib/testing-python.nix3
-rw-r--r--nixos/lib/testing.nix3
-rw-r--r--nixos/modules/config/console.nix203
-rw-r--r--nixos/modules/config/i18n.nix63
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix11
-rw-r--r--nixos/modules/installer/cd-dvd/sd-image.nix13
-rw-r--r--nixos/modules/installer/tools/nixos-generate-config.pl3
-rw-r--r--nixos/modules/module-list.nix5
-rw-r--r--nixos/modules/programs/oblogout.nix171
-rw-r--r--nixos/modules/programs/sway.nix3
-rw-r--r--nixos/modules/security/acme.nix4
-rw-r--r--nixos/modules/security/polkit.nix5
-rw-r--r--nixos/modules/services/admin/oxidized.nix1
-rw-r--r--nixos/modules/services/backup/bacula.nix166
-rw-r--r--nixos/modules/services/backup/postgresql-backup.nix2
-rw-r--r--nixos/modules/services/databases/postgresql.nix4
-rw-r--r--nixos/modules/services/development/lorri.nix2
-rw-r--r--nixos/modules/services/misc/mame.nix67
-rw-r--r--nixos/modules/services/misc/matrix-synapse.nix31
-rw-r--r--nixos/modules/services/networking/3proxy.nix424
-rw-r--r--nixos/modules/services/networking/firewall.nix15
-rw-r--r--nixos/modules/services/networking/helpers.nix11
-rw-r--r--nixos/modules/services/networking/nat.nix30
-rw-r--r--nixos/modules/services/networking/unbound.nix13
-rw-r--r--nixos/modules/services/torrent/transmission.nix2
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix31
-rw-r--r--nixos/modules/services/web-servers/nginx/location-options.nix2
-rw-r--r--nixos/modules/services/web-servers/unit/default.nix28
-rw-r--r--nixos/modules/services/x11/desktop-managers/default.nix23
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome3.nix4
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix8
-rw-r--r--nixos/modules/services/x11/desktop-managers/surf-display.nix2
-rw-r--r--nixos/modules/services/x11/display-managers/account-service-util.nix39
-rw-r--r--nixos/modules/services/x11/display-managers/default.nix251
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix57
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix5
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix3
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix35
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix20
-rw-r--r--nixos/modules/services/x11/imwheel.nix68
-rw-r--r--nixos/modules/services/x11/window-managers/default.nix15
-rw-r--r--nixos/modules/tasks/filesystems/nfs.nix6
-rw-r--r--nixos/modules/tasks/kbd.nix127
-rw-r--r--nixos/modules/tasks/network-interfaces-systemd.nix4
-rw-r--r--nixos/modules/virtualisation/container-config.nix1
-rw-r--r--nixos/modules/virtualisation/lxd.nix28
-rw-r--r--nixos/release-combined.nix4
-rw-r--r--nixos/tests/3proxy.nix162
-rw-r--r--nixos/tests/all-tests.nix8
-rw-r--r--nixos/tests/common/x11.nix8
-rw-r--r--nixos/tests/gitea.nix134
-rw-r--r--nixos/tests/gnome3-xorg.nix2
-rw-r--r--nixos/tests/hadoop/hdfs.nix26
-rw-r--r--nixos/tests/hadoop/yarn.nix24
-rw-r--r--nixos/tests/haproxy.nix16
-rw-r--r--nixos/tests/hitch/default.nix12
-rw-r--r--nixos/tests/i3wm.nix2
-rw-r--r--nixos/tests/initrd-network.nix8
-rw-r--r--nixos/tests/leaps.nix12
-rw-r--r--nixos/tests/lidarr.nix10
-rw-r--r--nixos/tests/lightdm.nix3
-rw-r--r--nixos/tests/mailcatcher.nix16
-rw-r--r--nixos/tests/mutable-users.nix28
-rw-r--r--nixos/tests/mxisd.nix17
-rw-r--r--nixos/tests/nesting.nix36
-rw-r--r--nixos/tests/networking.nix473
-rw-r--r--nixos/tests/nfs.nix90
-rw-r--r--nixos/tests/nfs/default.nix9
-rw-r--r--nixos/tests/nfs/kerberos.nix133
-rw-r--r--nixos/tests/nfs/simple.nix94
-rw-r--r--nixos/tests/nghttpx.nix10
-rw-r--r--nixos/tests/novacomd.nix32
-rw-r--r--nixos/tests/nzbget.nix18
-rw-r--r--nixos/tests/orangefs.nix52
-rw-r--r--nixos/tests/osrm-backend.nix14
-rw-r--r--nixos/tests/overlayfs.nix77
-rw-r--r--nixos/tests/paperless.nix29
-rw-r--r--nixos/tests/pdns-recursor.nix6
-rw-r--r--nixos/tests/peerflix.nix8
-rw-r--r--nixos/tests/pgmanage.nix12
-rw-r--r--nixos/tests/php-pcre.nix9
-rw-r--r--nixos/tests/plasma5.nix2
-rw-r--r--nixos/tests/postgis.nix12
-rw-r--r--nixos/tests/quagga.nix28
-rw-r--r--nixos/tests/sddm.nix6
-rw-r--r--nixos/tests/sonarr.nix8
-rw-r--r--nixos/tests/switch-test.nix10
-rw-r--r--nixos/tests/systemd-analyze.nix46
-rw-r--r--nixos/tests/systemd-timesyncd.nix24
-rw-r--r--nixos/tests/wireguard/namespaces.nix18
-rw-r--r--nixos/tests/xmonad.nix8
-rw-r--r--nixos/tests/zsh-history.nix35
98 files changed, 2635 insertions, 1353 deletions
diff --git a/nixos/doc/manual/configuration/x-windows.xml b/nixos/doc/manual/configuration/x-windows.xml
index 9206f43ea39..55ad9fe6e65 100644
--- a/nixos/doc/manual/configuration/x-windows.xml
+++ b/nixos/doc/manual/configuration/x-windows.xml
@@ -83,8 +83,7 @@
   desktop environment. If you wanted no desktop environment and i3 as your your
   window manager, you'd define:
 <programlisting>
-<xref linkend="opt-services.xserver.desktopManager.default"/> = "none";
-<xref linkend="opt-services.xserver.windowManager.default"/> = "i3";
+<xref linkend="opt-services.xserver.displayManager.defaultSession"/> = "none+i3";
 </programlisting>
   And, finally, to enable auto-login for a user <literal>johndoe</literal>:
 <programlisting>
diff --git a/nixos/doc/manual/configuration/xfce.xml b/nixos/doc/manual/configuration/xfce.xml
index 6ac99c6b2be..027828bb936 100644
--- a/nixos/doc/manual/configuration/xfce.xml
+++ b/nixos/doc/manual/configuration/xfce.xml
@@ -7,9 +7,8 @@
  <para>
   To enable the Xfce Desktop Environment, set
 <programlisting>
-<link linkend="opt-services.xserver.desktopManager.default">services.xserver.desktopManager</link> = {
-  <link linkend="opt-services.xserver.desktopManager.xfce.enable">xfce.enable</link> = true;
-  <link linkend="opt-services.xserver.desktopManager.default">default</link> = "xfce";
+<xref linkend="opt-services.xserver.desktopManager.xfce.enable" /> = true;
+<xref linkend="opt-services.xserver.displayManager.defaultSession" /> = "xfce";
 };
 </programlisting>
  </para>
diff --git a/nixos/doc/manual/installation/installing.xml b/nixos/doc/manual/installation/installing.xml
index 5655295871e..4041b4ad163 100644
--- a/nixos/doc/manual/installation/installing.xml
+++ b/nixos/doc/manual/installation/installing.xml
@@ -478,7 +478,7 @@ Retype new UNIX password: ***</screen>
      shows what packages are available, and
 <screen>
 <prompt>$ </prompt>nix-env -f '&lt;nixpkgs&gt;' -iA w3m</screen>
-     install the <literal>w3m</literal> browser.
+     installs the <literal>w3m</literal> browser.
     </para>
    </listitem>
   </orderedlist>
diff --git a/nixos/doc/manual/man-nixos-option.xml b/nixos/doc/manual/man-nixos-option.xml
index beabf020c92..b82f3125609 100644
--- a/nixos/doc/manual/man-nixos-option.xml
+++ b/nixos/doc/manual/man-nixos-option.xml
@@ -119,4 +119,13 @@ Defined by:
    bug, please report to Nicolas Pierron.
   </para>
  </refsection>
+ <refsection>
+  <title>See also</title>
+  <para>
+   <citerefentry>
+    <refentrytitle>configuration.nix</refentrytitle>
+    <manvolnum>5</manvolnum>
+   </citerefentry>
+  </para>
+ </refsection>
 </refentry>
diff --git a/nixos/doc/manual/release-notes/rl-2003.xml b/nixos/doc/manual/release-notes/rl-2003.xml
index 886b16ef965..5744de96b74 100644
--- a/nixos/doc/manual/release-notes/rl-2003.xml
+++ b/nixos/doc/manual/release-notes/rl-2003.xml
@@ -55,6 +55,19 @@
       and adding a <option>--all</option> option which prints all options and their values.
     </para>
    </listitem>
+   <listitem>
+    <para>
+     <option>services.xserver.desktopManager.default</option> and <option>services.xserver.windowManager.default</option> options were replaced by a single <xref linkend="opt-services.xserver.displayManager.defaultSession"/> option to improve support for upstream session files. If you used something like:
+<programlisting>
+services.xserver.desktopManager.default = "xfce";
+services.xserver.windowManager.default = "icewm";
+</programlisting>
+     you should change it to:
+<programlisting>
+services.xserver.displayManager.defaultSession = "xfce+icewm";
+</programlisting>
+    </para>
+   </listitem>
   </itemizedlist>
  </section>
 
@@ -127,18 +140,18 @@
    </listitem>
    <listitem>
     <para>
-      The <literal>99-main.network</literal> file was removed. Maching all
-      network interfaces caused many breakages, see
-      <link xlink:href="https://github.com/NixOS/nixpkgs/pull/18962">#18962</link>
-        and <link xlink:href="https://github.com/NixOS/nixpkgs/pull/71106">#71106</link>.
+     The <literal>99-main.network</literal> file was removed. Maching all
+     network interfaces caused many breakages, see
+     <link xlink:href="https://github.com/NixOS/nixpkgs/pull/18962">#18962</link>
+       and <link xlink:href="https://github.com/NixOS/nixpkgs/pull/71106">#71106</link>.
     </para>
     <para>
-      We already don't support the global <link linkend="opt-networking.useDHCP">networking.useDHCP</link>,
-      <link linkend="opt-networking.defaultGateway">networking.defaultGateway</link> and
-      <link linkend="opt-networking.defaultGateway6">networking.defaultGateway6</link> options
-      if <link linkend="opt-networking.useNetworkd">networking.useNetworkd</link> is enabled,
-      but direct users to configure the per-device
-      <link linkend="opt-networking.interfaces">networking.interfaces.&lt;name&gt;.…</link> options.
+     We already don't support the global <link linkend="opt-networking.useDHCP">networking.useDHCP</link>,
+     <link linkend="opt-networking.defaultGateway">networking.defaultGateway</link> and
+     <link linkend="opt-networking.defaultGateway6">networking.defaultGateway6</link> options
+     if <link linkend="opt-networking.useNetworkd">networking.useNetworkd</link> is enabled,
+     but direct users to configure the per-device
+     <link linkend="opt-networking.interfaces">networking.interfaces.&lt;name&gt;.…</link> options.
     </para>
    </listitem>
    <listitem>
@@ -204,6 +217,14 @@
       The <literal>buildRustCrate</literal> infrastructure now produces <literal>lib</literal> outputs in addition to the <literal>out</literal> output.
       This has led to drastically reduced closed sizes for some rust crates since development dependencies are now in the <literal>lib</literal> output.
     </para>
+    </listitem>
+   <listitem>
+    <para>
+     Pango was upgraded to 1.44, which no longer uses freetype for font loading.  This means that type1
+     and bitmap fonts are no longer supported in applications relying on Pango for font rendering
+     (notably, GTK application). See <link xlink:href="https://gitlab.gnome.org/GNOME/pango/issues/386">
+     upstream issue</link> for more information.
+    </para>
    </listitem>
    <listitem>
     <para>
@@ -225,6 +246,75 @@
      The fourStore and fourStoreEndpoint modules have been removed.
     </para>
    </listitem>
+   <listitem>
+    <para>
+     Polkit no longer has the user of uid 0 (root) as an admin identity.
+     We now follow the upstream default of only having every member of the wheel
+     group admin privileged. Before it was root and members of wheel.
+     The positive outcome of this is pkexec GUI popups or terminal prompts
+     will no longer require the user to choose between two essentially equivalent
+     choices (whether to perform the action as themselves with wheel permissions, or as the root user).
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     NixOS containers no longer build NixOS manual by default. This saves evaluation time,
+     especially if there are many declarative containers defined. Note that this is already done
+     when <literal>&lt;nixos/modules/profiles/minimal.nix&gt;</literal> module is included
+     in container config.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
+     Virtual console options have been reorganized and can be found under
+     a single top-level attribute: <literal>console</literal>.
+     The full set of changes is as follows:
+    </para>
+    <itemizedlist>
+      <listitem>
+       <para>
+         <literal>i18n.consoleFont</literal> renamed to
+         <link linkend="opt-console.font">console.font</link>
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         <literal>i18n.consoleKeyMap</literal> renamed to
+         <link linkend="opt-console.keyMap">console.keyMap</link>
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         <literal>i18n.consoleColors</literal> renamed to
+         <link linkend="opt-console.colors">console.colors</link>
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         <literal>i18n.consolePackages</literal> renamed to
+         <link linkend="opt-console.packages">console.packages</link>
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         <literal>i18n.consoleUseXkbConfig</literal> renamed to
+         <link linkend="opt-console.useXkbConfig">console.useXkbConfig</link>
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         <literal>boot.earlyVconsoleSetup</literal> renamed to
+         <link linkend="opt-console.earlySetup">console.earlySetup</link>
+       </para>
+      </listitem>
+      <listitem>
+       <para>
+         <literal>boot.extraTTYs</literal> renamed to
+         <link linkend="opt-console.extraTTYs">console.extraTTYs</link>
+       </para>
+      </listitem>
+    </itemizedlist>
+   </listitem>
   </itemizedlist>
  </section>
 
@@ -241,6 +331,18 @@
    </listitem>
    <listitem>
     <para>
+     The nginx web server previously started its master process as root
+     privileged, then ran worker processes as a less privileged identity user.
+     This was changed to start all of nginx as a less privileged user (defined by
+     <literal>services.nginx.user</literal> and
+     <literal>services.nginx.group</literal>). As a consequence, all files that
+     are needed for nginx to run (included configuration fragments, SSL
+     certificates and keys, etc.) must now be readable by this less privileged
+     user/group.
+    </para>
+   </listitem>
+   <listitem>
+    <para>
      OpenSSH has been upgraded from 7.9 to 8.1, improving security and adding features
      but with potential incompatibilities.  Consult the
      <link xlink:href="https://www.openssh.com/txt/release-8.1">
diff --git a/nixos/lib/make-ext4-fs.nix b/nixos/lib/make-ext4-fs.nix
index 932adcd9796..f46d3990c06 100644
--- a/nixos/lib/make-ext4-fs.nix
+++ b/nixos/lib/make-ext4-fs.nix
@@ -4,8 +4,11 @@
 # generated image is sized to only fit its contents, with the expectation
 # that a script resizes the filesystem at boot time.
 { pkgs
+, lib
 # List of derivations to be included
 , storePaths
+# Whether or not to compress the resulting image with zstd
+, compressImage ? false, zstd
 # Shell commands to populate the ./files directory.
 # All files in that directory are copied to the root of the FS.
 , populateImageCommands ? ""
@@ -20,18 +23,20 @@
 let
   sdClosureInfo = pkgs.buildPackages.closureInfo { rootPaths = storePaths; };
 in
-
 pkgs.stdenv.mkDerivation {
-  name = "ext4-fs.img";
+  name = "ext4-fs.img${lib.optionalString compressImage ".zst"}";
 
-  nativeBuildInputs = [e2fsprogs.bin libfaketime perl lkl];
+  nativeBuildInputs = [ e2fsprogs.bin libfaketime perl lkl ]
+  ++ lib.optional compressImage zstd;
 
   buildCommand =
     ''
+      ${if compressImage then "img=temp.img" else "img=$out"}
       (
       mkdir -p ./files
       ${populateImageCommands}
       )
+
       # Add the closures of the top-level store objects.
       storePaths=$(cat ${sdClosureInfo}/store-paths)
 
@@ -42,28 +47,26 @@ pkgs.stdenv.mkDerivation {
       bytes=$((2 * 4096 * $numInodes + 4096 * $numDataBlocks))
       echo "Creating an EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks)"
 
-      truncate -s $bytes $out
-      faketime -f "1970-01-01 00:00:01" mkfs.ext4 -L ${volumeLabel} -U ${uuid} $out
+      truncate -s $bytes $img
+      faketime -f "1970-01-01 00:00:01" mkfs.ext4 -L ${volumeLabel} -U ${uuid} $img
 
       # Also include a manifest of the closures in a format suitable for nix-store --load-db.
       cp ${sdClosureInfo}/registration nix-path-registration
-      cptofs -t ext4 -i $out nix-path-registration /
+      cptofs -t ext4 -i $img nix-path-registration /
 
       # Create nix/store before copying paths
       faketime -f "1970-01-01 00:00:01" mkdir -p nix/store
-      cptofs -t ext4 -i $out nix /
+      cptofs -t ext4 -i $img nix /
 
       echo "copying store paths to image..."
-      cptofs -t ext4 -i $out $storePaths /nix/store/
+      cptofs -t ext4 -i $img $storePaths /nix/store/
 
-      (
       echo "copying files to image..."
-      cd ./files
-      cptofs -t ext4 -i $out ./* /
-      )
+      cptofs -t ext4 -i $img ./files/* /
+
 
       # I have ended up with corrupted images sometimes, I suspect that happens when the build machine's disk gets full during the build.
-      if ! fsck.ext4 -n -f $out; then
+      if ! fsck.ext4 -n -f $img; then
         echo "--- Fsck failed for EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks) ---"
         cat errorlog
         return 1
@@ -71,9 +74,9 @@ pkgs.stdenv.mkDerivation {
 
       (
         # Resizes **snugly** to its actual limits (or closer to)
-        free=$(dumpe2fs $out | grep '^Free blocks:')
-        blocksize=$(dumpe2fs $out | grep '^Block size:')
-        blocks=$(dumpe2fs $out | grep '^Block count:')
+        free=$(dumpe2fs $img | grep '^Free blocks:')
+        blocksize=$(dumpe2fs $img | grep '^Block size:')
+        blocks=$(dumpe2fs $img | grep '^Block count:')
         blocks=$((''${blocks##*:})) # format the number.
         blocksize=$((''${blocksize##*:})) # format the number.
         # System can't boot with 0 blocks free.
@@ -82,10 +85,15 @@ pkgs.stdenv.mkDerivation {
         size=$(( blocks - ''${free##*:} + fudge ))
 
         echo "Resizing from $blocks blocks to $size blocks. (~ $((size*blocksize/1024/1024))MiB)"
-        EXT2FS_NO_MTAB_OK=yes resize2fs $out -f $size
+        EXT2FS_NO_MTAB_OK=yes resize2fs $img -f $size
       )
 
       # And a final fsck, because of the previous truncating.
-      fsck.ext4 -n -f $out
+      fsck.ext4 -n -f $img
+
+      if [ ${builtins.toString compressImage} ]; then
+        echo "Compressing image"
+        zstd -v --no-progress ./$img -o $out
+      fi
     '';
 }
diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix
index d567d268765..c4eb9328b1d 100644
--- a/nixos/lib/testing-python.nix
+++ b/nixos/lib/testing-python.nix
@@ -262,9 +262,8 @@ in rec {
           virtualisation.memorySize = 1024;
           services.xserver.enable = true;
           services.xserver.displayManager.auto.enable = true;
-          services.xserver.windowManager.default = "icewm";
+          services.xserver.displayManager.defaultSession = "none+icewm";
           services.xserver.windowManager.icewm.enable = true;
-          services.xserver.desktopManager.default = "none";
         };
     in
       runInMachine ({
diff --git a/nixos/lib/testing.nix b/nixos/lib/testing.nix
index a5f060a8d8e..ae8ecd6270c 100644
--- a/nixos/lib/testing.nix
+++ b/nixos/lib/testing.nix
@@ -249,9 +249,8 @@ in rec {
           virtualisation.memorySize = 1024;
           services.xserver.enable = true;
           services.xserver.displayManager.auto.enable = true;
-          services.xserver.windowManager.default = "icewm";
+          services.xserver.displayManager.defaultSession = "none+icewm";
           services.xserver.windowManager.icewm.enable = true;
-          services.xserver.desktopManager.default = "none";
         };
     in
       runInMachine ({
diff --git a/nixos/modules/config/console.nix b/nixos/modules/config/console.nix
new file mode 100644
index 00000000000..f662ed62d31
--- /dev/null
+++ b/nixos/modules/config/console.nix
@@ -0,0 +1,203 @@
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.console;
+
+  makeColor = i: concatMapStringsSep "," (x: "0x" + substring (2*i) 2 x);
+
+  isUnicode = hasSuffix "UTF-8" (toUpper config.i18n.defaultLocale);
+
+  optimizedKeymap = pkgs.runCommand "keymap" {
+    nativeBuildInputs = [ pkgs.buildPackages.kbd ];
+    LOADKEYS_KEYMAP_PATH = "${consoleEnv}/share/keymaps/**";
+    preferLocalBuild = true;
+  } ''
+    loadkeys -b ${optionalString isUnicode "-u"} "${cfg.keyMap}" > $out
+  '';
+
+  # Sadly, systemd-vconsole-setup doesn't support binary keymaps.
+  vconsoleConf = pkgs.writeText "vconsole.conf" ''
+    KEYMAP=${cfg.keyMap}
+    FONT=${cfg.font}
+  '';
+
+  consoleEnv = pkgs.buildEnv {
+    name = "console-env";
+    paths = [ pkgs.kbd ] ++ cfg.packages;
+    pathsToLink = [
+      "/share/consolefonts"
+      "/share/consoletrans"
+      "/share/keymaps"
+      "/share/unimaps"
+    ];
+  };
+
+  setVconsole = !config.boot.isContainer;
+in
+
+{
+  ###### interface
+
+  options.console  = {
+    font = mkOption {
+      type = types.str;
+      default = "Lat2-Terminus16";
+      example = "LatArCyrHeb-16";
+      description = ''
+        The font used for the virtual consoles.  Leave empty to use
+        whatever the <command>setfont</command> program considers the
+        default font.
+      '';
+    };
+
+    keyMap = mkOption {
+      type = with types; either str path;
+      default = "us";
+      example = "fr";
+      description = ''
+        The keyboard mapping table for the virtual consoles.
+      '';
+    };
+
+    colors = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = [
+        "002b36" "dc322f" "859900" "b58900"
+        "268bd2" "d33682" "2aa198" "eee8d5"
+        "002b36" "cb4b16" "586e75" "657b83"
+        "839496" "6c71c4" "93a1a1" "fdf6e3"
+      ];
+      description = ''
+        The 16 colors palette used by the virtual consoles.
+        Leave empty to use the default colors.
+        Colors must be in hexadecimal format and listed in
+        order from color 0 to color 15.
+      '';
+
+    };
+
+    packages = mkOption {
+      type = types.listOf types.package;
+      default = with pkgs.kbdKeymaps; [ dvp neo ];
+      defaultText = ''with pkgs.kbdKeymaps; [ dvp neo ]'';
+      description = ''
+        List of additional packages that provide console fonts, keymaps and
+        other resources for virtual consoles use.
+      '';
+    };
+
+    extraTTYs = mkOption {
+      default = [];
+      type = types.listOf types.str;
+      example = ["tty8" "tty9"];
+      description = ''
+        TTY (virtual console) devices, in addition to the consoles on
+        which mingetty and syslogd run, that must be initialised.
+        Only useful if you have some program that you want to run on
+        some fixed console.  For example, the NixOS installation CD
+        opens the manual in a web browser on console 7, so it sets
+        <option>console.extraTTYs</option> to <literal>["tty7"]</literal>.
+      '';
+    };
+
+    useXkbConfig = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        If set, configure the virtual console keymap from the xserver
+        keyboard settings.
+      '';
+    };
+
+    earlySetup = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Enable setting virtual console options as early as possible (in initrd).
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkMerge [
+    { console.keyMap = with config.services.xserver;
+        mkIf cfg.useXkbConfig
+          (pkgs.runCommand "xkb-console-keymap" { preferLocalBuild = true; } ''
+            '${pkgs.ckbcomp}/bin/ckbcomp' -model '${xkbModel}' -layout '${layout}' \
+              -option '${xkbOptions}' -variant '${xkbVariant}' > "$out"
+          '');
+    }
+
+    (mkIf (!setVconsole) {
+      systemd.services.systemd-vconsole-setup.enable = false;
+    })
+
+    (mkIf setVconsole (mkMerge [
+      { environment.systemPackages = [ pkgs.kbd ];
+
+        # Let systemd-vconsole-setup.service do the work of setting up the
+        # virtual consoles.
+        environment.etc."vconsole.conf".source = vconsoleConf;
+        # Provide kbd with additional packages.
+        environment.etc.kbd.source = "${consoleEnv}/share";
+
+        boot.initrd.preLVMCommands = mkBefore ''
+          kbd_mode ${if isUnicode then "-u" else "-a"} -C /dev/console
+          printf "\033%%${if isUnicode then "G" else "@"}" >> /dev/console
+          loadkmap < ${optimizedKeymap}
+
+          ${optionalString cfg.earlySetup ''
+            setfont -C /dev/console $extraUtils/share/consolefonts/font.psf
+          ''}
+        '';
+
+        systemd.services.systemd-vconsole-setup =
+          { before = [ "display-manager.service" ];
+            after = [ "systemd-udev-settle.service" ];
+            restartTriggers = [ vconsoleConf consoleEnv ];
+          };
+      }
+
+      (mkIf (cfg.colors != []) {
+        boot.kernelParams = [
+          "vt.default_red=${makeColor 0 cfg.colors}"
+          "vt.default_grn=${makeColor 1 cfg.colors}"
+          "vt.default_blu=${makeColor 2 cfg.colors}"
+        ];
+      })
+
+      (mkIf cfg.earlySetup {
+        boot.initrd.extraUtilsCommands = ''
+          mkdir -p $out/share/consolefonts
+          ${if substring 0 1 cfg.font == "/" then ''
+            font="${cfg.font}"
+          '' else ''
+            font="$(echo ${consoleEnv}/share/consolefonts/${cfg.font}.*)"
+          ''}
+          if [[ $font == *.gz ]]; then
+            gzip -cd $font > $out/share/consolefonts/font.psf
+          else
+            cp -L $font $out/share/consolefonts/font.psf
+          fi
+        '';
+      })
+    ]))
+  ];
+
+  imports = [
+    (mkRenamedOptionModule [ "i18n" "consoleFont" ] [ "console" "font" ])
+    (mkRenamedOptionModule [ "i18n" "consoleKeyMap" ] [ "console" "keyMap" ])
+    (mkRenamedOptionModule [ "i18n" "consoleColors" ] [ "console" "colors" ])
+    (mkRenamedOptionModule [ "i18n" "consolePackages" ] [ "console" "packages" ])
+    (mkRenamedOptionModule [ "i18n" "consoleUseXkbConfig" ] [ "console" "useXkbConfig" ])
+    (mkRenamedOptionModule [ "boot" "earlyVconsoleSetup" ] [ "console" "earlySetup" ])
+    (mkRenamedOptionModule [ "boot" "extraTTYs" ] [ "console" "extraTTYs" ])
+  ];
+}
diff --git a/nixos/modules/config/i18n.nix b/nixos/modules/config/i18n.nix
index d0db8fedecd..45691f4839c 100644
--- a/nixos/modules/config/i18n.nix
+++ b/nixos/modules/config/i18n.nix
@@ -58,62 +58,6 @@ with lib;
         '';
       };
 
-      consolePackages = mkOption {
-        type = types.listOf types.package;
-        default = with pkgs.kbdKeymaps; [ dvp neo ];
-        defaultText = ''with pkgs.kbdKeymaps; [ dvp neo ]'';
-        description = ''
-          List of additional packages that provide console fonts, keymaps and
-          other resources.
-        '';
-      };
-
-      consoleFont = mkOption {
-        type = types.str;
-        default = "Lat2-Terminus16";
-        example = "LatArCyrHeb-16";
-        description = ''
-          The font used for the virtual consoles.  Leave empty to use
-          whatever the <command>setfont</command> program considers the
-          default font.
-        '';
-      };
-
-      consoleUseXkbConfig = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          If set, configure the console keymap from the xserver keyboard
-          settings.
-        '';
-      };
-
-      consoleKeyMap = mkOption {
-        type = with types; either str path;
-        default = "us";
-        example = "fr";
-        description = ''
-          The keyboard mapping table for the virtual consoles.
-        '';
-      };
-
-      consoleColors = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        example = [
-          "002b36" "dc322f" "859900" "b58900"
-          "268bd2" "d33682" "2aa198" "eee8d5"
-          "002b36" "cb4b16" "586e75" "657b83"
-          "839496" "6c71c4" "93a1a1" "fdf6e3"
-        ];
-        description = ''
-          The 16 colors palette used by the virtual consoles.
-          Leave empty to use the default colors.
-          Colors must be in hexadecimal format and listed in
-          order from color 0 to color 15.
-        '';
-      };
-
     };
 
   };
@@ -123,13 +67,6 @@ with lib;
 
   config = {
 
-    i18n.consoleKeyMap = with config.services.xserver;
-      mkIf config.i18n.consoleUseXkbConfig
-        (pkgs.runCommand "xkb-console-keymap" { preferLocalBuild = true; } ''
-          '${pkgs.ckbcomp}/bin/ckbcomp' -model '${xkbModel}' -layout '${layout}' \
-            -option '${xkbOptions}' -variant '${xkbVariant}' > "$out"
-        '');
-
     environment.systemPackages =
       optional (config.i18n.supportedLocales != []) config.i18n.glibcLocales;
 
diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index 009f1e2c543..11319e5f4f8 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -603,9 +603,6 @@ in
         { source = config.system.build.squashfsStore;
           target = "/nix-store.squashfs";
         }
-        { source = config.isoImage.efiSplashImage;
-          target = "/EFI/boot/efi-background.png";
-        }
         { source = config.isoImage.splashImage;
           target = "/isolinux/background.png";
         }
@@ -630,8 +627,8 @@ in
         { source = "${efiDir}/EFI";
           target = "/EFI";
         }
-        { source = pkgs.writeText "loopback.cfg" "source /EFI/boot/grub.cfg";
-          target = "/boot/grub/loopback.cfg";
+        { source = (pkgs.writeTextDir "grub/loopback.cfg" "source /EFI/boot/grub.cfg") + "/grub";
+          target = "/boot/grub";
         }
       ] ++ optionals (config.boot.loader.grub.memtest86.enable && canx86BiosBoot) [
         { source = "${pkgs.memtest86plus}/memtest.bin";
@@ -641,6 +638,10 @@ in
         { source = config.isoImage.grubTheme;
           target = "/EFI/boot/grub-theme";
         }
+      ] ++ [
+        { source = config.isoImage.efiSplashImage;
+          target = "/EFI/boot/efi-background.png";
+        }
       ];
 
     boot.loader.timeout = 10;
diff --git a/nixos/modules/installer/cd-dvd/sd-image.nix b/nixos/modules/installer/cd-dvd/sd-image.nix
index 7865b767f0b..901c60befb6 100644
--- a/nixos/modules/installer/cd-dvd/sd-image.nix
+++ b/nixos/modules/installer/cd-dvd/sd-image.nix
@@ -18,6 +18,7 @@ with lib;
 let
   rootfsImage = pkgs.callPackage ../../../lib/make-ext4-fs.nix ({
     inherit (config.sdImage) storePaths;
+    compressImage = true;
     populateImageCommands = config.sdImage.populateRootCommands;
     volumeLabel = "NIXOS_SD";
   } // optionalAttrs (config.sdImage.rootPartitionUUID != null) {
@@ -128,10 +129,11 @@ in
 
     sdImage.storePaths = [ config.system.build.toplevel ];
 
-    system.build.sdImage = pkgs.callPackage ({ stdenv, dosfstools, e2fsprogs, mtools, libfaketime, utillinux, bzip2 }: stdenv.mkDerivation {
+    system.build.sdImage = pkgs.callPackage ({ stdenv, dosfstools, e2fsprogs,
+    mtools, libfaketime, utillinux, bzip2, zstd }: stdenv.mkDerivation {
       name = config.sdImage.imageName;
 
-      nativeBuildInputs = [ dosfstools e2fsprogs mtools libfaketime utillinux bzip2 ];
+      nativeBuildInputs = [ dosfstools e2fsprogs mtools libfaketime utillinux bzip2 zstd ];
 
       inherit (config.sdImage) compressImage;
 
@@ -146,11 +148,14 @@ in
           echo "file sd-image $img" >> $out/nix-support/hydra-build-products
         fi
 
+        echo "Decompressing rootfs image"
+        zstd -d --no-progress "${rootfsImage}" -o ./root-fs.img
+
         # Gap in front of the first partition, in MiB
         gap=8
 
         # Create the image file sized to fit /boot/firmware and /, plus slack for the gap.
-        rootSizeBlocks=$(du -B 512 --apparent-size ${rootfsImage} | awk '{ print $1 }')
+        rootSizeBlocks=$(du -B 512 --apparent-size ./root-fs.img | awk '{ print $1 }')
         firmwareSizeBlocks=$((${toString config.sdImage.firmwareSize} * 1024 * 1024 / 512))
         imageSize=$((rootSizeBlocks * 512 + firmwareSizeBlocks * 512 + gap * 1024 * 1024))
         truncate -s $imageSize $img
@@ -168,7 +173,7 @@ in
 
         # Copy the rootfs into the SD image
         eval $(partx $img -o START,SECTORS --nr 2 --pairs)
-        dd conv=notrunc if=${rootfsImage} of=$img seek=$START count=$SECTORS
+        dd conv=notrunc if=./root-fs.img of=$img seek=$START count=$SECTORS
 
         # Create a FAT32 /boot/firmware partition of suitable size into firmware_part.img
         eval $(partx $img -o START,SECTORS --nr 1 --pairs)
diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl
index f2ffe61c42c..7f98756a7d9 100644
--- a/nixos/modules/installer/tools/nixos-generate-config.pl
+++ b/nixos/modules/installer/tools/nixos-generate-config.pl
@@ -335,6 +335,9 @@ if (@swaps) {
         next unless -e $swapFilename;
         my $dev = findStableDevPath $swapFilename;
         if ($swapType =~ "partition") {
+            # zram devices are more likely created by configuration.nix, so
+            # ignore them here
+            next if ($swapFilename =~ /^\/dev\/zram/);
             push @swapDevices, "{ device = \"$dev\"; }";
         } elsif ($swapType =~ "file") {
             # swap *files* are more likely specified in configuration.nix, so
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index bb217d873bb..7650da89cbd 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -11,6 +11,7 @@
   ./config/xdg/mime.nix
   ./config/xdg/portal.nix
   ./config/appstream.nix
+  ./config/console.nix
   ./config/xdg/sounds.nix
   ./config/gtk/gtk-icon-cache.nix
   ./config/gnu.nix
@@ -443,6 +444,7 @@
   ./services/misc/logkeys.nix
   ./services/misc/leaps.nix
   ./services/misc/lidarr.nix
+  ./services/misc/mame.nix
   ./services/misc/mathics.nix
   ./services/misc/matrix-synapse.nix
   ./services/misc/mbpfan.nix
@@ -556,6 +558,7 @@
   ./services/network-filesystems/yandex-disk.nix
   ./services/network-filesystems/xtreemfs.nix
   ./services/network-filesystems/ceph.nix
+  ./services/networking/3proxy.nix
   ./services/networking/amuled.nix
   ./services/networking/aria2.nix
   ./services/networking/asterisk.nix
@@ -866,6 +869,7 @@
   ./services/x11/hardware/digimend.nix
   ./services/x11/hardware/cmt.nix
   ./services/x11/gdk-pixbuf.nix
+  ./services/x11/imwheel.nix
   ./services/x11/redshift.nix
   ./services/x11/urxvtd.nix
   ./services/x11/window-managers/awesome.nix
@@ -936,7 +940,6 @@
   ./tasks/filesystems/vfat.nix
   ./tasks/filesystems/xfs.nix
   ./tasks/filesystems/zfs.nix
-  ./tasks/kbd.nix
   ./tasks/lvm.nix
   ./tasks/network-interfaces.nix
   ./tasks/network-interfaces-systemd.nix
diff --git a/nixos/modules/programs/oblogout.nix b/nixos/modules/programs/oblogout.nix
index 720c29b1eae..a039b0623b5 100644
--- a/nixos/modules/programs/oblogout.nix
+++ b/nixos/modules/programs/oblogout.nix
@@ -1,176 +1,11 @@
-# Global configuration for oblogout.
-
 { config, lib, pkgs, ... }:
 
 with lib;
 
-let cfg = config.programs.oblogout;
-
-in
 {
-  ###### interface
-
-  options = {
-
-    programs.oblogout = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = ''
-          Whether to install OBLogout and create <filename>/etc/oblogout.conf</filename>.
-          See <filename>${pkgs.oblogout}/share/doc/README</filename>.
-        '';
-      };
-
-      opacity = mkOption {
-        type = types.int;
-        default = 70;
-        description = ''
-          Opacity percentage of Cairo rendered backgrounds.
-        '';
-      };
-
-      bgcolor = mkOption {
-        type = types.str;
-        default = "black";
-        description = ''
-          Colour name or hex code (#ffffff) of the background color.
-        '';
-      };
-
-      buttontheme = mkOption {
-        type = types.str;
-        default = "simplistic";
-        description = ''
-          Icon theme for the buttons, must be in the themes folder of
-          the package, or in
-          <filename>~/.themes/&lt;name&gt;/oblogout/</filename>.
-        '';
-      };
-
-      buttons = mkOption {
-        type = types.str;
-        default =  "cancel, logout, restart, shutdown, suspend, hibernate";
-        description = ''
-          List and order of buttons to show.
-        '';
-      };
-
-      cancel = mkOption {
-        type = types.str;
-        default =  "Escape";
-        description = ''
-          Cancel logout/shutdown shortcut.
-        '';
-      };
-
-      shutdown = mkOption {
-        type = types.str;
-        default = "S";
-        description = ''
-          Shutdown shortcut.
-        '';
-      };
-
-      restart = mkOption {
-        type = types.str;
-        default = "R";
-        description = ''
-          Restart shortcut.
-        '';
-      };
-
-      suspend = mkOption {
-        type = types.str;
-        default = "U";
-        description = ''
-          Suspend shortcut.
-        '';
-      };
-
-      logout = mkOption {
-        type = types.str;
-        default = "L";
-        description = ''
-          Logout shortcut.
-        '';
-      };
-
-      lock = mkOption {
-        type = types.str;
-        default = "K";
-        description = ''
-          Lock session shortcut.
-        '';
-      };
-
-      hibernate = mkOption {
-        type = types.str;
-        default =  "H";
-        description = ''
-          Hibernate shortcut.
-        '';
-      };
-
-      clogout = mkOption {
-        type = types.str;
-        default = "openbox --exit";
-        description = ''
-          Command to logout.
-        '';
-      };
-
-      clock = mkOption {
-        type = types.str;
-        default = "";
-        description = ''
-          Command to lock screen.
-        '';
-      };
-
-      cswitchuser = mkOption {
-        type = types.str;
-        default = "";
-        description = ''
-          Command to switch user.
-        '';
-      };
-    };
-  };
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-    environment.systemPackages = [ pkgs.oblogout ];
-
-    environment.etc."oblogout.conf".text = ''
-      [settings]
-      usehal = false
-
-      [looks]
-      opacity = ${toString cfg.opacity}
-      bgcolor = ${cfg.bgcolor}
-      buttontheme = ${cfg.buttontheme}
-      buttons = ${cfg.buttons}
 
-      [shortcuts]
-      cancel = ${cfg.cancel}
-      shutdown = ${cfg.shutdown}
-      restart = ${cfg.restart}
-      suspend = ${cfg.suspend}
-      logout = ${cfg.logout}
-      lock = ${cfg.lock}
-      hibernate = ${cfg.hibernate}
+  imports = [
+    (mkRemovedOptionModule [ "programs" "oblogout" ] "programs.oblogout has been removed from NixOS. This is because the oblogout repository has been archived upstream.")
+  ];
 
-      [commands]
-      shutdown = systemctl poweroff
-      restart = systemctl reboot
-      suspend = systemctl suspend
-      hibernate = systemctl hibernate
-      logout = ${cfg.clogout}
-      lock = ${cfg.clock}
-      switchuser = ${cfg.cswitchuser}
-    '';
-  };
 }
diff --git a/nixos/modules/programs/sway.nix b/nixos/modules/programs/sway.nix
index f92d09a7ef4..d685a525932 100644
--- a/nixos/modules/programs/sway.nix
+++ b/nixos/modules/programs/sway.nix
@@ -24,6 +24,7 @@ let
   swayJoined = pkgs.symlinkJoin {
     name = "sway-joined";
     paths = [ swayWrapped swayPackage ];
+    passthru.providedSessions = [ "sway" ];
   };
 in {
   options.programs.sway = {
@@ -87,6 +88,8 @@ in {
     hardware.opengl.enable = mkDefault true;
     fonts.enableDefaultFonts = mkDefault true;
     programs.dconf.enable = mkDefault true;
+    # To make a Sway session available if a display manager like SDDM is enabled:
+    services.xserver.displayManager.sessionPackages = [ swayJoined ];
   };
 
   meta.maintainers = with lib.maintainers; [ gnidorah primeos colemickens ];
diff --git a/nixos/modules/security/acme.nix b/nixos/modules/security/acme.nix
index 8b7d12552ed..890c421b0ea 100644
--- a/nixos/modules/security/acme.nix
+++ b/nixos/modules/security/acme.nix
@@ -241,9 +241,9 @@ in
                     StateDirectoryMode = rights;
                     WorkingDirectory = "/var/lib/${lpath}";
                     ExecStart = "${pkgs.simp_le}/bin/simp_le ${escapeShellArgs cmdline}";
-                    ExecStopPost =
+                    ExecStartPost =
                       let
-                        script = pkgs.writeScript "acme-post-stop" ''
+                        script = pkgs.writeScript "acme-post-start" ''
                           #!${pkgs.runtimeShell} -e
                           ${data.postRun}
                         '';
diff --git a/nixos/modules/security/polkit.nix b/nixos/modules/security/polkit.nix
index f2b2df4004c..a6724bd7583 100644
--- a/nixos/modules/security/polkit.nix
+++ b/nixos/modules/security/polkit.nix
@@ -42,15 +42,14 @@ in
 
     security.polkit.adminIdentities = mkOption {
       type = types.listOf types.str;
-      default = [ "unix-user:0" "unix-group:wheel" ];
+      default = [ "unix-group:wheel" ];
       example = [ "unix-user:alice" "unix-group:admin" ];
       description =
         ''
           Specifies which users are considered “administrators”, for those
           actions that require the user to authenticate as an
           administrator (i.e. have an <literal>auth_admin</literal>
-          value).  By default, this is the <literal>root</literal>
-          user and all users in the <literal>wheel</literal> group.
+          value).  By default, this is all users in the <literal>wheel</literal> group.
         '';
     };
 
diff --git a/nixos/modules/services/admin/oxidized.nix b/nixos/modules/services/admin/oxidized.nix
index da81be3f23e..885eaed1de6 100644
--- a/nixos/modules/services/admin/oxidized.nix
+++ b/nixos/modules/services/admin/oxidized.nix
@@ -111,6 +111,7 @@ in
         Restart  = "always";
         WorkingDirectory = cfg.dataDir;
         KillSignal = "SIGKILL";
+        PIDFile = "${cfg.dataDir}.config/oxidized/pid";
       };
     };
   };
diff --git a/nixos/modules/services/backup/bacula.nix b/nixos/modules/services/backup/bacula.nix
index 41bda7893a7..cef304734ae 100644
--- a/nixos/modules/services/backup/bacula.nix
+++ b/nixos/modules/services/backup/bacula.nix
@@ -44,7 +44,17 @@ let
         Pid Directory = "/run";
         ${sd_cfg.extraStorageConfig}
       }
- 
+
+      ${concatStringsSep "\n" (mapAttrsToList (name: value: ''
+      Autochanger {
+        Name = "${name}";
+        Device = ${concatStringsSep ", " (map (a: "\"${a}\"") value.devices)};
+        Changer Device =  "${value.changerDevice}";
+        Changer Command = "${value.changerCommand}";
+        ${value.extraAutochangerConfig}
+      }
+      '') sd_cfg.autochanger)}
+
       ${concatStringsSep "\n" (mapAttrsToList (name: value: ''
       Device {
         Name = "${name}";
@@ -103,7 +113,19 @@ let
       password = mkOption {
         # TODO: required?
         description = ''
-           Specifies the password that must be supplied for a Director to b
+          Specifies the password that must be supplied for the default Bacula
+          Console to be authorized. The same password must appear in the
+          Director resource of the Console configuration file. For added
+          security, the password is never passed across the network but instead
+          a challenge response hash code created with the password. This
+          directive is required. If you have either /dev/random or bc on your
+          machine, Bacula will generate a random password during the
+          configuration process, otherwise it will be left blank and you must
+          manually supply it.
+
+          The password is plain text. It is not generated through any special
+          process but as noted above, it is better to use random text for
+          security reasons. 
         '';
       };
       
@@ -111,26 +133,133 @@ let
         default = "no";
         example = "yes";
         description = ''
-           If Monitor is set to no (default), this director will have full 
+          If Monitor is set to <literal>no</literal>, this director will have
+          full access to this Storage daemon. If Monitor is set to
+          <literal>yes</literal>, this director will only be able to fetch the
+          current status of this Storage daemon.
+
+          Please note that if this director is being used by a Monitor, we
+          highly recommend to set this directive to yes to avoid serious
+          security problems. 
+        '';
+      };
+    };
+  };
+
+  autochangerOptions = {...}:
+  {
+    options = {
+      changerDevice = mkOption {
+        description = ''
+          The specified name-string must be the generic SCSI device name of the
+          autochanger that corresponds to the normal read/write Archive Device
+          specified in the Device resource. This generic SCSI device name
+          should be specified if you have an autochanger or if you have a
+          standard tape drive and want to use the Alert Command (see below).
+          For example, on Linux systems, for an Archive Device name of
+          <literal>/dev/nst0</literal>, you would specify
+          <literal>/dev/sg0</literal> for the Changer Device name.  Depending
+          on your exact configuration, and the number of autochangers or the
+          type of autochanger, what you specify here can vary. This directive
+          is optional. See the Using AutochangersAutochangersChapter chapter of
+          this manual for more details of using this and the following
+          autochanger directives.         
+          '';
+      };
+
+      changerCommand = mkOption {
+        description = ''
+          The name-string specifies an external program to be called that will
+          automatically change volumes as required by Bacula. Normally, this
+          directive will be specified only in the AutoChanger resource, which
+          is then used for all devices. However, you may also specify the
+          different Changer Command in each Device resource. Most frequently,
+          you will specify the Bacula supplied mtx-changer script as follows:
+
+          <literal>"/path/mtx-changer %c %o %S %a %d"</literal>
+
+          and you will install the mtx on your system (found in the depkgs
+          release). An example of this command is in the default bacula-sd.conf
+          file. For more details on the substitution characters that may be
+          specified to configure your autochanger please see the
+          AutochangersAutochangersChapter chapter of this manual. For FreeBSD
+          users, you might want to see one of the several chio scripts in
+          examples/autochangers.
+          '';
+        default = "/etc/bacula/mtx-changer %c %o %S %a %d";
+      };
+
+      devices = mkOption {
+        description = ''
+        '';
+      };
+
+      extraAutochangerConfig = mkOption {
+        default = "";
+        description = ''
+          Extra configuration to be passed in Autochanger directive.
+        '';
+        example = ''
+   
         '';
       };
     };
   };
 
+
   deviceOptions = {...}:
   {
     options = {
       archiveDevice = mkOption {
         # TODO: required?
         description = ''
-          The specified name-string gives the system file name of the storage device managed by this storage daemon. This will usually be the device file name of a removable storage device (tape drive), for example " /dev/nst0" or "/dev/rmt/0mbn". For a DVD-writer, it will be for example /dev/hdc. It may also be a directory name if you are archiving to disk storage.
+          The specified name-string gives the system file name of the storage
+          device managed by this storage daemon. This will usually be the
+          device file name of a removable storage device (tape drive), for
+          example <literal>/dev/nst0</literal> or
+          <literal>/dev/rmt/0mbn</literal>. For a DVD-writer, it will be for
+          example <literal>/dev/hdc</literal>. It may also be a directory name
+          if you are archiving to disk storage. In this case, you must supply
+          the full absolute path to the directory. When specifying a tape
+          device, it is preferable that the "non-rewind" variant of the device
+          file name be given. 
         '';
       };
 
       mediaType = mkOption {
         # TODO: required?
         description = ''
-          The specified name-string names the type of media supported by this device, for example, "DLT7000". Media type names are arbitrary in that you set them to anything you want, but they must be known to the volume database to keep track of which storage daemons can read which volumes. In general, each different storage type should have a unique Media Type associated with it. The same name-string must appear in the appropriate Storage resource definition in the Director's configuration file.
+          The specified name-string names the type of media supported by this
+          device, for example, <literal>DLT7000</literal>. Media type names are
+          arbitrary in that you set them to anything you want, but they must be
+          known to the volume database to keep track of which storage daemons
+          can read which volumes. In general, each different storage type
+          should have a unique Media Type associated with it. The same
+          name-string must appear in the appropriate Storage resource
+          definition in the Director's configuration file.
+
+          Even though the names you assign are arbitrary (i.e. you choose the
+          name you want), you should take care in specifying them because the
+          Media Type is used to determine which storage device Bacula will
+          select during restore. Thus you should probably use the same Media
+          Type specification for all drives where the Media can be freely
+          interchanged. This is not generally an issue if you have a single
+          Storage daemon, but it is with multiple Storage daemons, especially
+          if they have incompatible media.
+
+          For example, if you specify a Media Type of <literal>DDS-4</literal>
+          then during the restore, Bacula will be able to choose any Storage
+          Daemon that handles <literal>DDS-4</literal>. If you have an
+          autochanger, you might want to name the Media Type in a way that is
+          unique to the autochanger, unless you wish to possibly use the
+          Volumes in other drives. You should also ensure to have unique Media
+          Type names if the Media is not compatible between drives. This
+          specification is required for all devices.
+
+          In addition, if you are using disk storage, each Device resource will
+          generally have a different mount point or directory. In order for
+          Bacula to select the correct Device resource, each one must have a
+          unique Media Type.
         '';
       };
 
@@ -166,8 +295,8 @@ in {
         default = "${config.networking.hostName}-fd";
         description = ''
           The client name that must be used by the Director when connecting.
-          Generally, it is a good idea to use a name related to the machine
-          so that error messages can be easily identified if you have multiple
+          Generally, it is a good idea to use a name related to the machine so
+          that error messages can be easily identified if you have multiple
           Clients. This directive is required.
         '';
       };
@@ -232,7 +361,8 @@ in {
         default = 9103;
         type = types.int;
         description = ''
-          Specifies port number on which the Storage daemon listens for Director connections. The default is 9103.
+          Specifies port number on which the Storage daemon listens for
+          Director connections.
         '';
       };
 
@@ -251,7 +381,15 @@ in {
         '';
         type = with types; attrsOf (submodule deviceOptions);
       };
- 
+
+      autochanger = mkOption {
+        default = {};
+        description = ''
+          This option defines Autochanger resources in Bacula Storage Daemon.
+        '';
+        type = with types; attrsOf (submodule autochangerOptions);
+      };
+
       extraStorageConfig = mkOption {
         default = "";
         description = ''
@@ -287,7 +425,8 @@ in {
       name = mkOption {
         default = "${config.networking.hostName}-dir";
         description = ''
-          The director name used by the system administrator. This directive is required.
+          The director name used by the system administrator. This directive is
+          required.
         '';
       };
  
@@ -295,7 +434,12 @@ in {
         default = 9101;
         type = types.int;
         description = ''
-          Specify the port (a positive integer) on which the Director daemon will listen for Bacula Console connections. This same port number must be specified in the Director resource of the Console configuration file. The default is 9101, so normally this directive need not be specified. This directive should not be used if you specify DirAddresses (N.B plural) directive.
+          Specify the port (a positive integer) on which the Director daemon
+          will listen for Bacula Console connections. This same port number
+          must be specified in the Director resource of the Console
+          configuration file. The default is 9101, so normally this directive
+          need not be specified. This directive should not be used if you
+          specify DirAddresses (N.B plural) directive.
         '';
       };
  
diff --git a/nixos/modules/services/backup/postgresql-backup.nix b/nixos/modules/services/backup/postgresql-backup.nix
index e768d1b9918..580c7ce68f1 100644
--- a/nixos/modules/services/backup/postgresql-backup.nix
+++ b/nixos/modules/services/backup/postgresql-backup.nix
@@ -89,7 +89,7 @@ in {
 
       pgdumpOptions = mkOption {
         type = types.separatedString " ";
-        default = "-Cbo";
+        default = "-C";
         description = ''
           Command line options for pg_dump. This options is not used
           if <literal>config.services.postgresqlBackup.backupAll</literal> is enabled.
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
index 3bedfe96a18..c8fdd89d0d8 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -339,9 +339,9 @@ in
             '') cfg.ensureDatabases}
           '' + ''
             ${concatMapStrings (user: ''
-              $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc "CREATE USER ${user.name}"
+              $PSQL -tAc "SELECT 1 FROM pg_roles WHERE rolname='${user.name}'" | grep -q 1 || $PSQL -tAc 'CREATE USER "${user.name}"'
               ${concatStringsSep "\n" (mapAttrsToList (database: permission: ''
-                $PSQL -tAc 'GRANT ${permission} ON ${database} TO ${user.name}'
+                $PSQL -tAc 'GRANT ${permission} ON ${database} TO "${user.name}"'
               '') user.ensurePermissions)}
             '') cfg.ensureUsers}
           '';
diff --git a/nixos/modules/services/development/lorri.nix b/nixos/modules/services/development/lorri.nix
index 68264ee869d..c843aa56d13 100644
--- a/nixos/modules/services/development/lorri.nix
+++ b/nixos/modules/services/development/lorri.nix
@@ -32,7 +32,7 @@ in {
       description = "Lorri Daemon";
       requires = [ "lorri.socket" ];
       after = [ "lorri.socket" ];
-      path = with pkgs; [ config.nix.package gnutar gzip ];
+      path = with pkgs; [ config.nix.package git gnutar gzip ];
       serviceConfig = {
         ExecStart = "${pkgs.lorri}/bin/lorri daemon";
         PrivateTmp = true;
diff --git a/nixos/modules/services/misc/mame.nix b/nixos/modules/services/misc/mame.nix
new file mode 100644
index 00000000000..c5d5e9e4837
--- /dev/null
+++ b/nixos/modules/services/misc/mame.nix
@@ -0,0 +1,67 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mame;
+  mame = "mame${lib.optionalString pkgs.stdenv.is64bit "64"}";
+in
+{
+  options = {
+    services.mame = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to setup TUN/TAP Ethernet interface for MAME emulator.
+        '';
+      };
+      user = mkOption {
+        type = types.str;
+        description = ''
+          User from which you run MAME binary.
+        '';
+      };
+      hostAddr = mkOption {
+        type = types.str;
+        description = ''
+          IP address of the host system. Usually an address of the main network
+          adapter or the adapter through which you get an internet connection.
+        '';
+        example = "192.168.31.156";
+      };
+      emuAddr = mkOption {
+        type = types.str;
+        description = ''
+          IP address of the guest system. The same you set inside guest OS under
+          MAME. Should be on the same subnet as <option>services.mame.hostAddr</option>.
+        '';
+        example = "192.168.31.155";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.mame ];
+
+    security.wrappers."${mame}" = {
+      source = "${pkgs.mame}/bin/${mame}";
+      capabilities = "cap_net_admin,cap_net_raw+eip";
+    };
+
+    systemd.services.mame = {
+      description = "MAME TUN/TAP Ethernet interface";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.iproute ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = "${pkgs.mame}/bin/taputil.sh -c ${cfg.user} ${cfg.emuAddr} ${cfg.hostAddr} -";
+        ExecStop = "${pkgs.mame}/bin/taputil.sh -d ${cfg.user}";
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ gnidorah ];
+}
diff --git a/nixos/modules/services/misc/matrix-synapse.nix b/nixos/modules/services/misc/matrix-synapse.nix
index 50661b873f6..0bda8980720 100644
--- a/nixos/modules/services/misc/matrix-synapse.nix
+++ b/nixos/modules/services/misc/matrix-synapse.nix
@@ -671,43 +671,30 @@ in {
         gid = config.ids.gids.matrix-synapse;
       } ];
 
-    services.postgresql.enable = mkIf usePostgresql (mkDefault true);
+    services.postgresql = mkIf (usePostgresql && cfg.create_local_database) {
+      enable = mkDefault true;
+      ensureDatabases = [ cfg.database_name ];
+      ensureUsers = [{
+        name = cfg.database_user;
+        ensurePermissions = { "DATABASE \"${cfg.database_name}\"" = "ALL PRIVILEGES"; };
+      }];
+    };
 
     systemd.services.matrix-synapse = {
       description = "Synapse Matrix homeserver";
-      after = [ "network.target" "postgresql.service" ];
+      after = [ "network.target" ] ++ lib.optional config.services.postgresql.enable "postgresql.service" ;
       wantedBy = [ "multi-user.target" ];
       preStart = ''
         ${cfg.package}/bin/homeserver \
           --config-path ${configFile} \
           --keys-directory ${cfg.dataDir} \
           --generate-keys
-      '' + optionalString (usePostgresql && cfg.create_local_database) ''
-        if ! test -e "${cfg.dataDir}/db-created"; then
-          ${pkgs.sudo}/bin/sudo -u ${pg.superUser} \
-            ${pg.package}/bin/createuser \
-            --login \
-            --no-createdb \
-            --no-createrole \
-            --encrypted \
-            ${cfg.database_user}
-          ${pkgs.sudo}/bin/sudo -u ${pg.superUser} \
-            ${pg.package}/bin/createdb \
-            --owner=${cfg.database_user} \
-            --encoding=UTF8 \
-            --lc-collate=C \
-            --lc-ctype=C \
-            --template=template0 \
-            ${cfg.database_name}
-          touch "${cfg.dataDir}/db-created"
-        fi
       '';
       serviceConfig = {
         Type = "notify";
         User = "matrix-synapse";
         Group = "matrix-synapse";
         WorkingDirectory = cfg.dataDir;
-        PermissionsStartOnly = true;
         ExecStart = ''
           ${cfg.package}/bin/homeserver \
             ${ concatMapStringsSep "\n  " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) }
diff --git a/nixos/modules/services/networking/3proxy.nix b/nixos/modules/services/networking/3proxy.nix
new file mode 100644
index 00000000000..26aa1667946
--- /dev/null
+++ b/nixos/modules/services/networking/3proxy.nix
@@ -0,0 +1,424 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  pkg = pkgs._3proxy;
+  cfg = config.services._3proxy;
+  optionalList = list: if list == [ ] then "*" else concatMapStringsSep "," toString list;
+in {
+  options.services._3proxy = {
+    enable = mkEnableOption "3proxy";
+    confFile = mkOption {
+      type = types.path;
+      example = "/var/lib/3proxy/3proxy.conf";
+      description = ''
+        Ignore all other 3proxy options and load configuration from this file.
+      '';
+    };
+    usersFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      example = "/var/lib/3proxy/3proxy.passwd";
+      description = ''
+        Load users and passwords from this file.
+
+        Example users file with plain-text passwords:
+
+        <literal>
+          test1:CL:password1
+          test2:CL:password2
+        </literal>
+
+        Example users file with md5-crypted passwords:
+
+        <literal>
+          test1:CR:$1$tFkisVd2$1GA8JXkRmTXdLDytM/i3a1
+          test2:CR:$1$rkpibm5J$Aq1.9VtYAn0JrqZ8M.1ME.
+        </literal>
+
+        You can generate md5-crypted passwords via https://unix4lyfe.org/crypt/
+        Note that htpasswd tool generates incompatible md5-crypted passwords.
+        Consult <link xlink:href="https://github.com/z3APA3A/3proxy/wiki/How-To-(incomplete)#USERS">documentation</link> for more information.
+      '';
+    };
+    services = mkOption {
+      type = types.listOf (types.submodule {
+        options = {
+          type = mkOption {
+            type = types.enum [
+              "proxy"
+              "socks"
+              "pop3p"
+              "ftppr"
+              "admin"
+              "dnspr"
+              "tcppm"
+              "udppm"
+            ];
+            example = "proxy";
+            description = ''
+              Service type. The following values are valid:
+
+              <itemizedlist>
+                <listitem><para>
+                  <literal>"proxy"</literal>: HTTP/HTTPS proxy (default port 3128).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"socks"</literal>: SOCKS 4/4.5/5 proxy (default port 1080).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"pop3p"</literal>: POP3 proxy (default port 110).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"ftppr"</literal>: FTP proxy (default port 21).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"admin"</literal>: Web interface (default port 80).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"dnspr"</literal>: Caching DNS proxy (default port 53).
+                </para></listitem>
+                <listitem><para>
+                  <literal>"tcppm"</literal>: TCP portmapper.
+                </para></listitem>
+                <listitem><para>
+                  <literal>"udppm"</literal>: UDP portmapper.
+                </para></listitem>
+              </itemizedlist>
+            '';
+          };
+          bindAddress = mkOption {
+            type = types.str;
+            default = "[::]";
+            example = "127.0.0.1";
+            description = ''
+              Address used for service.
+            '';
+          };
+          bindPort = mkOption {
+            type = types.nullOr types.int;
+            default = null;
+            example = 3128;
+            description = ''
+              Override default port used for service.
+            '';
+          };
+          maxConnections = mkOption {
+            type = types.int;
+            default = 100;
+            example = 1000;
+            description = ''
+              Maximum number of simulationeous connections to this service.
+            '';
+          };
+          auth = mkOption {
+            type = types.listOf (types.enum [ "none" "iponly" "strong" ]);
+            example = [ "iponly" "strong" ];
+            description = ''
+              Authentication type. The following values are valid:
+
+              <itemizedlist>
+                <listitem><para>
+                  <literal>"none"</literal>: disables both authentication and authorization. You can not use ACLs.
+                </para></listitem>
+                <listitem><para>
+                  <literal>"iponly"</literal>: specifies no authentication. ACLs authorization is used.
+                </para></listitem>
+                <listitem><para>
+                  <literal>"strong"</literal>: authentication by username/password. If user is not registered his access is denied regardless of ACLs.
+                </para></listitem>
+              </itemizedlist>
+
+              Double authentication is possible, e.g.
+
+              <literal>
+                {
+                  auth = [ "iponly" "strong" ];
+                  acl = [
+                    {
+                      rule = "allow";
+                      targets = [ "192.168.0.0/16" ];
+                    }
+                    {
+                      rule = "allow"
+                      users = [ "user1" "user2" ];
+                    }
+                  ];
+                }
+              </literal>
+              In this example strong username authentication is not required to access 192.168.0.0/16.
+            '';
+          };
+          acl = mkOption {
+            type = types.listOf (types.submodule {
+              options = {
+                rule = mkOption {
+                  type = types.enum [ "allow" "deny" ];
+                  example = "allow";
+                  description = ''
+                    ACL rule. The following values are valid:
+
+                    <itemizedlist>
+                      <listitem><para>
+                        <literal>"allow"</literal>: connections allowed.
+                      </para></listitem>
+                      <listitem><para>
+                        <literal>"deny"</literal>: connections not allowed.
+                      </para></listitem>
+                    </itemizedlist>
+                  '';
+                };
+                users = mkOption {
+                  type = types.listOf types.str;
+                  default = [ ];
+                  example = [ "user1" "user2" "user3" ];
+                  description = ''
+                    List of users, use empty list for any.
+                  '';
+                };
+                sources = mkOption {
+                  type = types.listOf types.str;
+                  default = [ ];
+                  example = [ "127.0.0.1" "192.168.1.0/24" ];
+                  description = ''
+                    List of source IP range, use empty list for any.
+                  '';
+                };
+                targets = mkOption {
+                  type = types.listOf types.str;
+                  default = [ ];
+                  example = [ "127.0.0.1" "192.168.1.0/24" ];
+                  description = ''
+                    List of target IP ranges, use empty list for any.
+                    May also contain host names instead of addresses.
+                    It's possible to use wildmask in the begginning and in the the end of hostname, e.g. *badsite.com or *badcontent*.
+                    Hostname is only checked if hostname presents in request.
+                  '';
+                };
+                targetPorts = mkOption {
+                  type = types.listOf types.int;
+                  default = [ ];
+                  example = [ 80 443 ];
+                  description = ''
+                    List of target ports, use empty list for any.
+                  '';
+                };
+              };
+            });
+            default = [ ];
+            example = literalExample ''
+              [
+                {
+                  rule = "allow";
+                  users = [ "user1" ];
+                }
+                {
+                  rule = "allow";
+                  sources = [ "192.168.1.0/24" ];
+                }
+                {
+                  rule = "deny";
+                }
+              ]
+            '';
+            description = ''
+              Use this option to limit user access to resources.
+            '';
+          };
+          extraArguments = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            example = "-46";
+            description = ''
+              Extra arguments for service.
+              Consult "Options" section in <link xlink:href="https://github.com/z3APA3A/3proxy/wiki/3proxy.cfg">documentation</link> for available arguments.
+            '';
+          };
+          extraConfig = mkOption {
+            type = types.nullOr types.lines;
+            default = null;
+            description = ''
+              Extra configuration for service. Use this to configure things like bandwidth limiter or ACL-based redirection.
+              Consult <link xlink:href="https://github.com/z3APA3A/3proxy/wiki/3proxy.cfg">documentation</link> for available options.
+            '';
+          };
+        };
+      });
+      default = [ ];
+      example = literalExample ''
+        [
+          {
+            type = "proxy";
+            bindAddress = "192.168.1.24";
+            bindPort = 3128;
+            auth = [ "none" ];
+          }
+          {
+            type = "proxy";
+            bindAddress = "10.10.1.20";
+            bindPort = 3128;
+            auth = [ "iponly" ];
+          }
+          {
+            type = "socks";
+            bindAddress = "172.17.0.1";
+            bindPort = 1080;
+            auth = [ "strong" ];
+          }
+        ]
+      '';
+      description = ''
+        Use this option to define 3proxy services.
+      '';
+    };
+    denyPrivate = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to deny access to private IP ranges including loopback.
+      '';
+    };
+    privateRanges = mkOption {
+      type = types.listOf types.str;
+      default = [
+        "0.0.0.0/8"
+        "127.0.0.0/8"
+        "10.0.0.0/8"
+        "100.64.0.0/10"
+        "172.16.0.0/12"
+        "192.168.0.0/16"
+        "::"
+        "::1"
+        "fc00::/7"
+      ];
+      example = [
+        "0.0.0.0/8"
+        "127.0.0.0/8"
+        "10.0.0.0/8"
+        "100.64.0.0/10"
+        "172.16.0.0/12"
+        "192.168.0.0/16"
+        "::"
+        "::1"
+        "fc00::/7"
+      ];
+      description = ''
+        What IP ranges to deny access when denyPrivate is set tu true.
+      '';
+    };
+    resolution = mkOption {
+      type = types.submodule {
+        options = {
+          nserver = mkOption {
+            type = types.listOf types.str;
+            default = [ ];
+            example = [ "127.0.0.53" "192.168.1.3:5353/tcp" ];
+            description = ''
+              List of nameservers to use.
+
+              Up to 5 nservers may be specified. If no nserver is configured,
+              default system name resolution functions are used.
+            '';
+          };
+          nscache = mkOption {
+            type = types.int;
+            default = 65535;
+            example = 65535;
+            description = "Set name cache size for IPv4.";
+          };
+          nscache6 = mkOption {
+            type = types.int;
+            default = 65535;
+            example = 65535;
+            description = "Set name cache size for IPv6.";
+          };
+          nsrecord = mkOption {
+            type = types.attrsOf types.str;
+            default = { };
+            example = {
+              "files.local" = "192.168.1.12";
+              "site.local" = "192.168.1.43";
+            };
+            description = "Adds static nsrecords.";
+          };
+        };
+      };
+      default = { };
+      description = ''
+        Use this option to configure name resolution and DNS caching.
+      '';
+    };
+    extraConfig = mkOption {
+      type = types.nullOr types.lines;
+      default = null;
+      description = ''
+        Extra configuration, appended to the 3proxy configuration file.
+        Consult <link xlink:href="https://github.com/z3APA3A/3proxy/wiki/3proxy.cfg">documentation</link> for available options.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services._3proxy.confFile = mkDefault (pkgs.writeText "3proxy.conf" ''
+      # log to stdout
+      log
+
+      ${concatMapStringsSep "\n" (x: "nserver " + x) cfg.resolution.nserver}
+
+      nscache ${toString cfg.resolution.nscache}
+      nscache6 ${toString cfg.resolution.nscache6}
+
+      ${concatMapStringsSep "\n" (x: "nsrecord " + x)
+      (mapAttrsToList (name: value: "${name} ${value}")
+        cfg.resolution.nsrecord)}
+
+      ${optionalString (cfg.usersFile != null)
+        ''users $"${cfg.usersFile}"''
+      }
+
+      ${concatMapStringsSep "\n" (service: ''
+        auth ${concatStringsSep " " service.auth}
+
+        ${optionalString (cfg.denyPrivate)
+        "deny * * ${optionalList cfg.privateRanges}"}
+
+        ${concatMapStringsSep "\n" (acl:
+          "${acl.rule} ${
+            concatMapStringsSep " " optionalList [
+              acl.users
+              acl.sources
+              acl.targets
+              acl.targetPorts
+            ]
+          }") service.acl}
+
+        maxconn ${toString service.maxConnections}
+
+        ${optionalString (service.extraConfig != null) service.extraConfig}
+
+        ${service.type} -i${toString service.bindAddress} ${
+          optionalString (service.bindPort != null)
+          "-p${toString service.bindPort}"
+        } ${
+          optionalString (service.extraArguments != null) service.extraArguments
+        }
+
+        flush
+      '') cfg.services}
+      ${optionalString (cfg.extraConfig != null) cfg.extraConfig}
+    '');
+    systemd.services."3proxy" = {
+      description = "Tiny free proxy server";
+      documentation = [ "https://github.com/z3APA3A/3proxy/wiki" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "3proxy";
+        ExecStart = "${pkg}/bin/3proxy ${cfg.confFile}";
+        Restart = "on-failure";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ misuzu ];
+}
diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix
index 5919962837a..15aaf741067 100644
--- a/nixos/modules/services/networking/firewall.nix
+++ b/nixos/modules/services/networking/firewall.nix
@@ -42,16 +42,7 @@ let
 
   kernelHasRPFilter = ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") || (kernel.features.netfilterRPFilter or false);
 
-  helpers =
-    ''
-      # Helper command to manipulate both the IPv4 and IPv6 tables.
-      ip46tables() {
-        iptables -w "$@"
-        ${optionalString config.networking.enableIPv6 ''
-          ip6tables -w "$@"
-        ''}
-      }
-    '';
+  helpers = import ./helpers.nix { inherit config lib; };
 
   writeShScript = name: text: let dir = pkgs.writeScriptBin name ''
     #! ${pkgs.runtimeShell} -e
@@ -271,7 +262,7 @@ let
       apply = canonicalizePortList;
       example = [ 22 80 ];
       description =
-        '' 
+        ''
           List of TCP ports on which incoming connections are
           accepted.
         '';
@@ -282,7 +273,7 @@ let
       default = [ ];
       example = [ { from = 8999; to = 9003; } ];
       description =
-        '' 
+        ''
           A range of TCP ports on which incoming connections are
           accepted.
         '';
diff --git a/nixos/modules/services/networking/helpers.nix b/nixos/modules/services/networking/helpers.nix
new file mode 100644
index 00000000000..d7d42de0e3a
--- /dev/null
+++ b/nixos/modules/services/networking/helpers.nix
@@ -0,0 +1,11 @@
+{ config, lib, ... }: ''
+  # Helper command to manipulate both the IPv4 and IPv6 tables.
+  ip46tables() {
+    iptables -w "$@"
+    ${
+      lib.optionalString config.networking.enableIPv6 ''
+        ip6tables -w "$@"
+      ''
+    }
+  }
+''
diff --git a/nixos/modules/services/networking/nat.nix b/nixos/modules/services/networking/nat.nix
index 5681bda51cb..f1238bc6b16 100644
--- a/nixos/modules/services/networking/nat.nix
+++ b/nixos/modules/services/networking/nat.nix
@@ -7,26 +7,33 @@
 with lib;
 
 let
-
   cfg = config.networking.nat;
 
   dest = if cfg.externalIP == null then "-j MASQUERADE" else "-j SNAT --to-source ${cfg.externalIP}";
 
+  helpers = import ./helpers.nix { inherit config lib; };
+
   flushNat = ''
-    iptables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true
-    iptables -w -t nat -F nixos-nat-pre 2>/dev/null || true
-    iptables -w -t nat -X nixos-nat-pre 2>/dev/null || true
-    iptables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true
-    iptables -w -t nat -F nixos-nat-post 2>/dev/null || true
-    iptables -w -t nat -X nixos-nat-post 2>/dev/null || true
+    ${helpers}
+    ip46tables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true
+    ip46tables -w -t nat -F nixos-nat-pre 2>/dev/null || true
+    ip46tables -w -t nat -X nixos-nat-pre 2>/dev/null || true
+    ip46tables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true
+    ip46tables -w -t nat -F nixos-nat-post 2>/dev/null || true
+    ip46tables -w -t nat -X nixos-nat-post 2>/dev/null || true
+    ip46tables -w -t nat -D OUTPUT -j nixos-nat-out 2>/dev/null || true
+    ip46tables -w -t nat -F nixos-nat-out 2>/dev/null || true
+    ip46tables -w -t nat -X nixos-nat-out 2>/dev/null || true
 
     ${cfg.extraStopCommands}
   '';
 
   setupNat = ''
+    ${helpers}
     # Create subchain where we store rules
-    iptables -w -t nat -N nixos-nat-pre
-    iptables -w -t nat -N nixos-nat-post
+    ip46tables -w -t nat -N nixos-nat-pre
+    ip46tables -w -t nat -N nixos-nat-post
+    ip46tables -w -t nat -N nixos-nat-out
 
     # We can't match on incoming interface in POSTROUTING, so
     # mark packets coming from the internal interfaces.
@@ -88,8 +95,9 @@ let
     ${cfg.extraCommands}
 
     # Append our chains to the nat tables
-    iptables -w -t nat -A PREROUTING -j nixos-nat-pre
-    iptables -w -t nat -A POSTROUTING -j nixos-nat-post
+    ip46tables -w -t nat -A PREROUTING -j nixos-nat-pre
+    ip46tables -w -t nat -A POSTROUTING -j nixos-nat-post
+    ip46tables -w -t nat -A OUTPUT -j nixos-nat-out
   '';
 
 in
diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix
index 3cf82e8839b..baed83591e1 100644
--- a/nixos/modules/services/networking/unbound.nix
+++ b/nixos/modules/services/networking/unbound.nix
@@ -53,6 +53,13 @@ in
 
       enable = mkEnableOption "Unbound domain name server";
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.unbound;
+        defaultText = "pkgs.unbound";
+        description = "The unbound package to use";
+      };
+
       allowedAccess = mkOption {
         default = [ "127.0.0.0/24" ];
         type = types.listOf types.str;
@@ -94,7 +101,7 @@ in
 
   config = mkIf cfg.enable {
 
-    environment.systemPackages = [ pkgs.unbound ];
+    environment.systemPackages = [ cfg.package ];
 
     users.users.unbound = {
       description = "unbound daemon user";
@@ -114,7 +121,7 @@ in
         mkdir -m 0755 -p ${stateDir}/dev/
         cp ${confFile} ${stateDir}/unbound.conf
         ${optionalString cfg.enableRootTrustAnchor ''
-          ${pkgs.unbound}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
+          ${cfg.package}/bin/unbound-anchor -a ${rootTrustAnchorFile} || echo "Root anchor updated!"
           chown unbound ${stateDir} ${rootTrustAnchorFile}
         ''}
         touch ${stateDir}/dev/random
@@ -122,7 +129,7 @@ in
       '';
 
       serviceConfig = {
-        ExecStart = "${pkgs.unbound}/bin/unbound -d -c ${stateDir}/unbound.conf";
+        ExecStart = "${cfg.package}/bin/unbound -d -c ${stateDir}/unbound.conf";
         ExecStopPost="${pkgs.utillinux}/bin/umount ${stateDir}/dev/random";
 
         ProtectSystem = true;
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index 412f9180375..f7a88867b61 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -71,7 +71,7 @@ in
       };
 
       downloadDirPermissions = mkOption {
-        type = types.string;
+        type = types.str;
         default = "770";
         example = "775";
         description = ''
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index eb90dae94df..9b476ba7f1e 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -47,7 +47,7 @@ let
   ''));
 
   configFile = pkgs.writers.writeNginxConfig "nginx.conf" ''
-    user ${cfg.user} ${cfg.group};
+    pid /run/nginx/nginx.pid;
     error_log ${cfg.logError};
     daemon off;
 
@@ -366,12 +366,7 @@ in
 
       preStart =  mkOption {
         type = types.lines;
-        default = ''
-          test -d ${cfg.stateDir}/logs || mkdir -m 750 -p ${cfg.stateDir}/logs
-          test `stat -c %a ${cfg.stateDir}` = "750" || chmod 750 ${cfg.stateDir}
-          test `stat -c %a ${cfg.stateDir}/logs` = "750" || chmod 750 ${cfg.stateDir}/logs
-          chown -R ${cfg.user}:${cfg.group} ${cfg.stateDir}
-        '';
+        default = "";
         description = "
           Shell commands executed before the service's nginx is started.
         ";
@@ -673,23 +668,35 @@ in
       }
     ];
 
+    systemd.tmpfiles.rules = [
+      "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/logs' 0750 ${cfg.user} ${cfg.group} - -"
+    ];
+
     systemd.services.nginx = {
       description = "Nginx Web Server";
       wantedBy = [ "multi-user.target" ];
       wants = concatLists (map (vhostConfig: ["acme-${vhostConfig.serverName}.service" "acme-selfsigned-${vhostConfig.serverName}.service"]) acmeEnabledVhosts);
       after = [ "network.target" ] ++ map (vhostConfig: "acme-selfsigned-${vhostConfig.serverName}.service") acmeEnabledVhosts;
       stopIfChanged = false;
-      preStart =
-        ''
+      preStart = ''
         ${cfg.preStart}
-        ${cfg.package}/bin/nginx -c ${configPath} -p ${cfg.stateDir} -t
-        '';
+        ${cfg.package}/bin/nginx -c '${configPath}' -p '${cfg.stateDir}' -t
+      '';
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/nginx -c ${configPath} -p ${cfg.stateDir}";
+        ExecStart = "${cfg.package}/bin/nginx -c '${configPath}' -p '${cfg.stateDir}'";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         Restart = "always";
         RestartSec = "10s";
         StartLimitInterval = "1min";
+        # User and group
+        User = cfg.user;
+        Group = cfg.group;
+        # Runtime directory and mode
+        RuntimeDirectory = "nginx";
+        RuntimeDirectoryMode = "0750";
+        # Capabilities
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
       };
     };
 
diff --git a/nixos/modules/services/web-servers/nginx/location-options.nix b/nixos/modules/services/web-servers/nginx/location-options.nix
index 2b3749d8a74..3d9e391ecf2 100644
--- a/nixos/modules/services/web-servers/nginx/location-options.nix
+++ b/nixos/modules/services/web-servers/nginx/location-options.nix
@@ -67,7 +67,7 @@ with lib;
     return = mkOption {
       type = types.nullOr types.str;
       default = null;
-      example = "301 http://example.com$request_uri;";
+      example = "301 http://example.com$request_uri";
       description = ''
         Adds a return directive, for e.g. redirections.
       '';
diff --git a/nixos/modules/services/web-servers/unit/default.nix b/nixos/modules/services/web-servers/unit/default.nix
index 32f6d475b34..b07212580a5 100644
--- a/nixos/modules/services/web-servers/unit/default.nix
+++ b/nixos/modules/services/web-servers/unit/default.nix
@@ -85,7 +85,7 @@ in {
     systemd.tmpfiles.rules = [
       "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
       "d '${cfg.logDir}' 0750 ${cfg.user} ${cfg.group} - -"
-     ];
+    ];
 
     systemd.services.unit = {
       description = "Unit App Server";
@@ -93,23 +93,39 @@ in {
       wantedBy = [ "multi-user.target" ];
       path = with pkgs; [ curl ];
       preStart = ''
-        test -f '/run/unit/control.unit.sock' || rm -f '/run/unit/control.unit.sock'
+        test -f '${cfg.stateDir}/conf.json' || rm -f '${cfg.stateDir}/conf.json'
       '';
       postStart = ''
         curl -X PUT --data-binary '@${configFile}' --unix-socket '/run/unit/control.unit.sock' 'http://localhost/config'
       '';
       serviceConfig = {
-        User = cfg.user;
-        Group = cfg.group;
-        AmbientCapabilities = "CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID";
-        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID";
         ExecStart = ''
           ${cfg.package}/bin/unitd --control 'unix:/run/unit/control.unit.sock' --pid '/run/unit/unit.pid' \
                                    --log '${cfg.logDir}/unit.log' --state '${cfg.stateDir}' --no-daemon \
                                    --user ${cfg.user} --group ${cfg.group}
         '';
+        # User and group
+        User = cfg.user;
+        Group = cfg.group;
+        # Capabilities
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" "CAP_SETGID" "CAP_SETUID" ];
+        # Security
+        NoNewPrivileges = true;
+        # Sanboxing
+        ProtectSystem = "full";
+        ProtectHome = true;
         RuntimeDirectory = "unit";
         RuntimeDirectoryMode = "0750";
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
       };
     };
 
diff --git a/nixos/modules/services/x11/desktop-managers/default.nix b/nixos/modules/services/x11/desktop-managers/default.nix
index 671a959cdde..534551c0c4a 100644
--- a/nixos/modules/services/x11/desktop-managers/default.nix
+++ b/nixos/modules/services/x11/desktop-managers/default.nix
@@ -86,23 +86,14 @@ in
       };
 
       default = mkOption {
-        type = types.str;
-        default = "";
+        type = types.nullOr types.str;
+        default = null;
         example = "none";
-        description = "Default desktop manager loaded if none have been chosen.";
-        apply = defaultDM:
-          if defaultDM == "" && cfg.session.list != [] then
-            (head cfg.session.list).name
-          else if any (w: w.name == defaultDM) cfg.session.list then
-            defaultDM
-          else
-            builtins.trace ''
-              Default desktop manager (${defaultDM}) not found at evaluation time.
-              These are the known valid session names:
-                ${concatMapStringsSep "\n  " (w: "services.xserver.desktopManager.default = \"${w.name}\";") cfg.session.list}
-              It's also possible the default can be found in one of these packages:
-                ${concatMapStringsSep "\n  " (p: p.name) config.services.xserver.displayManager.extraSessionFilePackages}
-            '' defaultDM;
+        description = ''
+          <emphasis role="strong">Deprecated</emphasis>, please use <xref linkend="opt-services.xserver.displayManager.defaultSession"/> instead.
+
+          Default desktop manager loaded if none have been chosen.
+        '';
       };
 
     };
diff --git a/nixos/modules/services/x11/desktop-managers/gnome3.nix b/nixos/modules/services/x11/desktop-managers/gnome3.nix
index 6725595e1cf..6d9bd284bc7 100644
--- a/nixos/modules/services/x11/desktop-managers/gnome3.nix
+++ b/nixos/modules/services/x11/desktop-managers/gnome3.nix
@@ -144,7 +144,7 @@ in
       services.gnome3.core-shell.enable = true;
       services.gnome3.core-utilities.enable = mkDefault true;
 
-      services.xserver.displayManager.extraSessionFilePackages = [ pkgs.gnome3.gnome-session ];
+      services.xserver.displayManager.sessionPackages = [ pkgs.gnome3.gnome-session ];
 
       environment.extraInit = ''
         ${concatMapStrings (p: ''
@@ -171,7 +171,7 @@ in
     })
 
     (mkIf flashbackEnabled {
-      services.xserver.displayManager.extraSessionFilePackages =  map
+      services.xserver.displayManager.sessionPackages =  map
         (wm: pkgs.gnome3.gnome-flashback.mkSessionForWm {
           inherit (wm) wmName wmLabel wmCommand;
         }) (optional cfg.flashback.enableMetacity {
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
index 99db5b17b64..e07d5b5eaad 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -69,7 +69,7 @@ in
 
   config = mkIf cfg.enable {
 
-    services.xserver.displayManager.extraSessionFilePackages = [ pkgs.pantheon.elementary-session-settings ];
+    services.xserver.displayManager.sessionPackages = [ pkgs.pantheon.elementary-session-settings ];
 
     # Ensure lightdm is used when Pantheon is enabled
     # Without it screen locking will be nonfunctional because of the use of lightlocker
@@ -81,9 +81,9 @@ in
 
     services.xserver.displayManager.lightdm.greeters.pantheon.enable = mkDefault true;
 
-    # If not set manually Pantheon session cannot be started
-    # Known issue of https://github.com/NixOS/nixpkgs/pull/43992
-    services.xserver.desktopManager.default = mkForce "pantheon";
+    # Without this, Elementary LightDM greeter will pre-select non-existent `default` session
+    # https://github.com/elementary/greeter/issues/368
+    services.xserver.displayManager.defaultSession = "pantheon";
 
     services.xserver.displayManager.sessionCommands = ''
       if test "$XDG_CURRENT_DESKTOP" = "Pantheon"; then
diff --git a/nixos/modules/services/x11/desktop-managers/surf-display.nix b/nixos/modules/services/x11/desktop-managers/surf-display.nix
index 140dde828da..9aeb0bbd2a8 100644
--- a/nixos/modules/services/x11/desktop-managers/surf-display.nix
+++ b/nixos/modules/services/x11/desktop-managers/surf-display.nix
@@ -118,7 +118,7 @@ in {
   };
 
   config = mkIf cfg.enable {
-    services.xserver.displayManager.extraSessionFilePackages = [
+    services.xserver.displayManager.sessionPackages = [
       pkgs.surf-display
     ];
 
diff --git a/nixos/modules/services/x11/display-managers/account-service-util.nix b/nixos/modules/services/x11/display-managers/account-service-util.nix
new file mode 100644
index 00000000000..1dbe703b566
--- /dev/null
+++ b/nixos/modules/services/x11/display-managers/account-service-util.nix
@@ -0,0 +1,39 @@
+{ accountsservice
+, glib
+, gobject-introspection
+, python3
+, wrapGAppsHook
+}:
+
+python3.pkgs.buildPythonApplication {
+  name = "set-session";
+
+  format = "other";
+
+  src = ./set-session.py;
+
+  dontUnpack = true;
+
+  strictDeps = false;
+
+  nativeBuildInputs = [
+    wrapGAppsHook
+    gobject-introspection
+  ];
+
+  buildInputs = [
+    accountsservice
+    glib
+  ];
+
+  propagatedBuildInputs = with python3.pkgs; [
+    pygobject3
+    ordered-set
+  ];
+
+  installPhase = ''
+    mkdir -p $out/bin
+    cp $src $out/bin/set-session
+    chmod +x $out/bin/set-session
+  '';
+}
diff --git a/nixos/modules/services/x11/display-managers/default.nix b/nixos/modules/services/x11/display-managers/default.nix
index c252c3c90c9..2d809b5cc9f 100644
--- a/nixos/modules/services/x11/display-managers/default.nix
+++ b/nixos/modules/services/x11/display-managers/default.nix
@@ -27,16 +27,7 @@ let
     Xft.hintstyle: hintslight
   '';
 
-  mkCases = session:
-    concatStrings (
-      mapAttrsToList (name: starts: ''
-                       (${name})
-                         ${concatMapStringsSep "\n  " (n: n.start) starts}
-                         ;;
-                     '') (lib.groupBy (n: n.name) session)
-    );
-
-  # file provided by services.xserver.displayManager.session.wrapper
+  # file provided by services.xserver.displayManager.sessionData.wrapper
   xsessionWrapper = pkgs.writeScript "xsession-wrapper"
     ''
       #! ${pkgs.bash}/bin/bash
@@ -116,94 +107,44 @@ let
           # Run the supplied session command. Remove any double quotes with eval.
           eval exec "$@"
       else
-          # Fall back to the default window/desktopManager
-          exec ${cfg.displayManager.session.script}
+          # TODO: Do we need this? Should not the session always exist?
+          echo "error: unknown session $1" 1>&2
+          exit 1
       fi
     '';
 
-  # file provided by services.xserver.displayManager.session.script
-  xsession = wm: dm: pkgs.writeScript "xsession"
-    ''
-      #! ${pkgs.bash}/bin/bash
-
-      # Legacy session script used to construct .desktop files from
-      # `services.xserver.displayManager.session` entries. Called from
-      # `sessionWrapper`.
-
-      # Expected parameters:
-      #   $1 = <desktop-manager>+<window-manager>
-
-      # The first argument of this script is the session type.
-      sessionType="$1"
-      if [ "$sessionType" = default ]; then sessionType=""; fi
-
-      # The session type is "<desktop-manager>+<window-manager>", so
-      # extract those (see:
-      # http://wiki.bash-hackers.org/syntax/pe#substring_removal).
-      windowManager="''${sessionType##*+}"
-      : ''${windowManager:=${cfg.windowManager.default}}
-      desktopManager="''${sessionType%%+*}"
-      : ''${desktopManager:=${cfg.desktopManager.default}}
-
-      # Start the window manager.
-      case "$windowManager" in
-        ${mkCases wm}
-        (*) echo "$0: Window manager '$windowManager' not found.";;
-      esac
-
-      # Start the desktop manager.
-      case "$desktopManager" in
-        ${mkCases dm}
-        (*) echo "$0: Desktop manager '$desktopManager' not found.";;
-      esac
-
-      ${optionalString cfg.updateDbusEnvironment ''
-        ${lib.getBin pkgs.dbus}/bin/dbus-update-activation-environment --systemd --all
-      ''}
-
-      test -n "$waitPID" && wait "$waitPID"
-
-      ${config.systemd.package}/bin/systemctl --user stop graphical-session.target
-
-      exit 0
-    '';
-
-  # Desktop Entry Specification:
-  # - https://standards.freedesktop.org/desktop-entry-spec/latest/
-  # - https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
-  mkDesktops = names: pkgs.runCommand "desktops"
+  installedSessions = pkgs.runCommand "desktops"
     { # trivial derivation
       preferLocalBuild = true;
       allowSubstitutes = false;
     }
     ''
-      mkdir -p "$out/share/xsessions"
-      ${concatMapStrings (n: ''
-        cat - > "$out/share/xsessions/${n}.desktop" << EODESKTOP
-        [Desktop Entry]
-        Version=1.0
-        Type=XSession
-        TryExec=${cfg.displayManager.session.script}
-        Exec=${cfg.displayManager.session.script} "${n}"
-        Name=${n}
-        Comment=
-        EODESKTOP
-      '') names}
+      mkdir -p "$out/share/"{xsessions,wayland-sessions}
 
       ${concatMapStrings (pkg: ''
+        for n in ${concatStringsSep " " pkg.providedSessions}; do
+          if ! test -f ${pkg}/share/wayland-sessions/$n.desktop -o \
+                    -f ${pkg}/share/xsessions/$n.desktop; then
+            echo "Couldn't find provided session name, $n.desktop, in session package ${pkg.name}:"
+            echo "  ${pkg}"
+            return 1
+          fi
+        done
+
         if test -d ${pkg}/share/xsessions; then
           ${xorg.lndir}/bin/lndir ${pkg}/share/xsessions $out/share/xsessions
         fi
-      '') cfg.displayManager.extraSessionFilePackages}
-
-      ${concatMapStrings (pkg: ''
         if test -d ${pkg}/share/wayland-sessions; then
-          mkdir -p "$out/share/wayland-sessions"
           ${xorg.lndir}/bin/lndir ${pkg}/share/wayland-sessions $out/share/wayland-sessions
         fi
-      '') cfg.displayManager.extraSessionFilePackages}
+      '') cfg.displayManager.sessionPackages}
     '';
 
+  dmDefault = cfg.desktopManager.default;
+  wmDefault = cfg.windowManager.default;
+
+  defaultSessionFromLegacyOptions = concatStringsSep "+" (filter (s: s != null) ([ dmDefault ] ++ optional (wmDefault != "none") wmDefault));
+
 in
 
 {
@@ -261,11 +202,24 @@ in
         '';
       };
 
-      extraSessionFilePackages = mkOption {
-        type = types.listOf types.package;
+      sessionPackages = mkOption {
+        type = with types; listOf (package // {
+          description = "package with provided sessions";
+          check = p: assertMsg
+            (package.check p && p ? providedSessions
+            && p.providedSessions != [] && all isString p.providedSessions)
+            ''
+              Package, '${p.name}', did not specify any session names, as strings, in
+              'passthru.providedSessions'. This is required when used as a session package.
+
+              The session names can be looked up in:
+                ${p}/share/xsessions
+                ${p}/share/wayland-sessions
+           '';
+        });
         default = [];
         description = ''
-          A list of packages containing xsession files to be passed to the display manager.
+          A list of packages containing x11 or wayland session files to be passed to the display manager.
         '';
       };
 
@@ -296,18 +250,50 @@ in
           inside the display manager with the desktop manager name
           followed by the window manager name.
         '';
-        apply = list: rec {
-          wm = filter (s: s.manage == "window") list;
-          dm = filter (s: s.manage == "desktop") list;
-          names = flip concatMap dm
-            (d: map (w: d.name + optionalString (w.name != "none") ("+" + w.name))
-              (filter (w: d.name != "none" || w.name != "none") wm));
-          desktops = mkDesktops names;
-          script = xsession wm dm;
+      };
+
+      sessionData = mkOption {
+        description = "Data exported for display managers’ convenience";
+        internal = true;
+        default = {};
+        apply = val: {
           wrapper = xsessionWrapper;
+          desktops = installedSessions;
+          sessionNames = concatMap (p: p.providedSessions) cfg.displayManager.sessionPackages;
+          # We do not want to force users to set defaultSession when they have only single DE.
+          autologinSession =
+            if cfg.displayManager.defaultSession != null then
+              cfg.displayManager.defaultSession
+            else if cfg.displayManager.sessionData.sessionNames != [] then
+              head cfg.displayManager.sessionData.sessionNames
+            else
+              null;
         };
       };
 
+      defaultSession = mkOption {
+        type = with types; nullOr str // {
+          description = "session name";
+          check = d:
+            assertMsg (d != null -> (str.check d && elem d cfg.displayManager.sessionData.sessionNames)) ''
+                Default graphical session, '${d}', not found.
+                Valid names for 'services.xserver.displayManager.defaultSession' are:
+                  ${concatStringsSep "\n  " cfg.displayManager.sessionData.sessionNames}
+              '';
+        };
+        default =
+          if dmDefault != null || wmDefault != null then
+            defaultSessionFromLegacyOptions
+          else
+            null;
+        example = "gnome";
+        description = ''
+          Graphical session to pre-select in the session chooser (only effective for GDM and LightDM).
+
+          On GDM, LightDM and SDDM, it will also be used as a session for auto-login.
+        '';
+      };
+
       job = {
 
         preStart = mkOption {
@@ -356,6 +342,27 @@ in
   };
 
   config = {
+    assertions = [
+      {
+        assertion = cfg.desktopManager.default != null || cfg.windowManager.default != null -> cfg.displayManager.defaultSession == defaultSessionFromLegacyOptions;
+        message = "You cannot use both services.xserver.displayManager.defaultSession option and legacy options (services.xserver.desktopManager.default and services.xserver.windowManager.default).";
+      }
+    ];
+
+    warnings =
+      mkIf (dmDefault != null || wmDefault != null) [
+        ''
+          The following options are deprecated:
+            ${concatStringsSep "\n  " (map ({c, t}: t) (filter ({c, t}: c != null) [
+            { c = dmDefault; t = "- services.xserver.desktopManager.default"; }
+            { c = wmDefault; t = "- services.xserver.windowManager.default"; }
+            ]))}
+          Please use
+            services.xserver.displayManager.defaultSession = "${concatStringsSep "+" (filter (s: s != null) [ dmDefault wmDefault ])}";
+          instead.
+        ''
+      ];
+
     services.xserver.displayManager.xserverBin = "${xorg.xorgserver.out}/bin/X";
 
     systemd.user.targets.graphical-session = {
@@ -364,6 +371,67 @@ in
         StopWhenUnneeded = false;
       };
     };
+
+    # Create desktop files and scripts for starting sessions for WMs/DMs
+    # that do not have upstream session files (those defined using services.{display,desktop,window}Manager.session options).
+    services.xserver.displayManager.sessionPackages =
+      let
+        dms = filter (s: s.manage == "desktop") cfg.displayManager.session;
+        wms = filter (s: s.manage == "window") cfg.displayManager.session;
+
+        # Script responsible for starting the window manager and the desktop manager.
+        xsession = wm: dm: pkgs.writeScript "xsession" ''
+          #! ${pkgs.bash}/bin/bash
+
+          # Legacy session script used to construct .desktop files from
+          # `services.xserver.displayManager.session` entries. Called from
+          # `sessionWrapper`.
+
+          # Start the window manager.
+          ${wm.start}
+
+          # Start the desktop manager.
+          ${dm.start}
+
+          ${optionalString cfg.updateDbusEnvironment ''
+            ${lib.getBin pkgs.dbus}/bin/dbus-update-activation-environment --systemd --all
+          ''}
+
+          test -n "$waitPID" && wait "$waitPID"
+
+          ${config.systemd.package}/bin/systemctl --user stop graphical-session.target
+
+          exit 0
+        '';
+      in
+        # We will generate every possible pair of WM and DM.
+        concatLists (
+          crossLists
+            (dm: wm: let
+              sessionName = "${dm.name}${optionalString (wm.name != "none") ("+" + wm.name)}";
+              script = xsession dm wm;
+            in
+              optional (dm.name != "none" || wm.name != "none")
+                (pkgs.writeTextFile {
+                  name = "${sessionName}-xsession";
+                  destination = "/share/xsessions/${sessionName}.desktop";
+                  # Desktop Entry Specification:
+                  # - https://standards.freedesktop.org/desktop-entry-spec/latest/
+                  # - https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
+                  text = ''
+                    [Desktop Entry]
+                    Version=1.0
+                    Type=XSession
+                    TryExec=${script}
+                    Exec=${script}
+                    Name=${sessionName}
+                  '';
+                } // {
+                  providedSessions = [ sessionName ];
+                })
+            )
+            [dms wms]
+          );
   };
 
   imports = [
@@ -371,6 +439,7 @@ in
      "The option is no longer necessary because all display managers have already delegated lid management to systemd.")
     (mkRenamedOptionModule [ "services" "xserver" "displayManager" "job" "logsXsession" ] [ "services" "xserver" "displayManager" "job" "logToFile" ])
     (mkRenamedOptionModule [ "services" "xserver" "displayManager" "logToJournal" ] [ "services" "xserver" "displayManager" "job" "logToJournal" ])
+    (mkRenamedOptionModule [ "services" "xserver" "displayManager" "extraSessionFilesPackages" ] [ "services" "xserver" "displayManager" "sessionPackages" ])
   ];
 
 }
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index 29a80aac6e6..6630f012f04 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -31,44 +31,9 @@ let
     load-module module-position-event-sounds
   '';
 
-  dmDefault = config.services.xserver.desktopManager.default;
-  wmDefault = config.services.xserver.windowManager.default;
-  hasDefaultUserSession = dmDefault != "none" || wmDefault != "none";
-  defaultSessionName = dmDefault + optionalString (wmDefault != "none") ("+" + wmDefault);
-
-  setSessionScript = pkgs.python3.pkgs.buildPythonApplication {
-    name = "set-session";
-
-    format = "other";
-
-    src = ./set-session.py;
-
-    dontUnpack = true;
-
-    strictDeps = false;
-
-    nativeBuildInputs = with pkgs; [
-      wrapGAppsHook
-      gobject-introspection
-    ];
-
-    buildInputs = with pkgs; [
-      accountsservice
-      glib
-    ];
-
-    propagatedBuildInputs = with pkgs.python3.pkgs; [
-      pygobject3
-      ordered-set
-    ];
-
-    installPhase = ''
-      mkdir -p $out/bin
-      cp $src $out/bin/set-session
-      chmod +x $out/bin/set-session
-    '';
-  };
+  defaultSessionName = config.services.xserver.displayManager.defaultSession;
 
+  setSessionScript = pkgs.callPackage ./account-service-util.nix { };
 in
 
 {
@@ -186,7 +151,7 @@ in
         environment = {
           GDM_X_SERVER_EXTRA_ARGS = toString
             (filter (arg: arg != "-terminate") cfg.xserverArgs);
-          XDG_DATA_DIRS = "${cfg.session.desktops}/share/";
+          XDG_DATA_DIRS = "${cfg.sessionData.desktops}/share/";
         } // optionalAttrs (xSessionWrapper != null) {
           # Make GDM use this wrapper before running the session, which runs the
           # configured setupCommands. This relies on a patched GDM which supports
@@ -204,13 +169,19 @@ in
           cat - > /run/gdm/.config/gnome-initial-setup-done <<- EOF
           yes
           EOF
-        '' + optionalString hasDefaultUserSession ''
-          ${setSessionScript}/bin/set-session ${defaultSessionName}
+        '' + optionalString (defaultSessionName != null) ''
+          # Set default session in session chooser to a specified values – basically ignore session history.
+          ${setSessionScript}/bin/set-session ${cfg.sessionData.autologinSession}
         '';
       };
 
-    # Because sd_login_monitor_new requires /run/systemd/machines
-    systemd.services.display-manager.wants = [ "systemd-machined.service" ];
+    systemd.services.display-manager.wants = [
+      # Because sd_login_monitor_new requires /run/systemd/machines
+      "systemd-machined.service"
+      # setSessionScript wants AccountsService
+      "accounts-daemon.service"
+    ];
+
     systemd.services.display-manager.after = [
       "rc-local.service"
       "systemd-machined.service"
@@ -326,7 +297,7 @@ in
       ${optionalString cfg.gdm.debug "Enable=true"}
     '';
 
-    environment.etc."gdm/Xsession".source = config.services.xserver.displayManager.session.wrapper;
+    environment.etc."gdm/Xsession".source = config.services.xserver.displayManager.sessionData.wrapper;
 
     # GDM LFS PAM modules, adapted somehow to NixOS
     security.pam.services = {
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix
index fa9445af32e..0025f9b3603 100644
--- a/nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/mini.nix
@@ -53,9 +53,8 @@ in
           Whether to enable lightdm-mini-greeter as the lightdm greeter.
 
           Note that this greeter starts only the default X session.
-          You can configure the default X session by
-          <option>services.xserver.desktopManager.default</option> and
-          <option>services.xserver.windowManager.default</option>.
+          You can configure the default X session using
+          <xref linkend="opt-services.xserver.displayManager.defaultSession"/>.
         '';
       };
 
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
index 29cb6ccbc06..77c94114e6d 100644
--- a/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/pantheon.nix
@@ -35,6 +35,9 @@ in
       name = "io.elementary.greeter";
     };
 
+    # Show manual login card.
+    services.xserver.displayManager.lightdm.extraSeatDefaults = "greeter-show-manual-login=true";
+
     environment.etc."lightdm/io.elementary.greeter.conf".source = "${pkgs.pantheon.elementary-greeter}/etc/lightdm/io.elementary.greeter.conf";
     environment.etc."wingpanel.d/io.elementary.greeter.whitelist".source = "${pkgs.pantheon.elementary-default-settings}/etc/wingpanel.d/io.elementary.greeter.whitelist";
 
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
index cf4c05acbcc..f7face0adb7 100644
--- a/nixos/modules/services/x11/display-managers/lightdm.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -8,10 +8,9 @@ let
   dmcfg = xcfg.displayManager;
   xEnv = config.systemd.services.display-manager.environment;
   cfg = dmcfg.lightdm;
+  sessionData = dmcfg.sessionData;
 
-  dmDefault = xcfg.desktopManager.default;
-  wmDefault = xcfg.windowManager.default;
-  hasDefaultUserSession = dmDefault != "none" || wmDefault != "none";
+  setSessionScript = pkgs.callPackage ./account-service-util.nix { };
 
   inherit (pkgs) lightdm writeScript writeText;
 
@@ -45,22 +44,19 @@ let
         greeter-user = ${config.users.users.lightdm.name}
         greeters-directory = ${cfg.greeter.package}
       ''}
-      sessions-directory = ${dmcfg.session.desktops}/share/xsessions
+      sessions-directory = ${dmcfg.sessionData.desktops}/share/xsessions:${dmcfg.sessionData.desktops}/share/wayland-sessions
       ${cfg.extraConfig}
 
       [Seat:*]
       xserver-command = ${xserverWrapper}
-      session-wrapper = ${dmcfg.session.wrapper}
+      session-wrapper = ${dmcfg.sessionData.wrapper}
       ${optionalString cfg.greeter.enable ''
         greeter-session = ${cfg.greeter.name}
       ''}
       ${optionalString cfg.autoLogin.enable ''
         autologin-user = ${cfg.autoLogin.user}
         autologin-user-timeout = ${toString cfg.autoLogin.timeout}
-        autologin-session = ${defaultSessionName}
-      ''}
-      ${optionalString hasDefaultUserSession ''
-        user-session=${defaultSessionName}
+        autologin-session = ${sessionData.autologinSession}
       ''}
       ${optionalString (dmcfg.setupCommands != "") ''
         display-setup-script=${pkgs.writeScript "lightdm-display-setup" ''
@@ -71,7 +67,6 @@ let
       ${cfg.extraSeatDefaults}
     '';
 
-  defaultSessionName = dmDefault + optionalString (wmDefault != "none") ("+" + wmDefault);
 in
 {
   # Note: the order in which lightdm greeter modules are imported
@@ -199,11 +194,9 @@ in
           LightDM auto-login requires services.xserver.displayManager.lightdm.autoLogin.user to be set
         '';
       }
-      { assertion = cfg.autoLogin.enable -> dmDefault != "none" || wmDefault != "none";
+      { assertion = cfg.autoLogin.enable -> sessionData.autologinSession != null;
         message = ''
-          LightDM auto-login requires that services.xserver.desktopManager.default and
-          services.xserver.windowManager.default are set to valid values. The current
-          default session: ${defaultSessionName} is not valid.
+          LightDM auto-login requires that services.xserver.displayManager.defaultSession is set.
         '';
       }
       { assertion = !cfg.greeter.enable -> (cfg.autoLogin.enable && cfg.autoLogin.timeout == 0);
@@ -214,6 +207,20 @@ in
       }
     ];
 
+    # Set default session in session chooser to a specified values – basically ignore session history.
+    # Auto-login is already covered by a config value.
+    services.xserver.displayManager.job.preStart = optionalString (!cfg.autoLogin.enable && dmcfg.defaultSession != null) ''
+      ${setSessionScript}/bin/set-session ${dmcfg.defaultSession}
+    '';
+
+    # setSessionScript needs session-files in XDG_DATA_DIRS
+    services.xserver.displayManager.job.environment.XDG_DATA_DIRS = "${dmcfg.sessionData.desktops}/share/";
+
+    # setSessionScript wants AccountsService
+    systemd.services.display-manager.wants = [
+      "accounts-daemon.service"
+    ];
+
     # lightdm relaunches itself via just `lightdm`, so needs to be on the PATH
     services.xserver.displayManager.job.execCmd = ''
       export PATH=${lightdm}/sbin:$PATH
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
index 24e120c4f2c..4224c557ed6 100644
--- a/nixos/modules/services/x11/display-managers/sddm.nix
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -50,8 +50,8 @@ let
     MinimumVT=${toString (if xcfg.tty != null then xcfg.tty else 7)}
     ServerPath=${xserverWrapper}
     XephyrPath=${pkgs.xorg.xorgserver.out}/bin/Xephyr
-    SessionCommand=${dmcfg.session.wrapper}
-    SessionDir=${dmcfg.session.desktops}/share/xsessions
+    SessionCommand=${dmcfg.sessionData.wrapper}
+    SessionDir=${dmcfg.sessionData.desktops}/share/xsessions
     XauthPath=${pkgs.xorg.xauth}/bin/xauth
     DisplayCommand=${Xsetup}
     DisplayStopCommand=${Xstop}
@@ -59,23 +59,19 @@ let
 
     [Wayland]
     EnableHidpi=${if cfg.enableHidpi then "true" else "false"}
-    SessionDir=${dmcfg.session.desktops}/share/wayland-sessions
+    SessionDir=${dmcfg.sessionData.desktops}/share/wayland-sessions
 
     ${optionalString cfg.autoLogin.enable ''
     [Autologin]
     User=${cfg.autoLogin.user}
-    Session=${defaultSessionName}.desktop
+    Session=${autoLoginSessionName}.desktop
     Relogin=${boolToString cfg.autoLogin.relogin}
     ''}
 
     ${cfg.extraConfig}
   '';
 
-  defaultSessionName =
-    let
-      dm = xcfg.desktopManager.default;
-      wm = xcfg.windowManager.default;
-    in dm + optionalString (wm != "none") ("+" + wm);
+  autoLoginSessionName = dmcfg.sessionData.autologinSession;
 
 in
 {
@@ -210,11 +206,9 @@ in
           SDDM auto-login requires services.xserver.displayManager.sddm.autoLogin.user to be set
         '';
       }
-      { assertion = cfg.autoLogin.enable -> elem defaultSessionName dmcfg.session.names;
+      { assertion = cfg.autoLogin.enable -> autoLoginSessionName != null;
         message = ''
-          SDDM auto-login requires that services.xserver.desktopManager.default and
-          services.xserver.windowManager.default are set to valid values. The current
-          default session: ${defaultSessionName} is not valid.
+          SDDM auto-login requires that services.xserver.displayManager.defaultSession is set.
         '';
       }
     ];
diff --git a/nixos/modules/services/x11/imwheel.nix b/nixos/modules/services/x11/imwheel.nix
new file mode 100644
index 00000000000..3923df498e7
--- /dev/null
+++ b/nixos/modules/services/x11/imwheel.nix
@@ -0,0 +1,68 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.xserver.imwheel;
+in
+  {
+    options = {
+      services.xserver.imwheel = {
+        enable = mkEnableOption "IMWheel service";
+
+        extraOptions = mkOption {
+          type = types.listOf types.str;
+          default = [ "--buttons=45" ];
+          example = [ "--debug" ];
+          description = ''
+            Additional command-line arguments to pass to
+            <command>imwheel</command>.
+          '';
+        };
+
+        rules = mkOption {
+          type = types.attrsOf types.str;
+          default = {};
+          example = literalExample ''
+            ".*" = '''
+              None,      Up,   Button4, 8
+              None,      Down, Button5, 8
+              Shift_L,   Up,   Shift_L|Button4, 4
+              Shift_L,   Down, Shift_L|Button5, 4
+              Control_L, Up,   Control_L|Button4
+              Control_L, Down, Control_L|Button5
+            ''';
+          '';
+          description = ''
+            Window class translation rules.
+            /etc/X11/imwheelrc is generated based on this config
+            which means this config is global for all users.
+            See <link xlink:href="http://imwheel.sourceforge.net/imwheel.1.html">offical man pages</link>
+            for more informations.
+          '';
+        };
+      };
+    };
+
+    config = mkIf cfg.enable {
+      environment.systemPackages = [ pkgs.imwheel ];
+
+      environment.etc."X11/imwheel/imwheelrc".source =
+        pkgs.writeText "imwheelrc" (concatStringsSep "\n\n"
+          (mapAttrsToList
+            (rule: conf: "\"${rule}\"\n${conf}") cfg.rules
+          ));
+
+      systemd.user.services.imwheel = {
+        description = "imwheel service";
+        wantedBy = [ "graphical-session.target" ];
+        partOf = [ "graphical-session.target" ];
+        serviceConfig = {
+          ExecStart = "${pkgs.imwheel}/bin/imwheel " + escapeShellArgs ([
+            "--detach"
+            "--kill"
+          ] ++ cfg.extraOptions);
+          ExecStop = "${pkgs.procps}/bin/pkill imwheel";
+          Restart = "on-failure";
+        };
+      };
+    };
+  }
diff --git a/nixos/modules/services/x11/window-managers/default.nix b/nixos/modules/services/x11/window-managers/default.nix
index c17f3830d0e..04a9fc46628 100644
--- a/nixos/modules/services/x11/window-managers/default.nix
+++ b/nixos/modules/services/x11/window-managers/default.nix
@@ -59,15 +59,14 @@ in
       };
 
       default = mkOption {
-        type = types.str;
-        default = "none";
+        type = types.nullOr types.str;
+        default = null;
         example = "wmii";
-        description = "Default window manager loaded if none have been chosen.";
-        apply = defaultWM:
-          if any (w: w.name == defaultWM) cfg.session then
-            defaultWM
-          else
-            throw "Default window manager (${defaultWM}) not found.";
+        description = ''
+          <emphasis role="strong">Deprecated</emphasis>, please use <xref linkend="opt-services.xserver.displayManager.defaultSession"/> instead.
+
+          Default window manager loaded if none have been chosen.
+        '';
       };
 
     };
diff --git a/nixos/modules/tasks/filesystems/nfs.nix b/nixos/modules/tasks/filesystems/nfs.nix
index e0e8bb1f03d..ddcc0ed8f5a 100644
--- a/nixos/modules/tasks/filesystems/nfs.nix
+++ b/nixos/modules/tasks/filesystems/nfs.nix
@@ -25,6 +25,9 @@ let
   '';
 
   nfsConfFile = pkgs.writeText "nfs.conf" cfg.extraConfig;
+  requestKeyConfFile = pkgs.writeText "request-key.conf" ''
+    create id_resolver * * ${pkgs.nfs-utils}/bin/nfsidmap -t 600 %k %d
+  '';
 
   cfg = config.services.nfs;
 
@@ -57,9 +60,12 @@ in
 
     systemd.packages = [ pkgs.nfs-utils ];
 
+    environment.systemPackages = [ pkgs.keyutils ];
+
     environment.etc = {
       "idmapd.conf".source = idmapdConfFile;
       "nfs.conf".source = nfsConfFile;
+      "request-key.conf".source = requestKeyConfFile;
     };
 
     systemd.services.nfs-blkmap =
diff --git a/nixos/modules/tasks/kbd.nix b/nixos/modules/tasks/kbd.nix
deleted file mode 100644
index c6ba998b19e..00000000000
--- a/nixos/modules/tasks/kbd.nix
+++ /dev/null
@@ -1,127 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  makeColor = n: value: "COLOR_${toString n}=${value}";
-  makeColorCS =
-    let positions = [ "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F" ];
-    in n: value: "\\033]P${elemAt positions (n - 1)}${value}";
-  colors = concatImapStringsSep "\n" makeColor config.i18n.consoleColors;
-
-  isUnicode = hasSuffix "UTF-8" (toUpper config.i18n.defaultLocale);
-
-  optimizedKeymap = pkgs.runCommand "keymap" {
-    nativeBuildInputs = [ pkgs.buildPackages.kbd ];
-    LOADKEYS_KEYMAP_PATH = "${kbdEnv}/share/keymaps/**";
-    preferLocalBuild = true;
-  } ''
-    loadkeys -b ${optionalString isUnicode "-u"} "${config.i18n.consoleKeyMap}" > $out
-  '';
-
-  # Sadly, systemd-vconsole-setup doesn't support binary keymaps.
-  vconsoleConf = pkgs.writeText "vconsole.conf" ''
-    KEYMAP=${config.i18n.consoleKeyMap}
-    FONT=${config.i18n.consoleFont}
-    ${colors}
-  '';
-
-  kbdEnv = pkgs.buildEnv {
-    name = "kbd-env";
-    paths = [ pkgs.kbd ] ++ config.i18n.consolePackages;
-    pathsToLink = [ "/share/consolefonts" "/share/consoletrans" "/share/keymaps" "/share/unimaps" ];
-  };
-
-  setVconsole = !config.boot.isContainer;
-in
-
-{
-  ###### interface
-
-  options = {
-
-    # most options are defined in i18n.nix
-
-    # FIXME: still needed?
-    boot.extraTTYs = mkOption {
-      default = [];
-      type = types.listOf types.str;
-      example = ["tty8" "tty9"];
-      description = ''
-        Tty (virtual console) devices, in addition to the consoles on
-        which mingetty and syslogd run, that must be initialised.
-        Only useful if you have some program that you want to run on
-        some fixed console.  For example, the NixOS installation CD
-        opens the manual in a web browser on console 7, so it sets
-        <option>boot.extraTTYs</option> to <literal>["tty7"]</literal>.
-      '';
-    };
-
-    boot.earlyVconsoleSetup = mkOption {
-      default = false;
-      type = types.bool;
-      description = ''
-        Enable setting font as early as possible (in initrd).
-      '';
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkMerge [
-    (mkIf (!setVconsole) {
-      systemd.services.systemd-vconsole-setup.enable = false;
-    })
-
-    (mkIf setVconsole (mkMerge [
-      { environment.systemPackages = [ pkgs.kbd ];
-
-        # Let systemd-vconsole-setup.service do the work of setting up the
-        # virtual consoles.
-        environment.etc."vconsole.conf".source = vconsoleConf;
-        # Provide kbd with additional packages.
-        environment.etc.kbd.source = "${kbdEnv}/share";
-
-        boot.initrd.preLVMCommands = mkBefore ''
-          kbd_mode ${if isUnicode then "-u" else "-a"} -C /dev/console
-          printf "\033%%${if isUnicode then "G" else "@"}" >> /dev/console
-          loadkmap < ${optimizedKeymap}
-
-          ${optionalString config.boot.earlyVconsoleSetup ''
-            setfont -C /dev/console $extraUtils/share/consolefonts/font.psf
-          ''}
-
-          ${concatImapStringsSep "\n" (n: color: ''
-            printf "${makeColorCS n color}" >> /dev/console
-          '') config.i18n.consoleColors}
-        '';
-
-        systemd.services.systemd-vconsole-setup =
-          { before = [ "display-manager.service" ];
-            after = [ "systemd-udev-settle.service" ];
-            restartTriggers = [ vconsoleConf kbdEnv ];
-          };
-      }
-
-      (mkIf config.boot.earlyVconsoleSetup {
-        boot.initrd.extraUtilsCommands = ''
-          mkdir -p $out/share/consolefonts
-          ${if substring 0 1 config.i18n.consoleFont == "/" then ''
-            font="${config.i18n.consoleFont}"
-          '' else ''
-            font="$(echo ${kbdEnv}/share/consolefonts/${config.i18n.consoleFont}.*)"
-          ''}
-          if [[ $font == *.gz ]]; then
-            gzip -cd $font > $out/share/consolefonts/font.psf
-          else
-            cp -L $font $out/share/consolefonts/font.psf
-          fi
-        '';
-      })
-    ]))
-  ];
-
-}
diff --git a/nixos/modules/tasks/network-interfaces-systemd.nix b/nixos/modules/tasks/network-interfaces-systemd.nix
index 9ffa1089ee6..e25dc0c0b39 100644
--- a/nixos/modules/tasks/network-interfaces-systemd.nix
+++ b/nixos/modules/tasks/network-interfaces-systemd.nix
@@ -60,8 +60,8 @@ in
       let
         domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain);
         genericNetwork = override:
-          let gateway = optional (cfg.defaultGateway != null) cfg.defaultGateway.address
-            ++ optional (cfg.defaultGateway6 != null) cfg.defaultGateway6.address;
+          let gateway = optional (cfg.defaultGateway != null && (cfg.defaultGateway.address or "") != "") cfg.defaultGateway.address
+            ++ optional (cfg.defaultGateway6 != null && (cfg.defaultGateway6.address or "") != "") cfg.defaultGateway6.address;
           in optionalAttrs (gateway != [ ]) {
             routes = override [
               {
diff --git a/nixos/modules/virtualisation/container-config.nix b/nixos/modules/virtualisation/container-config.nix
index f7a37d8c9f3..6ff6bdd30c2 100644
--- a/nixos/modules/virtualisation/container-config.nix
+++ b/nixos/modules/virtualisation/container-config.nix
@@ -10,6 +10,7 @@ with lib;
     nix.optimise.automatic = mkDefault false; # the store is host managed
     services.udisks2.enable = mkDefault false;
     powerManagement.enable = mkDefault false;
+    documentation.nixos.enable = mkDefault false;
 
     networking.useHostResolvConf = mkDefault true;
 
diff --git a/nixos/modules/virtualisation/lxd.nix b/nixos/modules/virtualisation/lxd.nix
index 505c11abd20..b4934a86cf5 100644
--- a/nixos/modules/virtualisation/lxd.nix
+++ b/nixos/modules/virtualisation/lxd.nix
@@ -35,6 +35,18 @@ in
           with nixos.
         '';
       };
+      recommendedSysctlSettings = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          enables various settings to avoid common pitfalls when
+          running containers requiring many file operations.
+          Fixes errors like "Too many open files" or
+          "neighbour: ndisc_cache: neighbor table overflow!".
+          See https://lxd.readthedocs.io/en/latest/production-setup/
+          for details.
+        '';
+      };
     };
   };
 
@@ -69,8 +81,11 @@ in
         ExecStart = "@${pkgs.lxd.bin}/bin/lxd lxd --group lxd";
         Type = "simple";
         KillMode = "process"; # when stopping, leave the containers alone
+        LimitMEMLOCK = "infinity";
+        LimitNOFILE = "1048576";
+        LimitNPROC = "infinity";
+        TasksMax = "infinity";
       };
-
     };
 
     users.groups.lxd.gid = config.ids.gids.lxd;
@@ -79,5 +94,16 @@ in
       subUidRanges = [ { startUid = 1000000; count = 65536; } ];
       subGidRanges = [ { startGid = 1000000; count = 65536; } ];
     };
+
+    boot.kernel.sysctl = mkIf cfg.recommendedSysctlSettings {
+      "fs.inotify.max_queued_events" = 1048576;
+      "fs.inotify.max_user_instances" = 1048576;
+      "fs.inotify.max_user_watches" = 1048576;
+      "vm.max_map_count" = 262144;
+      "kernel.dmesg_restrict" = 1;
+      "net.ipv4.neigh.default.gc_thresh3" = 8192;
+      "net.ipv6.neigh.default.gc_thresh3" = 8192;
+      "kernel.keys.maxkeys" = 2000;
+    };
   };
 }
diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix
index 678ce3c2880..ca9c6f9a7f9 100644
--- a/nixos/release-combined.nix
+++ b/nixos/release-combined.nix
@@ -120,8 +120,8 @@ in rec {
         (all nixos.tests.networking.scripted.macvlan)
         (all nixos.tests.networking.scripted.sit)
         (all nixos.tests.networking.scripted.vlan)
-        (all nixos.tests.nfs3)
-        (all nixos.tests.nfs4)
+        (all nixos.tests.nfs3.simple)
+        (all nixos.tests.nfs4.simple)
         (all nixos.tests.openssh)
         (all nixos.tests.php-pcre)
         (all nixos.tests.predictable-interface-names.predictable)
diff --git a/nixos/tests/3proxy.nix b/nixos/tests/3proxy.nix
new file mode 100644
index 00000000000..b8e1dac0e89
--- /dev/null
+++ b/nixos/tests/3proxy.nix
@@ -0,0 +1,162 @@
+import ./make-test.nix ({ pkgs, ...} : {
+  name = "3proxy";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ misuzu ];
+  };
+
+  nodes = {
+    peer0 = { lib, ... }: {
+      networking.useDHCP = false;
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [
+          {
+            address = "192.168.0.1";
+            prefixLength = 24;
+          }
+          {
+            address = "216.58.211.111";
+            prefixLength = 24;
+          }
+        ];
+      };
+    };
+
+    peer1 = { lib, ... }: {
+      networking.useDHCP = false;
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [
+          {
+            address = "192.168.0.2";
+            prefixLength = 24;
+          }
+          {
+            address = "216.58.211.112";
+            prefixLength = 24;
+          }
+        ];
+      };
+      # test that binding to [::] is working when ipv6 is disabled
+      networking.enableIPv6 = false;
+      services._3proxy = {
+        enable = true;
+        services = [
+          {
+            type = "admin";
+            bindPort = 9999;
+            auth = [ "none" ];
+          }
+          {
+            type = "proxy";
+            bindPort = 3128;
+            auth = [ "none" ];
+          }
+        ];
+      };
+      networking.firewall.allowedTCPPorts = [ 3128 9999 ];
+    };
+
+    peer2 = { lib, ... }: {
+      networking.useDHCP = false;
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [
+          {
+            address = "192.168.0.3";
+            prefixLength = 24;
+          }
+          {
+            address = "216.58.211.113";
+            prefixLength = 24;
+          }
+        ];
+      };
+      services._3proxy = {
+        enable = true;
+        services = [
+          {
+            type = "admin";
+            bindPort = 9999;
+            auth = [ "none" ];
+          }
+          {
+            type = "proxy";
+            bindPort = 3128;
+            auth = [ "iponly" ];
+            acl = [
+              {
+                rule = "allow";
+              }
+            ];
+          }
+        ];
+      };
+      networking.firewall.allowedTCPPorts = [ 3128 9999 ];
+    };
+
+    peer3 = { lib, ... }: {
+      networking.useDHCP = false;
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [
+          {
+            address = "192.168.0.4";
+            prefixLength = 24;
+          }
+          {
+            address = "216.58.211.114";
+            prefixLength = 24;
+          }
+        ];
+      };
+      services._3proxy = {
+        enable = true;
+        usersFile = pkgs.writeText "3proxy.passwd" ''
+          admin:CR:$1$.GUV4Wvk$WnEVQtaqutD9.beO5ar1W/
+        '';
+        services = [
+          {
+            type = "admin";
+            bindPort = 9999;
+            auth = [ "none" ];
+          }
+          {
+            type = "proxy";
+            bindPort = 3128;
+            auth = [ "strong" ];
+            acl = [
+              {
+                rule = "allow";
+              }
+            ];
+          }
+        ];
+      };
+      networking.firewall.allowedTCPPorts = [ 3128 9999 ];
+    };
+  };
+
+  testScript = ''
+    startAll;
+
+    $peer1->waitForUnit("3proxy.service");
+
+    # test none auth
+    $peer0->succeed("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.2:3128 -S -O /dev/null http://216.58.211.112:9999");
+    $peer0->succeed("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.2:3128 -S -O /dev/null http://192.168.0.2:9999");
+    $peer0->succeed("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.2:3128 -S -O /dev/null http://127.0.0.1:9999");
+
+    $peer2->waitForUnit("3proxy.service");
+
+    # test iponly auth
+    $peer0->succeed("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.3:3128 -S -O /dev/null http://216.58.211.113:9999");
+    $peer0->fail("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.3:3128 -S -O /dev/null http://192.168.0.3:9999");
+    $peer0->fail("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.3:3128 -S -O /dev/null http://127.0.0.1:9999");
+
+    $peer3->waitForUnit("3proxy.service");
+
+    # test strong auth
+    $peer0->succeed("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://admin:bigsecret\@192.168.0.4:3128 -S -O /dev/null http://216.58.211.114:9999");
+    $peer0->fail("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://admin:bigsecret\@192.168.0.4:3128 -S -O /dev/null http://192.168.0.4:9999");
+    $peer0->fail("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.4:3128 -S -O /dev/null http://216.58.211.114:9999");
+    $peer0->fail("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.4:3128 -S -O /dev/null http://192.168.0.4:9999");
+    $peer0->fail("${pkgs.wget}/bin/wget -e use_proxy=yes -e http_proxy=http://192.168.0.4:3128 -S -O /dev/null http://127.0.0.1:9999");
+  '';
+})
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 184a156a29e..6413895c4c5 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -21,6 +21,7 @@ let
     else {};
 in
 {
+  _3proxy = handleTest ./3proxy.nix {};
   acme = handleTestOn ["x86_64-linux"] ./acme.nix {};
   atd = handleTest ./atd.nix {};
   automysqlbackup = handleTest ./automysqlbackup.nix {};
@@ -190,8 +191,9 @@ in
   networkingProxy = handleTest ./networking-proxy.nix {};
   nextcloud = handleTest ./nextcloud {};
   nexus = handleTest ./nexus.nix {};
-  nfs3 = handleTest ./nfs.nix { version = 3; };
-  nfs4 = handleTest ./nfs.nix { version = 4; };
+  # TODO: Test nfsv3 + Kerberos
+  nfs3 = handleTest ./nfs { version = 3; };
+  nfs4 = handleTest ./nfs { version = 4; };
   nghttpx = handleTest ./nghttpx.nix {};
   nginx = handleTest ./nginx.nix {};
   nginx-sso = handleTest ./nginx-sso.nix {};
@@ -262,6 +264,7 @@ in
   syncthing-init = handleTest ./syncthing-init.nix {};
   syncthing-relay = handleTest ./syncthing-relay.nix {};
   systemd = handleTest ./systemd.nix {};
+  systemd-analyze = handleTest ./systemd-analyze.nix {};
   systemd-confinement = handleTest ./systemd-confinement.nix {};
   systemd-timesyncd = handleTest ./systemd-timesyncd.nix {};
   systemd-networkd-wireguard = handleTest ./systemd-networkd-wireguard.nix {};
@@ -293,5 +296,6 @@ in
   xss-lock = handleTest ./xss-lock.nix {};
   yabar = handleTest ./yabar.nix {};
   yggdrasil = handleTest ./yggdrasil.nix {};
+  zsh-history = handleTest ./zsh-history.nix {};
   zookeeper = handleTest ./zookeeper.nix {};
 }
diff --git a/nixos/tests/common/x11.nix b/nixos/tests/common/x11.nix
index c5a7c165d12..5ad0ac20fac 100644
--- a/nixos/tests/common/x11.nix
+++ b/nixos/tests/common/x11.nix
@@ -1,12 +1,12 @@
+{ lib, ... }:
+
 { services.xserver.enable = true;
 
   # Automatically log in.
   services.xserver.displayManager.auto.enable = true;
 
   # Use IceWM as the window manager.
-  services.xserver.windowManager.default = "icewm";
-  services.xserver.windowManager.icewm.enable = true;
-
   # Don't use a desktop manager.
-  services.xserver.desktopManager.default = "none";
+  services.xserver.displayManager.defaultSession = lib.mkDefault "none+icewm";
+  services.xserver.windowManager.icewm.enable = true;
 }
diff --git a/nixos/tests/gitea.nix b/nixos/tests/gitea.nix
index ffbc07cfbb2..aaed2486421 100644
--- a/nixos/tests/gitea.nix
+++ b/nixos/tests/gitea.nix
@@ -6,64 +6,104 @@
 with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
-{
-  mysql = makeTest {
-    name = "gitea-mysql";
-    meta.maintainers = with maintainers; [ aanderse kolaente ];
-
-    machine =
-      { config, pkgs, ... }:
-      { services.gitea.enable = true;
-        services.gitea.database.type = "mysql";
+let
+  supportedDbTypes = [ "mysql" "postgres" "sqlite3" ];
+  makeGiteaTest = type: nameValuePair type (makeTest {
+    name = "gitea-${type}";
+    meta.maintainers = with maintainers; [ aanderse kolaente ma27 ];
+
+    nodes = {
+      server = { config, pkgs, ... }: {
+        services.gitea = {
+          enable = true;
+          database = { inherit type; };
+          disableRegistration = true;
+        };
+        environment.systemPackages = [ pkgs.gitea pkgs.jq ];
+        services.openssh.enable = true;
+      };
+      client1 = { config, pkgs, ... }: {
+        environment.systemPackages = [ pkgs.git ];
+      };
+      client2 = { config, pkgs, ... }: {
+        environment.systemPackages = [ pkgs.git ];
       };
+    };
+
+    testScript = let
+      inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
+    in ''
+      GIT_SSH_COMMAND = "ssh -i $HOME/.ssh/privk -o StrictHostKeyChecking=no"
+      REPO = "gitea@server:test/repo"
+      PRIVK = "${snakeOilPrivateKey}"
 
-    testScript = ''
       start_all()
 
-      machine.wait_for_unit("gitea.service")
-      machine.wait_for_open_port(3000)
-      machine.succeed("curl --fail http://localhost:3000/")
-    '';
-  };
+      client1.succeed("mkdir /tmp/repo")
+      client1.succeed("mkdir -p $HOME/.ssh")
+      client1.succeed(f"cat {PRIVK} > $HOME/.ssh/privk")
+      client1.succeed("chmod 0400 $HOME/.ssh/privk")
+      client1.succeed("git -C /tmp/repo init")
+      client1.succeed("echo hello world > /tmp/repo/testfile")
+      client1.succeed("git -C /tmp/repo add .")
+      client1.succeed("git config --global user.email test@localhost")
+      client1.succeed("git config --global user.name test")
+      client1.succeed("git -C /tmp/repo commit -m 'Initial import'")
+      client1.succeed(f"git -C /tmp/repo remote add origin {REPO}")
 
-  postgres = makeTest {
-    name = "gitea-postgres";
-    meta.maintainers = [ maintainers.aanderse ];
+      server.wait_for_unit("gitea.service")
+      server.wait_for_open_port(3000)
+      server.succeed("curl --fail http://localhost:3000/")
 
-    machine =
-      { config, pkgs, ... }:
-      { services.gitea.enable = true;
-        services.gitea.database.type = "postgres";
-      };
+      server.succeed(
+          "curl --fail http://localhost:3000/user/sign_up | grep 'Registration is disabled. "
+          + "Please contact your site administrator.'"
+      )
+      server.succeed(
+          "su -l gitea -c 'GITEA_WORK_DIR=/var/lib/gitea gitea admin create-user "
+          + "--username test --password totallysafe --email test@localhost'"
+      )
 
-    testScript = ''
-      start_all()
+      api_token = server.succeed(
+          "curl --fail -X POST http://test:totallysafe@localhost:3000/api/v1/users/test/tokens "
+          + "-H 'Accept: application/json' -H 'Content-Type: application/json' -d "
+          + "'{\"name\":\"token\"}' | jq '.sha1' | xargs echo -n"
+      )
 
-      machine.wait_for_unit("gitea.service")
-      machine.wait_for_open_port(3000)
-      machine.succeed("curl --fail http://localhost:3000/")
-    '';
-  };
+      server.succeed(
+          "curl --fail -X POST http://localhost:3000/api/v1/user/repos "
+          + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
+          + f"-H 'Authorization: token {api_token}'"
+          + ' -d \'{"auto_init":false, "description":"string", "license":"mit", "name":"repo", "private":false}\'''
+      )
 
-  sqlite = makeTest {
-    name = "gitea-sqlite";
-    meta.maintainers = [ maintainers.aanderse ];
+      server.succeed(
+          "curl --fail -X POST http://localhost:3000/api/v1/user/keys "
+          + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
+          + f"-H 'Authorization: token {api_token}'"
+          + ' -d \'{"key":"${snakeOilPublicKey}","read_only":true,"title":"SSH"}\'''
+      )
 
-    machine =
-      { config, pkgs, ... }:
-      { services.gitea.enable = true;
-        services.gitea.disableRegistration = true;
-      };
+      client1.succeed(
+          f"GIT_SSH_COMMAND='{GIT_SSH_COMMAND}' git -C /tmp/repo push origin master"
+      )
 
-    testScript = ''
-      start_all()
+      client2.succeed("mkdir -p $HOME/.ssh")
+      client2.succeed(f"cat {PRIVK} > $HOME/.ssh/privk")
+      client2.succeed("chmod 0400 $HOME/.ssh/privk")
+      client2.succeed(f"GIT_SSH_COMMAND='{GIT_SSH_COMMAND}' git clone {REPO}")
+      client2.succeed('test "$(cat repo/testfile | xargs echo -n)" = "hello world"')
 
-      machine.wait_for_unit("gitea.service")
-      machine.wait_for_open_port(3000)
-      machine.succeed("curl --fail http://localhost:3000/")
-      machine.succeed(
-          "curl --fail http://localhost:3000/user/sign_up | grep 'Registration is disabled. Please contact your site administrator.'"
+      server.succeed(
+          'test "$(curl http://localhost:3000/api/v1/repos/test/repo/commits '
+          + '-H "Accept: application/json" | jq length)" = "1"'
       )
+
+      client1.shutdown()
+      client2.shutdown()
+      server.shutdown()
     '';
-  };
-}
+  });
+in
+
+listToAttrs (map makeGiteaTest supportedDbTypes)
diff --git a/nixos/tests/gnome3-xorg.nix b/nixos/tests/gnome3-xorg.nix
index eb4c376319b..aa03501f6a5 100644
--- a/nixos/tests/gnome3-xorg.nix
+++ b/nixos/tests/gnome3-xorg.nix
@@ -16,7 +16,7 @@ import ./make-test.nix ({ pkgs, ...} : {
       services.xserver.displayManager.lightdm.autoLogin.enable = true;
       services.xserver.displayManager.lightdm.autoLogin.user = "alice";
       services.xserver.desktopManager.gnome3.enable = true;
-      services.xserver.desktopManager.default = "gnome-xorg";
+      services.xserver.displayManager.defaultSession = "gnome-xorg";
 
       virtualisation.memorySize = 1024;
     };
diff --git a/nixos/tests/hadoop/hdfs.nix b/nixos/tests/hadoop/hdfs.nix
index e7d72a56e1e..85aaab34b15 100644
--- a/nixos/tests/hadoop/hdfs.nix
+++ b/nixos/tests/hadoop/hdfs.nix
@@ -1,4 +1,4 @@
-import ../make-test.nix ({...}: {
+import ../make-test-python.nix ({...}: {
   nodes = {
     namenode = {pkgs, ...}: {
       services.hadoop = {
@@ -35,20 +35,20 @@ import ../make-test.nix ({...}: {
   };
 
   testScript = ''
-    startAll
+    start_all()
 
-    $namenode->waitForUnit("hdfs-namenode");
-    $namenode->waitForUnit("network.target");
-    $namenode->waitForOpenPort(8020);
-    $namenode->waitForOpenPort(9870);
+    namenode.wait_for_unit("hdfs-namenode")
+    namenode.wait_for_unit("network.target")
+    namenode.wait_for_open_port(8020)
+    namenode.wait_for_open_port(9870)
 
-    $datanode->waitForUnit("hdfs-datanode");
-    $datanode->waitForUnit("network.target");
-    $datanode->waitForOpenPort(9864);
-    $datanode->waitForOpenPort(9866);
-    $datanode->waitForOpenPort(9867);
+    datanode.wait_for_unit("hdfs-datanode")
+    datanode.wait_for_unit("network.target")
+    datanode.wait_for_open_port(9864)
+    datanode.wait_for_open_port(9866)
+    datanode.wait_for_open_port(9867)
 
-    $namenode->succeed("curl http://namenode:9870");
-    $datanode->succeed("curl http://datanode:9864");
+    namenode.succeed("curl http://namenode:9870")
+    datanode.succeed("curl http://datanode:9864")
   '';
 })
diff --git a/nixos/tests/hadoop/yarn.nix b/nixos/tests/hadoop/yarn.nix
index 031592301f1..2264ecaff15 100644
--- a/nixos/tests/hadoop/yarn.nix
+++ b/nixos/tests/hadoop/yarn.nix
@@ -1,4 +1,4 @@
-import ../make-test.nix ({...}: {
+import ../make-test-python.nix ({...}: {
   nodes = {
     resourcemanager = {pkgs, ...}: {
       services.hadoop.package = pkgs.hadoop_3_1;
@@ -28,19 +28,19 @@ import ../make-test.nix ({...}: {
   };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $resourcemanager->waitForUnit("yarn-resourcemanager");
-    $resourcemanager->waitForUnit("network.target");
-    $resourcemanager->waitForOpenPort(8031);
-    $resourcemanager->waitForOpenPort(8088);
+    resourcemanager.wait_for_unit("yarn-resourcemanager")
+    resourcemanager.wait_for_unit("network.target")
+    resourcemanager.wait_for_open_port(8031)
+    resourcemanager.wait_for_open_port(8088)
 
-    $nodemanager->waitForUnit("yarn-nodemanager");
-    $nodemanager->waitForUnit("network.target");
-    $nodemanager->waitForOpenPort(8042);
-    $nodemanager->waitForOpenPort(8041);
+    nodemanager.wait_for_unit("yarn-nodemanager")
+    nodemanager.wait_for_unit("network.target")
+    nodemanager.wait_for_open_port(8042)
+    nodemanager.wait_for_open_port(8041)
 
-    $resourcemanager->succeed("curl http://localhost:8088");
-    $nodemanager->succeed("curl http://localhost:8042");
+    resourcemanager.succeed("curl http://localhost:8088")
+    nodemanager.succeed("curl http://localhost:8042")
   '';
 })
diff --git a/nixos/tests/haproxy.nix b/nixos/tests/haproxy.nix
index 72e77a68193..b6fed3e2108 100644
--- a/nixos/tests/haproxy.nix
+++ b/nixos/tests/haproxy.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...}: {
+import ./make-test-python.nix ({ pkgs, ...}: {
   name = "haproxy";
   nodes = {
     machine = { ... }: {
@@ -33,11 +33,13 @@ import ./make-test.nix ({ pkgs, ...}: {
     };
   };
   testScript = ''
-    startAll;
-    $machine->waitForUnit('multi-user.target');
-    $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');
+    start_all()
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_for_unit("haproxy.service")
+    machine.wait_for_unit("httpd.service")
+    assert "We are all good!" in machine.succeed("curl -k http://localhost:80/index.txt")
+    assert "haproxy_process_pool_allocated_bytes" in machine.succeed(
+        "curl -k http://localhost:80/metrics"
+    )
   '';
 })
diff --git a/nixos/tests/hitch/default.nix b/nixos/tests/hitch/default.nix
index cb24c4dcffc..10612025641 100644
--- a/nixos/tests/hitch/default.nix
+++ b/nixos/tests/hitch/default.nix
@@ -1,4 +1,4 @@
-import ../make-test.nix ({ pkgs, ... }:
+import ../make-test-python.nix ({ pkgs, ... }:
 {
   name = "hitch";
   meta = with pkgs.stdenv.lib.maintainers; {
@@ -23,11 +23,11 @@ import ../make-test.nix ({ pkgs, ... }:
 
   testScript =
     ''
-      startAll;
+      start_all()
 
-      $machine->waitForUnit('multi-user.target');
-      $machine->waitForUnit('hitch.service');
-      $machine->waitForOpenPort(443);
-      $machine->succeed('curl -k https://localhost:443/index.txt | grep "We are all good!"');
+      machine.wait_for_unit("multi-user.target")
+      machine.wait_for_unit("hitch.service")
+      machine.wait_for_open_port(443)
+      assert "We are all good!" in machine.succeed("curl -k https://localhost:443/index.txt")
     '';
 })
diff --git a/nixos/tests/i3wm.nix b/nixos/tests/i3wm.nix
index 8afa845f1e2..126178d1187 100644
--- a/nixos/tests/i3wm.nix
+++ b/nixos/tests/i3wm.nix
@@ -7,7 +7,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
   machine = { lib, ... }: {
     imports = [ ./common/x11.nix ./common/user-account.nix ];
     services.xserver.displayManager.auto.user = "alice";
-    services.xserver.windowManager.default = lib.mkForce "i3";
+    services.xserver.displayManager.defaultSession = lib.mkForce "none+i3";
     services.xserver.windowManager.i3.enable = true;
   };
 
diff --git a/nixos/tests/initrd-network.nix b/nixos/tests/initrd-network.nix
index ed9b82e2da7..4796ff9b7c8 100644
--- a/nixos/tests/initrd-network.nix
+++ b/nixos/tests/initrd-network.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "initrd-network";
 
   meta.maintainers = [ pkgs.stdenv.lib.maintainers.eelco ];
@@ -15,8 +15,8 @@ import ./make-test.nix ({ pkgs, ...} : {
 
   testScript =
     ''
-      startAll;
-      $machine->waitForUnit("multi-user.target");
-      $machine->succeed("ip link >&2");
+      start_all()
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed("ip link >&2")
     '';
 })
diff --git a/nixos/tests/leaps.nix b/nixos/tests/leaps.nix
index 6163fed56b6..65b475d734e 100644
--- a/nixos/tests/leaps.nix
+++ b/nixos/tests/leaps.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs,  ... }:
+import ./make-test-python.nix ({ pkgs,  ... }:
 
 {
   name = "leaps";
@@ -22,9 +22,11 @@ import ./make-test.nix ({ pkgs,  ... }:
 
   testScript =
     ''
-      startAll;
-      $server->waitForOpenPort(6666);
-      $client->waitForUnit("network.target");
-      $client->succeed("${pkgs.curl}/bin/curl http://server:6666/leaps/ | grep -i 'leaps'");
+      start_all()
+      server.wait_for_open_port(6666)
+      client.wait_for_unit("network.target")
+      assert "leaps" in client.succeed(
+          "${pkgs.curl}/bin/curl http://server:6666/leaps/"
+      )
     '';
 })
diff --git a/nixos/tests/lidarr.nix b/nixos/tests/lidarr.nix
index 85fcbd21d8c..d3f83e5d914 100644
--- a/nixos/tests/lidarr.nix
+++ b/nixos/tests/lidarr.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ lib, ... }:
+import ./make-test-python.nix ({ lib, ... }:
 
 with lib;
 
@@ -11,8 +11,10 @@ with lib;
     { services.lidarr.enable = true; };
 
   testScript = ''
-    $machine->waitForUnit('lidarr.service');
-    $machine->waitForOpenPort('8686');
-    $machine->succeed("curl --fail http://localhost:8686/");
+    start_all()
+
+    machine.wait_for_unit("lidarr.service")
+    machine.wait_for_open_port("8686")
+    machine.succeed("curl --fail http://localhost:8686/")
   '';
 })
diff --git a/nixos/tests/lightdm.nix b/nixos/tests/lightdm.nix
index ef30f7741e2..46c2ed7ccc5 100644
--- a/nixos/tests/lightdm.nix
+++ b/nixos/tests/lightdm.nix
@@ -8,9 +8,8 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     imports = [ ./common/user-account.nix ];
     services.xserver.enable = true;
     services.xserver.displayManager.lightdm.enable = true;
-    services.xserver.windowManager.default = "icewm";
+    services.xserver.displayManager.defaultSession = "none+icewm";
     services.xserver.windowManager.icewm.enable = true;
-    services.xserver.desktopManager.default = "none";
   };
 
   enableOCR = true;
diff --git a/nixos/tests/mailcatcher.nix b/nixos/tests/mailcatcher.nix
index eb5b606ecc8..2ef38544fe0 100644
--- a/nixos/tests/mailcatcher.nix
+++ b/nixos/tests/mailcatcher.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ lib, ... }:
+import ./make-test-python.nix ({ lib, ... }:
 
 {
   name = "mailcatcher";
@@ -16,11 +16,15 @@ import ./make-test.nix ({ lib, ... }:
     };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $machine->waitForUnit('mailcatcher.service');
-    $machine->waitForOpenPort('1025');
-    $machine->succeed('echo "this is the body of the email" | mail -s "subject" root@example.org');
-    $machine->succeed('curl http://localhost:1080/messages/1.source') =~ /this is the body of the email/ or die;
+    machine.wait_for_unit("mailcatcher.service")
+    machine.wait_for_open_port("1025")
+    machine.succeed(
+        'echo "this is the body of the email" | mail -s "subject" root@example.org'
+    )
+    assert "this is the body of the email" in machine.succeed(
+        "curl http://localhost:1080/messages/1.source"
+    )
   '';
 })
diff --git a/nixos/tests/mutable-users.nix b/nixos/tests/mutable-users.nix
index e590703ab2f..49c7f78b82e 100644
--- a/nixos/tests/mutable-users.nix
+++ b/nixos/tests/mutable-users.nix
@@ -1,6 +1,6 @@
 # Mutable users tests.
 
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "mutable-users";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ gleber ];
@@ -19,21 +19,27 @@ import ./make-test.nix ({ pkgs, ...} : {
     immutableSystem = nodes.machine.config.system.build.toplevel;
     mutableSystem = nodes.mutable.config.system.build.toplevel;
   in ''
-    $machine->start();
-    $machine->waitForUnit("default.target");
+    machine.start()
+    machine.wait_for_unit("default.target")
 
     # Machine starts in immutable mode. Add a user and test if reactivating
     # configuration removes the user.
-    $machine->fail("cat /etc/passwd | grep ^foobar:");
-    $machine->succeed("sudo useradd foobar");
-    $machine->succeed("cat /etc/passwd | grep ^foobar:");
-    $machine->succeed("${immutableSystem}/bin/switch-to-configuration test");
-    $machine->fail("cat /etc/passwd | grep ^foobar:");
+    with subtest("Machine in immutable mode"):
+        assert "foobar" not in machine.succeed("cat /etc/passwd")
+        machine.succeed("sudo useradd foobar")
+        assert "foobar" in machine.succeed("cat /etc/passwd")
+        machine.succeed(
+            "${immutableSystem}/bin/switch-to-configuration test"
+        )
+        assert "foobar" not in machine.succeed("cat /etc/passwd")
 
     # In immutable mode passwd is not wrapped, while in mutable mode it is
     # wrapped.
-    $machine->succeed('which passwd | grep /run/current-system/');
-    $machine->succeed("${mutableSystem}/bin/switch-to-configuration test");
-    $machine->succeed('which passwd | grep /run/wrappers/');
+    with subtest("Password is wrapped in mutable mode"):
+        assert "/run/current-system/" in machine.succeed("which passwd")
+        machine.succeed(
+            "${mutableSystem}/bin/switch-to-configuration test"
+        )
+        assert "/run/wrappers/" in machine.succeed("which passwd")
   '';
 })
diff --git a/nixos/tests/mxisd.nix b/nixos/tests/mxisd.nix
index 0039256f586..b2b60db4d82 100644
--- a/nixos/tests/mxisd.nix
+++ b/nixos/tests/mxisd.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... } : {
+import ./make-test-python.nix ({ pkgs, ... } : {
 
   name = "mxisd";
   meta = with pkgs.stdenv.lib.maintainers; {
@@ -19,13 +19,12 @@ import ./make-test.nix ({ pkgs, ... } : {
   };
 
   testScript = ''
-    startAll;
-    $server_mxisd->waitForUnit("mxisd.service");
-    $server_mxisd->waitForOpenPort(8090);
-    $server_mxisd->succeed("curl -Ssf \"http://127.0.0.1:8090/_matrix/identity/api/v1\"");
-    $server_ma1sd->waitForUnit("mxisd.service");
-    $server_ma1sd->waitForOpenPort(8090);
-    $server_ma1sd->succeed("curl -Ssf \"http://127.0.0.1:8090/_matrix/identity/api/v1\"")
-
+    start_all()
+    server_mxisd.wait_for_unit("mxisd.service")
+    server_mxisd.wait_for_open_port(8090)
+    server_mxisd.succeed("curl -Ssf 'http://127.0.0.1:8090/_matrix/identity/api/v1'")
+    server_ma1sd.wait_for_unit("mxisd.service")
+    server_ma1sd.wait_for_open_port(8090)
+    server_ma1sd.succeed("curl -Ssf 'http://127.0.0.1:8090/_matrix/identity/api/v1'")
   '';
 })
diff --git a/nixos/tests/nesting.nix b/nixos/tests/nesting.nix
index 1306d6f8e0c..6388b67a6e4 100644
--- a/nixos/tests/nesting.nix
+++ b/nixos/tests/nesting.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix {
+import ./make-test-python.nix {
   name = "nesting";
   nodes =  {
     clone = { pkgs, ... }: {
@@ -19,24 +19,26 @@ import ./make-test.nix {
     };
   };
   testScript = ''
-    $clone->waitForUnit("default.target");
-    $clone->succeed("cowsay hey");
-    $clone->fail("hello");
+    clone.wait_for_unit("default.target")
+    clone.succeed("cowsay hey")
+    clone.fail("hello")
 
-    # Nested clones do inherit from parent
-    $clone->succeed("/run/current-system/fine-tune/child-1/bin/switch-to-configuration test");
-    $clone->succeed("cowsay hey");
-    $clone->succeed("hello");
+    with subtest("Nested clones do inherit from parent"):
+        clone.succeed(
+            "/run/current-system/fine-tune/child-1/bin/switch-to-configuration test"
+        )
+        clone.succeed("cowsay hey")
+        clone.succeed("hello")
     
+        children.wait_for_unit("default.target")
+        children.succeed("cowsay hey")
+        children.fail("hello")
 
-    $children->waitForUnit("default.target");
-    $children->succeed("cowsay hey");
-    $children->fail("hello");
-
-    # Nested children do not inherit from parent
-    $children->succeed("/run/current-system/fine-tune/child-1/bin/switch-to-configuration test");
-    $children->fail("cowsay hey");
-    $children->succeed("hello");
-
+    with subtest("Nested children do not inherit from parent"):
+        children.succeed(
+            "/run/current-system/fine-tune/child-1/bin/switch-to-configuration test"
+        )
+        children.fail("cowsay hey")
+        children.succeed("hello")
   '';
 }
diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix
index e0585d8f1bb..9448a104073 100644
--- a/nixos/tests/networking.nix
+++ b/nixos/tests/networking.nix
@@ -4,7 +4,7 @@
 # bool: whether to use networkd in the tests
 , networkd }:
 
-with import ../lib/testing.nix { inherit system pkgs; };
+with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
 let
@@ -75,10 +75,11 @@ let
       machine.networking.useDHCP = false;
       machine.networking.useNetworkd = networkd;
       testScript = ''
-        startAll;
-        $machine->waitForUnit("network.target");
-        $machine->succeed("ip addr show lo | grep -q 'inet 127.0.0.1/8 '");
-        $machine->succeed("ip addr show lo | grep -q 'inet6 ::1/128 '");
+        start_all()
+        machine.wait_for_unit("network.target")
+        loopback_addresses = machine.succeed("ip addr show lo")
+        assert "inet 127.0.0.1/8" in loopback_addresses
+        assert "inet6 ::1/128" in loopback_addresses
       '';
     };
     static = {
@@ -102,35 +103,35 @@ let
       };
       testScript = { ... }:
         ''
-          startAll;
+          start_all()
 
-          $client->waitForUnit("network.target");
-          $router->waitForUnit("network-online.target");
+          client.wait_for_unit("network.target")
+          router.wait_for_unit("network-online.target")
 
-          # Make sure dhcpcd is not started
-          $client->fail("systemctl status dhcpcd.service");
+          with subtest("Make sure dhcpcd is not started"):
+              client.fail("systemctl status dhcpcd.service")
 
-          # Test vlan 1
-          $client->waitUntilSucceeds("ping -c 1 192.168.1.1");
-          $client->waitUntilSucceeds("ping -c 1 192.168.1.2");
-          $client->waitUntilSucceeds("ping -c 1 192.168.1.3");
-          $client->waitUntilSucceeds("ping -c 1 192.168.1.10");
+          with subtest("Test vlan 1"):
+              client.wait_until_succeeds("ping -c 1 192.168.1.1")
+              client.wait_until_succeeds("ping -c 1 192.168.1.2")
+              client.wait_until_succeeds("ping -c 1 192.168.1.3")
+              client.wait_until_succeeds("ping -c 1 192.168.1.10")
 
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.1");
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.2");
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.3");
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.10");
+              router.wait_until_succeeds("ping -c 1 192.168.1.1")
+              router.wait_until_succeeds("ping -c 1 192.168.1.2")
+              router.wait_until_succeeds("ping -c 1 192.168.1.3")
+              router.wait_until_succeeds("ping -c 1 192.168.1.10")
 
-          # Test vlan 2
-          $client->waitUntilSucceeds("ping -c 1 192.168.2.1");
-          $client->waitUntilSucceeds("ping -c 1 192.168.2.2");
+          with subtest("Test vlan 2"):
+              client.wait_until_succeeds("ping -c 1 192.168.2.1")
+              client.wait_until_succeeds("ping -c 1 192.168.2.2")
 
-          $router->waitUntilSucceeds("ping -c 1 192.168.2.1");
-          $router->waitUntilSucceeds("ping -c 1 192.168.2.2");
+              router.wait_until_succeeds("ping -c 1 192.168.2.1")
+              router.wait_until_succeeds("ping -c 1 192.168.2.2")
 
-          # Test default gateway
-          $router->waitUntilSucceeds("ping -c 1 192.168.3.1");
-          $client->waitUntilSucceeds("ping -c 1 192.168.3.1");
+          with subtest("Test default gateway"):
+              router.wait_until_succeeds("ping -c 1 192.168.3.1")
+              client.wait_until_succeeds("ping -c 1 192.168.3.1")
         '';
     };
     dhcpSimple = {
@@ -155,38 +156,38 @@ let
       };
       testScript = { ... }:
         ''
-          startAll;
-
-          $client->waitForUnit("network.target");
-          $router->waitForUnit("network-online.target");
-
-          # Wait until we have an ip address on each interface
-          $client->waitUntilSucceeds("ip addr show dev eth1 | grep -q '192.168.1'");
-          $client->waitUntilSucceeds("ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'");
-          $client->waitUntilSucceeds("ip addr show dev eth2 | grep -q '192.168.2'");
-          $client->waitUntilSucceeds("ip addr show dev eth2 | grep -q 'fd00:1234:5678:2:'");
-
-          # Test vlan 1
-          $client->waitUntilSucceeds("ping -c 1 192.168.1.1");
-          $client->waitUntilSucceeds("ping -c 1 192.168.1.2");
-          $client->waitUntilSucceeds("ping -c 1 fd00:1234:5678:1::1");
-          $client->waitUntilSucceeds("ping -c 1 fd00:1234:5678:1::2");
-
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.1");
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.2");
-          $router->waitUntilSucceeds("ping -c 1 fd00:1234:5678:1::1");
-          $router->waitUntilSucceeds("ping -c 1 fd00:1234:5678:1::2");
-
-          # Test vlan 2
-          $client->waitUntilSucceeds("ping -c 1 192.168.2.1");
-          $client->waitUntilSucceeds("ping -c 1 192.168.2.2");
-          $client->waitUntilSucceeds("ping -c 1 fd00:1234:5678:2::1");
-          $client->waitUntilSucceeds("ping -c 1 fd00:1234:5678:2::2");
-
-          $router->waitUntilSucceeds("ping -c 1 192.168.2.1");
-          $router->waitUntilSucceeds("ping -c 1 192.168.2.2");
-          $router->waitUntilSucceeds("ping -c 1 fd00:1234:5678:2::1");
-          $router->waitUntilSucceeds("ping -c 1 fd00:1234:5678:2::2");
+          start_all()
+
+          client.wait_for_unit("network.target")
+          router.wait_for_unit("network-online.target")
+
+          with subtest("Wait until we have an ip address on each interface"):
+              client.wait_until_succeeds("ip addr show dev eth1 | grep -q '192.168.1'")
+              client.wait_until_succeeds("ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'")
+              client.wait_until_succeeds("ip addr show dev eth2 | grep -q '192.168.2'")
+              client.wait_until_succeeds("ip addr show dev eth2 | grep -q 'fd00:1234:5678:2:'")
+
+          with subtest("Test vlan 1"):
+              client.wait_until_succeeds("ping -c 1 192.168.1.1")
+              client.wait_until_succeeds("ping -c 1 192.168.1.2")
+              client.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1")
+              client.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::2")
+
+              router.wait_until_succeeds("ping -c 1 192.168.1.1")
+              router.wait_until_succeeds("ping -c 1 192.168.1.2")
+              router.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1")
+              router.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::2")
+
+          with subtest("Test vlan 2"):
+              client.wait_until_succeeds("ping -c 1 192.168.2.1")
+              client.wait_until_succeeds("ping -c 1 192.168.2.2")
+              client.wait_until_succeeds("ping -c 1 fd00:1234:5678:2::1")
+              client.wait_until_succeeds("ping -c 1 fd00:1234:5678:2::2")
+
+              router.wait_until_succeeds("ping -c 1 192.168.2.1")
+              router.wait_until_succeeds("ping -c 1 192.168.2.2")
+              router.wait_until_succeeds("ping -c 1 fd00:1234:5678:2::1")
+              router.wait_until_succeeds("ping -c 1 fd00:1234:5678:2::2")
         '';
     };
     dhcpOneIf = {
@@ -206,28 +207,28 @@ let
       };
       testScript = { ... }:
         ''
-          startAll;
+          start_all()
 
-          # Wait for networking to come up
-          $client->waitForUnit("network.target");
-          $router->waitForUnit("network.target");
+          with subtest("Wait for networking to come up"):
+              client.wait_for_unit("network.target")
+              router.wait_for_unit("network.target")
 
-          # Wait until we have an ip address on each interface
-          $client->waitUntilSucceeds("ip addr show dev eth1 | grep -q '192.168.1'");
+          with subtest("Wait until we have an ip address on each interface"):
+              client.wait_until_succeeds("ip addr show dev eth1 | grep -q '192.168.1'")
 
-          # Test vlan 1
-          $client->waitUntilSucceeds("ping -c 1 192.168.1.1");
-          $client->waitUntilSucceeds("ping -c 1 192.168.1.2");
+          with subtest("Test vlan 1"):
+              client.wait_until_succeeds("ping -c 1 192.168.1.1")
+              client.wait_until_succeeds("ping -c 1 192.168.1.2")
 
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.1");
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.2");
+              router.wait_until_succeeds("ping -c 1 192.168.1.1")
+              router.wait_until_succeeds("ping -c 1 192.168.1.2")
 
-          # Test vlan 2
-          $client->waitUntilSucceeds("ping -c 1 192.168.2.1");
-          $client->fail("ping -c 1 192.168.2.2");
+          with subtest("Test vlan 2"):
+              client.wait_until_succeeds("ping -c 1 192.168.2.1")
+              client.fail("ping -c 1 192.168.2.2")
 
-          $router->waitUntilSucceeds("ping -c 1 192.168.2.1");
-          $router->fail("ping -c 1 192.168.2.2");
+              router.wait_until_succeeds("ping -c 1 192.168.2.1")
+              router.fail("ping -c 1 192.168.2.2")
         '';
     };
     bond = let
@@ -252,18 +253,18 @@ let
       nodes.client2 = node "192.168.1.2";
       testScript = { ... }:
         ''
-          startAll;
+          start_all()
 
-          # Wait for networking to come up
-          $client1->waitForUnit("network.target");
-          $client2->waitForUnit("network.target");
+          with subtest("Wait for networking to come up"):
+              client1.wait_for_unit("network.target")
+              client2.wait_for_unit("network.target")
 
-          # Test bonding
-          $client1->waitUntilSucceeds("ping -c 2 192.168.1.1");
-          $client1->waitUntilSucceeds("ping -c 2 192.168.1.2");
+          with subtest("Test bonding"):
+              client1.wait_until_succeeds("ping -c 2 192.168.1.1")
+              client1.wait_until_succeeds("ping -c 2 192.168.1.2")
 
-          $client2->waitUntilSucceeds("ping -c 2 192.168.1.1");
-          $client2->waitUntilSucceeds("ping -c 2 192.168.1.2");
+              client2.wait_until_succeeds("ping -c 2 192.168.1.1")
+              client2.wait_until_succeeds("ping -c 2 192.168.1.2")
         '';
     };
     bridge = let
@@ -294,25 +295,24 @@ let
       };
       testScript = { ... }:
         ''
-          startAll;
+          start_all()
 
-          # Wait for networking to come up
-          $client1->waitForUnit("network.target");
-          $client2->waitForUnit("network.target");
-          $router->waitForUnit("network.target");
+          with subtest("Wait for networking to come up"):
+              for machine in client1, client2, router:
+                  machine.wait_for_unit("network.target")
 
-          # Test bridging
-          $client1->waitUntilSucceeds("ping -c 1 192.168.1.1");
-          $client1->waitUntilSucceeds("ping -c 1 192.168.1.2");
-          $client1->waitUntilSucceeds("ping -c 1 192.168.1.3");
+          with subtest("Test bridging"):
+              client1.wait_until_succeeds("ping -c 1 192.168.1.1")
+              client1.wait_until_succeeds("ping -c 1 192.168.1.2")
+              client1.wait_until_succeeds("ping -c 1 192.168.1.3")
 
-          $client2->waitUntilSucceeds("ping -c 1 192.168.1.1");
-          $client2->waitUntilSucceeds("ping -c 1 192.168.1.2");
-          $client2->waitUntilSucceeds("ping -c 1 192.168.1.3");
+              client2.wait_until_succeeds("ping -c 1 192.168.1.1")
+              client2.wait_until_succeeds("ping -c 1 192.168.1.2")
+              client2.wait_until_succeeds("ping -c 1 192.168.1.3")
 
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.1");
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.2");
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.3");
+              router.wait_until_succeeds("ping -c 1 192.168.1.1")
+              router.wait_until_succeeds("ping -c 1 192.168.1.2")
+              router.wait_until_succeeds("ping -c 1 192.168.1.3")
         '';
     };
     macvlan = {
@@ -340,35 +340,35 @@ let
       };
       testScript = { ... }:
         ''
-          startAll;
-
-          # Wait for networking to come up
-          $client->waitForUnit("network.target");
-          $router->waitForUnit("network.target");
-
-          # Wait until we have an ip address on each interface
-          $client->waitUntilSucceeds("ip addr show dev eth1 | grep -q '192.168.1'");
-          $client->waitUntilSucceeds("ip addr show dev macvlan | grep -q '192.168.1'");
-
-          # Print lots of diagnostic information
-          $router->log('**********************************************');
-          $router->succeed("ip addr >&2");
-          $router->succeed("ip route >&2");
-          $router->execute("iptables-save >&2");
-          $client->log('==============================================');
-          $client->succeed("ip addr >&2");
-          $client->succeed("ip route >&2");
-          $client->execute("iptables-save >&2");
-          $client->log('##############################################');
-
-          # Test macvlan creates routable ips
-          $client->waitUntilSucceeds("ping -c 1 192.168.1.1");
-          $client->waitUntilSucceeds("ping -c 1 192.168.1.2");
-          $client->waitUntilSucceeds("ping -c 1 192.168.1.3");
-
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.1");
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.2");
-          $router->waitUntilSucceeds("ping -c 1 192.168.1.3");
+          start_all()
+
+          with subtest("Wait for networking to come up"):
+              client.wait_for_unit("network.target")
+              router.wait_for_unit("network.target")
+
+          with subtest("Wait until we have an ip address on each interface"):
+              client.wait_until_succeeds("ip addr show dev eth1 | grep -q '192.168.1'")
+              client.wait_until_succeeds("ip addr show dev macvlan | grep -q '192.168.1'")
+
+          with subtest("Print lots of diagnostic information"):
+              router.log("**********************************************")
+              router.succeed("ip addr >&2")
+              router.succeed("ip route >&2")
+              router.execute("iptables-save >&2")
+              client.log("==============================================")
+              client.succeed("ip addr >&2")
+              client.succeed("ip route >&2")
+              client.execute("iptables-save >&2")
+              client.log("##############################################")
+
+          with subtest("Test macvlan creates routable ips"):
+              client.wait_until_succeeds("ping -c 1 192.168.1.1")
+              client.wait_until_succeeds("ping -c 1 192.168.1.2")
+              client.wait_until_succeeds("ping -c 1 192.168.1.3")
+
+              router.wait_until_succeeds("ping -c 1 192.168.1.1")
+              router.wait_until_succeeds("ping -c 1 192.168.1.2")
+              router.wait_until_succeeds("ping -c 1 192.168.1.3")
         '';
     };
     sit = let
@@ -395,22 +395,22 @@ let
       nodes.client2 = node { address4 = "192.168.1.2"; remote = "192.168.1.1"; address6 = "fc00::2"; };
       testScript = { ... }:
         ''
-          startAll;
+          start_all()
 
-          # Wait for networking to be configured
-          $client1->waitForUnit("network.target");
-          $client2->waitForUnit("network.target");
+          with subtest("Wait for networking to be configured"):
+              client1.wait_for_unit("network.target")
+              client2.wait_for_unit("network.target")
 
-          # Print diagnostic information
-          $client1->succeed("ip addr >&2");
-          $client2->succeed("ip addr >&2");
+              # Print diagnostic information
+              client1.succeed("ip addr >&2")
+              client2.succeed("ip addr >&2")
 
-          # Test ipv6
-          $client1->waitUntilSucceeds("ping -c 1 fc00::1");
-          $client1->waitUntilSucceeds("ping -c 1 fc00::2");
+          with subtest("Test ipv6"):
+              client1.wait_until_succeeds("ping -c 1 fc00::1")
+              client1.wait_until_succeeds("ping -c 1 fc00::2")
 
-          $client2->waitUntilSucceeds("ping -c 1 fc00::1");
-          $client2->waitUntilSucceeds("ping -c 1 fc00::2");
+              client2.wait_until_succeeds("ping -c 1 fc00::1")
+              client2.wait_until_succeeds("ping -c 1 fc00::2")
         '';
     };
     vlan = let
@@ -435,15 +435,15 @@ let
       nodes.client2 = node "192.168.1.2";
       testScript = { ... }:
         ''
-          startAll;
+          start_all()
 
-          # Wait for networking to be configured
-          $client1->waitForUnit("network.target");
-          $client2->waitForUnit("network.target");
+          with subtest("Wait for networking to be configured"):
+              client1.wait_for_unit("network.target")
+              client2.wait_for_unit("network.target")
 
-          # Test vlan is setup
-          $client1->succeed("ip addr show dev vlan >&2");
-          $client2->succeed("ip addr show dev vlan >&2");
+          with subtest("Test vlan is setup"):
+              client1.succeed("ip addr show dev vlan >&2")
+              client2.succeed("ip addr show dev vlan >&2")
         '';
     };
     virtual = {
@@ -464,33 +464,38 @@ let
       };
 
       testScript = ''
-        my $targetList = <<'END';
+        targetList = """
         tap0: tap persist user 0
         tun0: tun persist user 0
-        END
-
-        # Wait for networking to come up
-        $machine->start;
-        $machine->waitForUnit("network-online.target");
-
-        # Test interfaces set up
-        my $list = $machine->succeed("ip tuntap list | sort");
-        "$list" eq "$targetList" or die(
-          "The list of virtual interfaces does not match the expected one:\n",
-          "Result:\n", "$list\n",
-          "Expected:\n", "$targetList\n"
-        );
-
-        # Test interfaces clean up
-        $machine->succeed("systemctl stop network-addresses-tap0");
-        $machine->sleep(10);
-        $machine->succeed("systemctl stop network-addresses-tun0");
-        $machine->sleep(10);
-        my $residue = $machine->succeed("ip tuntap list");
-        $residue eq "" or die(
-          "Some virtual interface has not been properly cleaned:\n",
-          "$residue\n"
-        );
+        """.strip()
+
+        with subtest("Wait for networking to come up"):
+            machine.start()
+            machine.wait_for_unit("network-online.target")
+
+        with subtest("Test interfaces set up"):
+            list = machine.succeed("ip tuntap list | sort").strip()
+            assert (
+                list == targetList
+            ), """
+            The list of virtual interfaces does not match the expected one:
+            Result:
+              {}
+            Expected:
+              {}
+            """.format(
+                list, targetList
+            )
+
+        with subtest("Test interfaces clean up"):
+            machine.succeed("systemctl stop network-addresses-tap0")
+            machine.sleep(10)
+            machine.succeed("systemctl stop network-addresses-tun0")
+            machine.sleep(10)
+            residue = machine.succeed("ip tuntap list")
+            assert (
+                residue is ""
+            ), "Some virtual interface has not been properly cleaned:\n{}".format(residue)
       '';
     };
     privacy = {
@@ -522,7 +527,7 @@ let
           '';
         };
       };
-      nodes.clientWithPrivacy = { pkgs, ... }: with pkgs.lib; {
+      nodes.client_with_privacy = { pkgs, ... }: with pkgs.lib; {
         virtualisation.vlans = [ 1 ];
         networking = {
           useNetworkd = networkd;
@@ -550,25 +555,31 @@ let
       };
       testScript = { ... }:
         ''
-          startAll;
-
-          $client->waitForUnit("network.target");
-          $clientWithPrivacy->waitForUnit("network.target");
-          $router->waitForUnit("network-online.target");
-
-          # Wait until we have an ip address
-          $clientWithPrivacy->waitUntilSucceeds("ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'");
-          $client->waitUntilSucceeds("ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'");
-
-          # Test vlan 1
-          $clientWithPrivacy->waitUntilSucceeds("ping -c 1 fd00:1234:5678:1::1");
-          $client->waitUntilSucceeds("ping -c 1 fd00:1234:5678:1::1");
-
-          # Test address used is temporary
-          $clientWithPrivacy->waitUntilSucceeds("! ip route get fd00:1234:5678:1::1 | grep -q ':[a-f0-9]*ff:fe[a-f0-9]*:'");
-
-          # Test address used is EUI-64
-          $client->waitUntilSucceeds("ip route get fd00:1234:5678:1::1 | grep -q ':[a-f0-9]*ff:fe[a-f0-9]*:'");
+          start_all()
+
+          client.wait_for_unit("network.target")
+          client_with_privacy.wait_for_unit("network.target")
+          router.wait_for_unit("network-online.target")
+
+          with subtest("Wait until we have an ip address"):
+              client_with_privacy.wait_until_succeeds(
+                  "ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'"
+              )
+              client.wait_until_succeeds("ip addr show dev eth1 | grep -q 'fd00:1234:5678:1:'")
+
+          with subtest("Test vlan 1"):
+              client_with_privacy.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1")
+              client.wait_until_succeeds("ping -c 1 fd00:1234:5678:1::1")
+
+          with subtest("Test address used is temporary"):
+              client_with_privacy.wait_until_succeeds(
+                  "! ip route get fd00:1234:5678:1::1 | grep -q ':[a-f0-9]*ff:fe[a-f0-9]*:'"
+              )
+
+          with subtest("Test address used is EUI-64"):
+              client.wait_until_succeeds(
+                  "ip route get fd00:1234:5678:1::1 | grep -q ':[a-f0-9]*ff:fe[a-f0-9]*:'"
+              )
         '';
     };
     routes = {
@@ -591,47 +602,57 @@ let
       };
 
       testScript = ''
-        my $targetIPv4Table = <<'END';
+        targetIPv4Table = """
         10.0.0.0/16 proto static scope link mtu 1500 
         192.168.1.0/24 proto kernel scope link src 192.168.1.2 
         192.168.2.0/24 via 192.168.1.1 proto static 
-        END
+        """.strip()
 
-        my $targetIPv6Table = <<'END';
+        targetIPv6Table = """
         2001:1470:fffd:2097::/64 proto kernel metric 256 pref medium
         2001:1470:fffd:2098::/64 via fdfd:b3f0::1 proto static metric 1024 pref medium
         fdfd:b3f0::/48 proto static metric 1024 pref medium
-        END
-
-        $machine->start;
-        $machine->waitForUnit("network.target");
-
-        # test routing tables
-        my $ipv4Table = $machine->succeed("ip -4 route list dev eth0 | head -n3");
-        my $ipv6Table = $machine->succeed("ip -6 route list dev eth0 | head -n3");
-        "$ipv4Table" eq "$targetIPv4Table" or die(
-          "The IPv4 routing table does not match the expected one:\n",
-          "Result:\n", "$ipv4Table\n",
-          "Expected:\n", "$targetIPv4Table\n"
-        );
-        "$ipv6Table" eq "$targetIPv6Table" or die(
-          "The IPv6 routing table does not match the expected one:\n",
-          "Result:\n", "$ipv6Table\n",
-          "Expected:\n", "$targetIPv6Table\n"
-        );
-
-        # test clean-up of the tables
-        $machine->succeed("systemctl stop network-addresses-eth0");
-        my $ipv4Residue = $machine->succeed("ip -4 route list dev eth0 | head -n-3");
-        my $ipv6Residue = $machine->succeed("ip -6 route list dev eth0 | head -n-3");
-        $ipv4Residue eq "" or die(
-          "The IPv4 routing table has not been properly cleaned:\n",
-          "$ipv4Residue\n"
-        );
-        $ipv6Residue eq "" or die(
-          "The IPv6 routing table has not been properly cleaned:\n",
-          "$ipv6Residue\n"
-        );
+        """.strip()
+
+        machine.start()
+        machine.wait_for_unit("network.target")
+
+        with subtest("test routing tables"):
+            ipv4Table = machine.succeed("ip -4 route list dev eth0 | head -n3").strip()
+            ipv6Table = machine.succeed("ip -6 route list dev eth0 | head -n3").strip()
+            assert (
+                ipv4Table == targetIPv4Table
+            ), """
+              The IPv4 routing table does not match the expected one:
+                Result:
+                  {}
+                Expected:
+                  {}
+              """.format(
+                ipv4Table, targetIPv4Table
+            )
+            assert (
+                ipv6Table == targetIPv6Table
+            ), """
+              The IPv6 routing table does not match the expected one:
+                Result:
+                  {}
+                Expected:
+                  {}
+              """.format(
+                ipv6Table, targetIPv6Table
+            )
+
+        with subtest("test clean-up of the tables"):
+            machine.succeed("systemctl stop network-addresses-eth0")
+            ipv4Residue = machine.succeed("ip -4 route list dev eth0 | head -n-3").strip()
+            ipv6Residue = machine.succeed("ip -6 route list dev eth0 | head -n-3").strip()
+            assert (
+                ipv4Residue is ""
+            ), "The IPv4 routing table has not been properly cleaned:\n{}".format(ipv4Residue)
+            assert (
+                ipv6Residue is ""
+            ), "The IPv6 routing table has not been properly cleaned:\n{}".format(ipv6Residue)
       '';
     };
   };
diff --git a/nixos/tests/nfs.nix b/nixos/tests/nfs.nix
deleted file mode 100644
index 2f655336e75..00000000000
--- a/nixos/tests/nfs.nix
+++ /dev/null
@@ -1,90 +0,0 @@
-import ./make-test.nix ({ pkgs, version ? 4, ... }:
-
-let
-
-  client =
-    { pkgs, ... }:
-    { fileSystems = pkgs.lib.mkVMOverride
-        [ { mountPoint = "/data";
-            # nfs4 exports the export with fsid=0 as a virtual root directory
-            device = if (version == 4) then "server:/" else "server:/data";
-            fsType = "nfs";
-            options = [ "vers=${toString version}" ];
-          }
-        ];
-      networking.firewall.enable = false; # FIXME: only open statd
-    };
-
-in
-
-{
-  name = "nfs";
-  meta = with pkgs.stdenv.lib.maintainers; {
-    maintainers = [ eelco ];
-  };
-
-  nodes =
-    { client1 = client;
-      client2 = client;
-
-      server =
-        { ... }:
-        { services.nfs.server.enable = true;
-          services.nfs.server.exports =
-            ''
-              /data 192.168.1.0/255.255.255.0(rw,no_root_squash,no_subtree_check,fsid=0)
-            '';
-          services.nfs.server.createMountPoints = true;
-          networking.firewall.enable = false; # FIXME: figure out what ports need to be allowed
-        };
-    };
-
-  testScript =
-    ''
-      $server->waitForUnit("nfs-server");
-      $server->succeed("systemctl start network-online.target");
-      $server->waitForUnit("network-online.target");
-
-      startAll;
-
-      $client1->waitForUnit("data.mount");
-      $client1->succeed("echo bla > /data/foo");
-      $server->succeed("test -e /data/foo");
-
-      $client2->waitForUnit("data.mount");
-      $client2->succeed("echo bla > /data/bar");
-      $server->succeed("test -e /data/bar");
-
-      # Test whether restarting ‘nfs-server’ works correctly.
-      $server->succeed("systemctl restart nfs-server");
-      $client2->succeed("echo bla >> /data/bar"); # will take 90 seconds due to the NFS grace period
-
-      # Test whether we can get a lock.
-      $client2->succeed("time flock -n -s /data/lock true");
-
-      # Test locking: client 1 acquires an exclusive lock, so client 2
-      # should then fail to acquire a shared lock.
-      $client1->succeed("flock -x /data/lock -c 'touch locked; sleep 100000' &");
-      $client1->waitForFile("locked");
-      $client2->fail("flock -n -s /data/lock true");
-
-      # Test whether client 2 obtains the lock if we reset client 1.
-      $client2->succeed("flock -x /data/lock -c 'echo acquired; touch locked; sleep 100000' >&2 &");
-      $client1->crash;
-      $client1->start;
-      $client2->waitForFile("locked");
-
-      # Test whether locks survive a reboot of the server.
-      $client1->waitForUnit("data.mount");
-      $server->shutdown;
-      $server->start;
-      $client1->succeed("touch /data/xyzzy");
-      $client1->fail("time flock -n -s /data/lock true");
-
-      # Test whether unmounting during shutdown happens quickly.
-      my $t1 = time;
-      $client1->shutdown;
-      my $duration = time - $t1;
-      die "shutdown took too long ($duration seconds)" if $duration > 30;
-    '';
-})
diff --git a/nixos/tests/nfs/default.nix b/nixos/tests/nfs/default.nix
new file mode 100644
index 00000000000..6bc803c91b4
--- /dev/null
+++ b/nixos/tests/nfs/default.nix
@@ -0,0 +1,9 @@
+{ version ? 4
+, system ? builtins.currentSystem
+, pkgs ? import ../../.. { inherit system; }
+}: {
+  simple = import ./simple.nix { inherit version system pkgs; };
+} // pkgs.lib.optionalAttrs (version == 4) {
+  # TODO: Test kerberos + nfsv3
+  kerberos = import ./kerberos.nix { inherit version system pkgs; };
+}
diff --git a/nixos/tests/nfs/kerberos.nix b/nixos/tests/nfs/kerberos.nix
new file mode 100644
index 00000000000..1f2d0d453ea
--- /dev/null
+++ b/nixos/tests/nfs/kerberos.nix
@@ -0,0 +1,133 @@
+import ../make-test-python.nix ({ pkgs, lib, ... }:
+
+with lib;
+
+let
+  krb5 = 
+    { enable = true;
+      domain_realm."nfs.test"   = "NFS.TEST";
+      libdefaults.default_realm = "NFS.TEST";
+      realms."NFS.TEST" =
+        { admin_server = "server.nfs.test";
+          kdc = "server.nfs.test";
+        };
+    };
+
+  hosts =
+    ''
+      192.168.1.1 client.nfs.test
+      192.168.1.2 server.nfs.test
+    '';
+
+  users = {
+    users.alice = {
+        isNormalUser = true;
+        name = "alice";
+        uid = 1000;
+      };
+  };
+
+in
+
+{
+  name = "nfsv4-with-kerberos";
+ 
+  nodes = {
+    client = { lib, ... }:
+      { inherit krb5 users;
+
+        networking.extraHosts = hosts;
+        networking.domain = "nfs.test";
+        networking.hostName = "client";
+
+        fileSystems = lib.mkVMOverride
+          { "/data" = {
+              device  = "server.nfs.test:/";
+              fsType  = "nfs";
+              options = [ "nfsvers=4" "sec=krb5p" "noauto" ];
+            };
+          };
+      };
+
+    server = { lib, ...}:
+      { inherit krb5 users;
+
+        networking.extraHosts = hosts;
+        networking.domain = "nfs.test";
+        networking.hostName = "server";
+
+        networking.firewall.allowedTCPPorts = [
+          111  # rpc
+          2049 # nfs
+          88   # kerberos
+          749  # kerberos admin
+        ];
+
+        services.kerberos_server.enable = true;
+        services.kerberos_server.realms =
+          { "NFS.TEST".acl =
+            [ { access = "all"; principal = "admin/admin"; } ];
+          };
+
+        services.nfs.server.enable = true;
+        services.nfs.server.createMountPoints = true;
+        services.nfs.server.exports =
+          ''
+            /data *(rw,no_root_squash,fsid=0,sec=krb5p)
+          '';
+      };
+  };
+
+  testScript =
+    ''
+      server.succeed("mkdir -p /data/alice")
+      server.succeed("chown alice:users /data/alice")
+
+      # set up kerberos database
+      server.succeed(
+          "kdb5_util create -s -r NFS.TEST -P master_key",
+          "systemctl restart kadmind.service kdc.service",
+      )
+      server.wait_for_unit(f"kadmind.service")
+      server.wait_for_unit(f"kdc.service")
+
+      # create principals
+      server.succeed(
+          "kadmin.local add_principal -randkey nfs/server.nfs.test",
+          "kadmin.local add_principal -randkey nfs/client.nfs.test",
+          "kadmin.local add_principal -pw admin_pw admin/admin",
+          "kadmin.local add_principal -pw alice_pw alice",
+      )
+
+      # add principals to server keytab
+      server.succeed("kadmin.local ktadd nfs/server.nfs.test")
+      server.succeed("systemctl start rpc-gssd.service rpc-svcgssd.service")
+      server.wait_for_unit(f"rpc-gssd.service")
+      server.wait_for_unit(f"rpc-svcgssd.service")
+
+      client.wait_for_unit("network-online.target")
+
+      # add principals to client keytab
+      client.succeed("echo admin_pw | kadmin -p admin/admin ktadd nfs/client.nfs.test")
+      client.succeed("systemctl start rpc-gssd.service")
+      client.wait_for_unit("rpc-gssd.service")
+
+      with subtest("nfs share mounts"):
+          client.succeed("systemctl restart data.mount")
+          client.wait_for_unit("data.mount")
+
+      with subtest("permissions on nfs share are enforced"):
+          client.fail("su alice -c 'ls /data'")
+          client.succeed("su alice -c 'echo alice_pw | kinit'")
+          client.succeed("su alice -c 'ls /data'")
+
+          client.fail("su alice -c 'echo bla >> /data/foo'")
+          client.succeed("su alice -c 'echo bla >> /data/alice/foo'")
+          server.succeed("test -e /data/alice/foo")
+
+      with subtest("uids/gids are mapped correctly on nfs share"):
+          ids = client.succeed("stat -c '%U %G' /data/alice").split()
+          expected = ["alice", "users"]
+          assert ids == expected, f"ids incorrect: got {ids} expected {expected}"
+    '';
+})
diff --git a/nixos/tests/nfs/simple.nix b/nixos/tests/nfs/simple.nix
new file mode 100644
index 00000000000..a1a09ee0f45
--- /dev/null
+++ b/nixos/tests/nfs/simple.nix
@@ -0,0 +1,94 @@
+import ../make-test-python.nix ({ pkgs, version ? 4, ... }:
+
+let
+
+  client =
+    { pkgs, ... }:
+    { fileSystems = pkgs.lib.mkVMOverride
+        [ { mountPoint = "/data";
+            # nfs4 exports the export with fsid=0 as a virtual root directory
+            device = if (version == 4) then "server:/" else "server:/data";
+            fsType = "nfs";
+            options = [ "vers=${toString version}" ];
+          }
+        ];
+      networking.firewall.enable = false; # FIXME: only open statd
+    };
+
+in
+
+{
+  name = "nfs";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ eelco ];
+  };
+
+  nodes =
+    { client1 = client;
+      client2 = client;
+
+      server =
+        { ... }:
+        { services.nfs.server.enable = true;
+          services.nfs.server.exports =
+            ''
+              /data 192.168.1.0/255.255.255.0(rw,no_root_squash,no_subtree_check,fsid=0)
+            '';
+          services.nfs.server.createMountPoints = true;
+          networking.firewall.enable = false; # FIXME: figure out what ports need to be allowed
+        };
+    };
+
+  testScript =
+    ''
+      import time
+
+      server.wait_for_unit("nfs-server")
+      server.succeed("systemctl start network-online.target")
+      server.wait_for_unit("network-online.target")
+
+      start_all()
+
+      client1.wait_for_unit("data.mount")
+      client1.succeed("echo bla > /data/foo")
+      server.succeed("test -e /data/foo")
+
+      client2.wait_for_unit("data.mount")
+      client2.succeed("echo bla > /data/bar")
+      server.succeed("test -e /data/bar")
+
+      with subtest("restarting 'nfs-server' works correctly"):
+          server.succeed("systemctl restart nfs-server")
+          # will take 90 seconds due to the NFS grace period
+          client2.succeed("echo bla >> /data/bar")
+
+      with subtest("can get a lock"):
+          client2.succeed("time flock -n -s /data/lock true")
+
+      with subtest("client 2 fails to acquire lock held by client 1"):
+          client1.succeed("flock -x /data/lock -c 'touch locked; sleep 100000' &")
+          client1.wait_for_file("locked")
+          client2.fail("flock -n -s /data/lock true")
+
+      with subtest("client 2 obtains lock after resetting client 1"):
+          client2.succeed(
+              "flock -x /data/lock -c 'echo acquired; touch locked; sleep 100000' >&2 &"
+          )
+          client1.crash()
+          client1.start()
+          client2.wait_for_file("locked")
+
+      with subtest("locks survive server reboot"):
+          client1.wait_for_unit("data.mount")
+          server.shutdown()
+          server.start()
+          client1.succeed("touch /data/xyzzy")
+          client1.fail("time flock -n -s /data/lock true")
+
+      with subtest("unmounting during shutdown happens quickly"):
+          t1 = time.monotonic()
+          client1.shutdown()
+          duration = time.monotonic() - t1
+          assert duration < 30, f"shutdown took too long ({duration} seconds)"
+    '';
+})
diff --git a/nixos/tests/nghttpx.nix b/nixos/tests/nghttpx.nix
index 11611bfe106..d83c1c4cae6 100644
--- a/nixos/tests/nghttpx.nix
+++ b/nixos/tests/nghttpx.nix
@@ -1,7 +1,7 @@
 let
   nginxRoot = "/run/nginx";
 in
-  import ./make-test.nix ({...}: {
+  import ./make-test-python.nix ({...}: {
     name  = "nghttpx";
     nodes = {
       webserver = {
@@ -52,10 +52,10 @@ in
     };
 
     testScript = ''
-      startAll;
+      start_all()
 
-      $webserver->waitForOpenPort("80");
-      $proxy->waitForOpenPort("80");
-      $client->waitUntilSucceeds("curl -s --fail http://proxy/hello-world.txt");
+      webserver.wait_for_open_port("80")
+      proxy.wait_for_open_port("80")
+      client.wait_until_succeeds("curl -s --fail http://proxy/hello-world.txt")
     '';
   })
diff --git a/nixos/tests/novacomd.nix b/nixos/tests/novacomd.nix
index 4eb60c0feb5..940210dee23 100644
--- a/nixos/tests/novacomd.nix
+++ b/nixos/tests/novacomd.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "novacomd";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ dtzWill ];
@@ -9,26 +9,20 @@ import ./make-test.nix ({ pkgs, ...} : {
   };
 
   testScript = ''
-    $machine->waitForUnit("multi-user.target");
+    machine.wait_for_unit("novacomd.service")
 
-    # multi-user.target wants novacomd.service, but let's make sure
-    $machine->waitForUnit("novacomd.service");
+    with subtest("Make sure the daemon is really listening"):
+        machine.wait_for_open_port(6968)
+        machine.succeed("novacom -l")
 
-    # Check status and try connecting with novacom
-    $machine->succeed("systemctl status novacomd.service >&2");
-    # to prevent non-deterministic failure,
-    # make sure the daemon is really listening
-    $machine->waitForOpenPort(6968);
-    $machine->succeed("novacom -l");
+    with subtest("Stop the daemon, double-check novacom fails if daemon isn't working"):
+        machine.stop_job("novacomd")
+        machine.fail("novacom -l")
 
-    # Stop the daemon, double-check novacom fails if daemon isn't working
-    $machine->stopJob("novacomd");
-    $machine->fail("novacom -l");
-
-    # And back again for good measure
-    $machine->startJob("novacomd");
-    # make sure the daemon is really listening
-    $machine->waitForOpenPort(6968);
-    $machine->succeed("novacom -l");
+    with subtest("Make sure the daemon starts back up again"):
+        machine.start_job("novacomd")
+        # make sure the daemon is really listening
+        machine.wait_for_open_port(6968)
+        machine.succeed("novacom -l")
   '';
 })
diff --git a/nixos/tests/nzbget.nix b/nixos/tests/nzbget.nix
index 042ccec98cf..12d8ed6ea8d 100644
--- a/nixos/tests/nzbget.nix
+++ b/nixos/tests/nzbget.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "nzbget";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ aanderse flokli ];
@@ -15,12 +15,16 @@ import ./make-test.nix ({ pkgs, ...} : {
   };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $server->waitForUnit("nzbget.service");
-    $server->waitForUnit("network.target");
-    $server->waitForOpenPort(6789);
-    $server->succeed("curl -s -u nzbget:tegbzn6789 http://127.0.0.1:6789 | grep -q 'This file is part of nzbget'");
-    $server->succeed("${pkgs.nzbget}/bin/nzbget -n -o ControlIP=127.0.0.1 -o ControlPort=6789 -o ControlPassword=tegbzn6789 -V");
+    server.wait_for_unit("nzbget.service")
+    server.wait_for_unit("network.target")
+    server.wait_for_open_port(6789)
+    assert "This file is part of nzbget" in server.succeed(
+        "curl -s -u nzbget:tegbzn6789 http://127.0.0.1:6789"
+    )
+    server.succeed(
+        "${pkgs.nzbget}/bin/nzbget -n -o Control_iP=127.0.0.1 -o Control_port=6789 -o Control_password=tegbzn6789 -V"
+    )
   '';
 })
diff --git a/nixos/tests/orangefs.nix b/nixos/tests/orangefs.nix
index bdf4fc10c44..46d7a6a72f8 100644
--- a/nixos/tests/orangefs.nix
+++ b/nixos/tests/orangefs.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ ... } :
+import ./make-test-python.nix ({ ... } :
 
 let
   server = { pkgs, ... } : {
@@ -52,37 +52,31 @@ in {
 
   testScript = ''
     # format storage
-    foreach my $server  (($server1,$server2))
-    {
-      $server->start();
-      $server->waitForUnit("multi-user.target");
-      $server->succeed("mkdir -p /data/storage /data/meta");
-      $server->succeed("chown orangefs:orangefs /data/storage /data/meta");
-      $server->succeed("chmod 0770 /data/storage /data/meta");
-      $server->succeed("sudo -g orangefs -u orangefs pvfs2-server -f /etc/orangefs/server.conf");
-    }
+    for server in server1, server2:
+        server.start()
+        server.wait_for_unit("multi-user.target")
+        server.succeed("mkdir -p /data/storage /data/meta")
+        server.succeed("chown orangefs:orangefs /data/storage /data/meta")
+        server.succeed("chmod 0770 /data/storage /data/meta")
+        server.succeed(
+            "sudo -g orangefs -u orangefs pvfs2-server -f /etc/orangefs/server.conf"
+        )
 
     # start services after storage is formated on all machines
-    foreach my $server  (($server1,$server2))
-    {
-      $server->succeed("systemctl start orangefs-server.service");
-    }
+    for server in server1, server2:
+        server.succeed("systemctl start orangefs-server.service")
 
-    # Check if clients can reach and mount the FS
-    foreach my $client  (($client1,$client2))
-    {
-      $client->start();
-      $client->waitForUnit("orangefs-client.service");
-      # Both servers need to be reachable
-      $client->succeed("pvfs2-check-server -h server1 -f orangefs -n tcp -p 3334");
-      $client->succeed("pvfs2-check-server -h server2 -f orangefs -n tcp -p 3334");
-      $client->waitForUnit("orangefs.mount");
-
-    }
-
-    # R/W test between clients
-    $client1->succeed("echo test > /orangefs/file1");
-    $client2->succeed("grep test /orangefs/file1");
+    with subtest("clients can reach and mount the FS"):
+        for client in client1, client2:
+            client.start()
+            client.wait_for_unit("orangefs-client.service")
+            # Both servers need to be reachable
+            client.succeed("pvfs2-check-server -h server1 -f orangefs -n tcp -p 3334")
+            client.succeed("pvfs2-check-server -h server2 -f orangefs -n tcp -p 3334")
+            client.wait_for_unit("orangefs.mount")
 
+    with subtest("R/W test between clients"):
+        client1.succeed("echo test > /orangefs/file1")
+        client2.succeed("grep test /orangefs/file1")
   '';
 })
diff --git a/nixos/tests/osrm-backend.nix b/nixos/tests/osrm-backend.nix
index 6e2d098d4ad..db67a5a589f 100644
--- a/nixos/tests/osrm-backend.nix
+++ b/nixos/tests/osrm-backend.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, lib, ... }:
+import ./make-test-python.nix ({ pkgs, lib, ... }:
 let
   port = 5000;
 in {
@@ -45,9 +45,13 @@ in {
   testScript = let
     query = "http://localhost:${toString port}/route/v1/driving/7.41720,43.73304;7.42463,43.73886?steps=true";
   in ''
-    $machine->waitForUnit("osrm.service");
-    $machine->waitForOpenPort(${toString port});
-    $machine->succeed("curl --silent '${query}' | jq .waypoints[0].name | grep -F 'Boulevard Rainier III'");
-    $machine->succeed("curl --silent '${query}' | jq .waypoints[1].name | grep -F 'Avenue de la Costa'");
+    machine.wait_for_unit("osrm.service")
+    machine.wait_for_open_port(${toString port})
+    assert "Boulevard Rainier III" in machine.succeed(
+        "curl --silent '${query}' | jq .waypoints[0].name"
+    )
+    assert "Avenue de la Costa" in machine.succeed(
+        "curl --silent '${query}' | jq .waypoints[1].name"
+    )
   '';
 })
diff --git a/nixos/tests/overlayfs.nix b/nixos/tests/overlayfs.nix
index 99bb6b0f553..33794deb9ed 100644
--- a/nixos/tests/overlayfs.nix
+++ b/nixos/tests/overlayfs.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "overlayfs";
   meta.maintainers = with pkgs.stdenv.lib.maintainers; [ bachp ];
 
@@ -9,49 +9,42 @@ import ./make-test.nix ({ pkgs, ... }: {
   };
 
   testScript = ''
-    $machine->succeed("ls /dev");
+    machine.succeed("ls /dev")
 
-    $machine->succeed("mkdir -p /tmp/mnt");
+    machine.succeed("mkdir -p /tmp/mnt")
 
     # Test ext4 + overlayfs
-    $machine->succeed(
-
-      "mkfs.ext4 -F -L overlay-ext4 /dev/vdb",
-      "mount -t ext4 /dev/vdb /tmp/mnt",
-
-      "mkdir -p /tmp/mnt/upper /tmp/mnt/lower /tmp/mnt/work /tmp/mnt/merged",
-
-      # Setup some existing files
-      "echo 'Replace' > /tmp/mnt/lower/replace.txt",
-      "echo 'Append' > /tmp/mnt/lower/append.txt",
-      "echo 'Overwrite' > /tmp/mnt/lower/overwrite.txt",
-
-      "mount -t overlay overlay -o lowerdir=/tmp/mnt/lower,upperdir=/tmp/mnt/upper,workdir=/tmp/mnt/work /tmp/mnt/merged",
-
-      # Test new
-      "echo 'New' > /tmp/mnt/merged/new.txt",
-      "[[ \"\$(cat /tmp/mnt/merged/new.txt)\" == \"New\" ]]",
-
-      # Test replace
-      "[[ \"\$(cat /tmp/mnt/merged/replace.txt)\" == \"Replace\" ]]",
-      "echo 'Replaced' > /tmp/mnt/merged/replace-tmp.txt",
-      "mv /tmp/mnt/merged/replace-tmp.txt /tmp/mnt/merged/replace.txt",
-      "[[ \"\$(cat /tmp/mnt/merged/replace.txt)\" == \"Replaced\" ]]",
-
-      # Overwrite
-      "[[ \"\$(cat /tmp/mnt/merged/overwrite.txt)\" == \"Overwrite\" ]]",
-      "echo 'Overwritten' > /tmp/mnt/merged/overwrite.txt",
-      "[[ \"\$(cat /tmp/mnt/merged/overwrite.txt)\" == \"Overwritten\" ]]",
-
-      # Test append
-      "[[ \"\$(cat /tmp/mnt/merged/append.txt)\" == \"Append\" ]]",
-      "echo 'ed' >> /tmp/mnt/merged/append.txt",
-      #"cat /tmp/mnt/merged/append.txt && exit 1",
-      "[[ \"\$(cat /tmp/mnt/merged/append.txt)\" == \"Append\ned\" ]]",
-
-      "umount /tmp/mnt/merged",
-      "umount /tmp/mnt",
-      "udevadm settle"
-    );
+    machine.succeed(
+        """
+          mkfs.ext4 -F -L overlay-ext4 /dev/vdb
+          mount -t ext4 /dev/vdb /tmp/mnt
+          mkdir -p /tmp/mnt/upper /tmp/mnt/lower /tmp/mnt/work /tmp/mnt/merged
+          # Setup some existing files
+          echo 'Replace' > /tmp/mnt/lower/replace.txt
+          echo 'Append' > /tmp/mnt/lower/append.txt
+          echo 'Overwrite' > /tmp/mnt/lower/overwrite.txt
+          mount -t overlay overlay -o lowerdir=/tmp/mnt/lower,upperdir=/tmp/mnt/upper,workdir=/tmp/mnt/work /tmp/mnt/merged
+          # Test new
+          echo 'New' > /tmp/mnt/merged/new.txt
+          [[ "\$(cat /tmp/mnt/merged/new.txt)" == "New" ]]
+          # Test replace
+          [[ "\$(cat /tmp/mnt/merged/replace.txt)" == "Replace" ]]
+          echo 'Replaced' > /tmp/mnt/merged/replace-tmp.txt
+          mv /tmp/mnt/merged/replace-tmp.txt /tmp/mnt/merged/replace.txt
+          [[ "\$(cat /tmp/mnt/merged/replace.txt)" == "Replaced" ]]
+          # Overwrite
+          [[ "\$(cat /tmp/mnt/merged/overwrite.txt)" == "Overwrite" ]]
+          echo 'Overwritten' > /tmp/mnt/merged/overwrite.txt
+          [[ "\$(cat /tmp/mnt/merged/overwrite.txt)" == "Overwritten" ]]
+          # Test append
+          [[ "\$(cat /tmp/mnt/merged/append.txt)" == "Append" ]]
+          echo 'ed' >> /tmp/mnt/merged/append.txt
+          #"cat /tmp/mnt/merged/append.txt && exit 1
+          [[ "\$(cat /tmp/mnt/merged/append.txt)" == "Append\ned" ]]
+          umount /tmp/mnt/merged
+          umount /tmp/mnt
+          udevadm settle
+      """
+    )
   '';
 })
diff --git a/nixos/tests/paperless.nix b/nixos/tests/paperless.nix
index 860ad0a6218..355e7041d3f 100644
--- a/nixos/tests/paperless.nix
+++ b/nixos/tests/paperless.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ lib, ... } : {
+import ./make-test-python.nix ({ lib, ... } : {
   name = "paperless";
   meta = with lib.maintainers; {
     maintainers = [ earvstedt ];
@@ -13,17 +13,24 @@ import ./make-test.nix ({ lib, ... } : {
   };
 
   testScript = ''
-    $machine->waitForUnit("paperless-consumer.service");
+    machine.wait_for_unit("paperless-consumer.service")
+
     # Create test doc
-    $machine->succeed('convert -size 400x40 xc:white -font "DejaVu-Sans" -pointsize 20 -fill black \
-      -annotate +5+20 "hello world 16-10-2005" /var/lib/paperless/consume/doc.png');
+    machine.succeed(
+        "convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black -annotate +5+20 'hello world 16-10-2005' /var/lib/paperless/consume/doc.png"
+    )
+
+    with subtest("Service gets ready"):
+        machine.wait_for_unit("paperless-server.service")
+        # Wait until server accepts connections
+        machine.wait_until_succeeds("curl -s localhost:28981")
 
-    $machine->waitForUnit("paperless-server.service");
-    # Wait until server accepts connections
-    $machine->waitUntilSucceeds("curl -s localhost:28981");
-    # Wait until document is consumed
-    $machine->waitUntilSucceeds('(($(curl -s localhost:28981/api/documents/ | jq .count) == 1))');
-    $machine->succeed("curl -s localhost:28981/api/documents/ | jq '.results | .[0] | .created'")
-      =~ /2005-10-16/ or die;
+    with subtest("Test document is consumed"):
+        machine.wait_until_succeeds(
+            "(($(curl -s localhost:28981/api/documents/ | jq .count) == 1))"
+        )
+        assert "2005-10-16" in machine.succeed(
+            "curl -s localhost:28981/api/documents/ | jq '.results | .[0] | .created'"
+        )
   '';
 })
diff --git a/nixos/tests/pdns-recursor.nix b/nixos/tests/pdns-recursor.nix
index bf6e6093d69..de1b60e0b1c 100644
--- a/nixos/tests/pdns-recursor.nix
+++ b/nixos/tests/pdns-recursor.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "powerdns";
 
   nodes.server = { ... }: {
@@ -6,7 +6,7 @@ import ./make-test.nix ({ pkgs, ... }: {
   };
 
   testScript = ''
-    $server->waitForUnit("pdns-recursor");
-    $server->waitForOpenPort("53");
+    server.wait_for_unit("pdns-recursor")
+    server.wait_for_open_port("53")
   '';
 })
diff --git a/nixos/tests/peerflix.nix b/nixos/tests/peerflix.nix
index fae37fedaac..37628604d49 100644
--- a/nixos/tests/peerflix.nix
+++ b/nixos/tests/peerflix.nix
@@ -1,6 +1,6 @@
 # This test runs peerflix and checks if peerflix starts
 
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "peerflix";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ offline ];
@@ -15,9 +15,9 @@ import ./make-test.nix ({ pkgs, ...} : {
     };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $peerflix->waitForUnit("peerflix.service");
-    $peerflix->waitUntilSucceeds("curl localhost:9000");
+    peerflix.wait_for_unit("peerflix.service")
+    peerflix.wait_until_succeeds("curl localhost:9000")
   '';
 })
diff --git a/nixos/tests/pgmanage.nix b/nixos/tests/pgmanage.nix
index bacaf3f4158..4f5dbed24a9 100644
--- a/nixos/tests/pgmanage.nix
+++ b/nixos/tests/pgmanage.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ... } :
+import ./make-test-python.nix ({ pkgs, ... } :
 let
   role     = "test";
   password = "secret";
@@ -29,11 +29,13 @@ in
   };
 
   testScript = ''
-    startAll;
-    $one->waitForUnit("default.target");
-    $one->requireActiveUnit("pgmanage.service");
+    start_all()
+    one.wait_for_unit("default.target")
+    one.require_unit_state("pgmanage.service", "active")
 
     # Test if we can log in.
-    $one->waitUntilSucceeds("curl 'http://localhost:8080/pgmanage/auth' --data 'action=login&connname=${conn}&username=${role}&password=${password}' --fail");
+    one.wait_until_succeeds(
+        "curl 'http://localhost:8080/pgmanage/auth' --data 'action=login&connname=${conn}&username=${role}&password=${password}' --fail"
+    )
   '';
 })
diff --git a/nixos/tests/php-pcre.nix b/nixos/tests/php-pcre.nix
index ae44aec7944..d5c22e0582a 100644
--- a/nixos/tests/php-pcre.nix
+++ b/nixos/tests/php-pcre.nix
@@ -1,7 +1,7 @@
 
 let testString = "can-use-subgroups"; in
 
-import ./make-test.nix ({ ...}: {
+import ./make-test-python.nix ({ ...}: {
   name = "php-httpd-pcre-jit-test";
   machine = { lib, pkgs, ... }: {
     time.timeZone = "UTC";
@@ -31,9 +31,10 @@ import ./make-test.nix ({ ...}: {
   };
   testScript = { ... }:
   ''
-    $machine->waitForUnit('httpd.service');
+    machine.wait_for_unit("httpd.service")
     # Ensure php evaluation by matching on the var_dump syntax
-    $machine->succeed('curl -vvv -s http://127.0.0.1:80/index.php \
-      | grep "string(${toString (builtins.stringLength testString)}) \"${testString}\""');
+    assert 'string(${toString (builtins.stringLength testString)}) "${testString}"' in machine.succeed(
+        "curl -vvv -s http://127.0.0.1:80/index.php"
+    )
   '';
 })
diff --git a/nixos/tests/plasma5.nix b/nixos/tests/plasma5.nix
index 6884f17aabb..2eccfdf47f5 100644
--- a/nixos/tests/plasma5.nix
+++ b/nixos/tests/plasma5.nix
@@ -12,8 +12,8 @@ import ./make-test-python.nix ({ pkgs, ...} :
     imports = [ ./common/user-account.nix ];
     services.xserver.enable = true;
     services.xserver.displayManager.sddm.enable = true;
+    services.xserver.displayManager.defaultSession = "plasma5";
     services.xserver.desktopManager.plasma5.enable = true;
-    services.xserver.desktopManager.default = "plasma5";
     services.xserver.displayManager.sddm.autoLogin = {
       enable = true;
       user = "alice";
diff --git a/nixos/tests/postgis.nix b/nixos/tests/postgis.nix
index 294eb50b5fe..84bbb0bc8ec 100644
--- a/nixos/tests/postgis.nix
+++ b/nixos/tests/postgis.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "postgis";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ lsix ];
@@ -20,10 +20,10 @@ import ./make-test.nix ({ pkgs, ...} : {
   };
 
   testScript = ''
-    startAll;
-    $master->waitForUnit("postgresql");
-    $master->sleep(10); # Hopefully this is long enough!!
-    $master->succeed("sudo -u postgres psql -c 'CREATE EXTENSION postgis;'");
-    $master->succeed("sudo -u postgres psql -c 'CREATE EXTENSION postgis_topology;'");
+    start_all()
+    master.wait_for_unit("postgresql")
+    master.sleep(10)  # Hopefully this is long enough!!
+    master.succeed("sudo -u postgres psql -c 'CREATE EXTENSION postgis;'")
+    master.succeed("sudo -u postgres psql -c 'CREATE EXTENSION postgis_topology;'")
   '';
 })
diff --git a/nixos/tests/quagga.nix b/nixos/tests/quagga.nix
index 6aee7ea57f0..04590aa0eb3 100644
--- a/nixos/tests/quagga.nix
+++ b/nixos/tests/quagga.nix
@@ -5,7 +5,7 @@
 #
 # All interfaces are in OSPF Area 0.
 
-import ./make-test.nix ({ pkgs, ... }:
+import ./make-test-python.nix ({ pkgs, ... }:
   let
 
     ifAddr = node: iface: (pkgs.lib.head node.config.networking.interfaces.${iface}.ipv4.addresses).address;
@@ -74,23 +74,23 @@ import ./make-test.nix ({ pkgs, ... }:
       testScript =
         { ... }:
         ''
-          startAll;
+          start_all()
 
           # Wait for the networking to start on all machines
-          $_->waitForUnit("network.target") foreach values %vms;
+          for machine in client, router1, router2, server:
+              machine.wait_for_unit("network.target")
 
-          # Wait for OSPF to form adjacencies
-          for my $gw ($router1, $router2) {
-              $gw->waitForUnit("ospfd");
-              $gw->waitUntilSucceeds("vtysh -c 'show ip ospf neighbor' | grep Full");
-              $gw->waitUntilSucceeds("vtysh -c 'show ip route' | grep '^O>'");
-          }
+          with subtest("Wait for OSPF to form adjacencies"):
+              for gw in router1, router2:
+                  gw.wait_for_unit("ospfd")
+                  gw.wait_until_succeeds("vtysh -c 'show ip ospf neighbor' | grep Full")
+                  gw.wait_until_succeeds("vtysh -c 'show ip route' | grep '^O>'")
 
-          # Test ICMP.
-          $client->succeed("ping -c 3 server >&2");
+          with subtest("Test ICMP"):
+              client.wait_until_succeeds("ping -c 3 server >&2")
 
-          # Test whether HTTP works.
-          $server->waitForUnit("httpd");
-          $client->succeed("curl --fail http://server/ >&2");
+          with subtest("Test whether HTTP works"):
+              server.wait_for_unit("httpd")
+              client.succeed("curl --fail http://server/ >&2")
         '';
     })
diff --git a/nixos/tests/sddm.nix b/nixos/tests/sddm.nix
index 4bdcd701dcf..a145705250f 100644
--- a/nixos/tests/sddm.nix
+++ b/nixos/tests/sddm.nix
@@ -16,9 +16,8 @@ let
         imports = [ ./common/user-account.nix ];
         services.xserver.enable = true;
         services.xserver.displayManager.sddm.enable = true;
-        services.xserver.windowManager.default = "icewm";
+        services.xserver.displayManager.defaultSession = "none+icewm";
         services.xserver.windowManager.icewm.enable = true;
-        services.xserver.desktopManager.default = "none";
       };
 
       enableOCR = true;
@@ -52,9 +51,8 @@ let
             user = "alice";
           };
         };
-        services.xserver.windowManager.default = "icewm";
+        services.xserver.displayManager.defaultSession = "none+icewm";
         services.xserver.windowManager.icewm.enable = true;
-        services.xserver.desktopManager.default = "none";
       };
 
       testScript = { nodes, ... }: let
diff --git a/nixos/tests/sonarr.nix b/nixos/tests/sonarr.nix
index 3e84445099a..764a4d05b38 100644
--- a/nixos/tests/sonarr.nix
+++ b/nixos/tests/sonarr.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ lib, ... }:
+import ./make-test-python.nix ({ lib, ... }:
 
 with lib;
 
@@ -11,8 +11,8 @@ with lib;
     { services.sonarr.enable = true; };
 
   testScript = ''
-    $machine->waitForUnit('sonarr.service');
-    $machine->waitForOpenPort('8989');
-    $machine->succeed("curl --fail http://localhost:8989/");
+    machine.wait_for_unit("sonarr.service")
+    machine.wait_for_open_port("8989")
+    machine.succeed("curl --fail http://localhost:8989/")
   '';
 })
diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix
index 0dba3697980..7076bd77b77 100644
--- a/nixos/tests/switch-test.nix
+++ b/nixos/tests/switch-test.nix
@@ -1,6 +1,6 @@
 # Test configuration switching.
 
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "switch-test";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ gleber ];
@@ -28,7 +28,11 @@ import ./make-test.nix ({ pkgs, ...} : {
       exec env -i "$@" | tee /dev/stderr
     '';
   in ''
-    $machine->succeed("${stderrRunner} ${originalSystem}/bin/switch-to-configuration test");
-    $machine->succeed("${stderrRunner} ${otherSystem}/bin/switch-to-configuration test");
+    machine.succeed(
+        "${stderrRunner} ${originalSystem}/bin/switch-to-configuration test"
+    )
+    machine.succeed(
+        "${stderrRunner} ${otherSystem}/bin/switch-to-configuration test"
+    )
   '';
 })
diff --git a/nixos/tests/systemd-analyze.nix b/nixos/tests/systemd-analyze.nix
new file mode 100644
index 00000000000..a78ba08cd55
--- /dev/null
+++ b/nixos/tests/systemd-analyze.nix
@@ -0,0 +1,46 @@
+import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
+
+{
+  name = "systemd-analyze";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ raskin ];
+  };
+
+  machine =
+    { pkgs, lib, ... }:
+    { boot.kernelPackages = lib.mkIf latestKernel pkgs.linuxPackages_latest;
+      sound.enable = true; # needed for the factl test, /dev/snd/* exists without them but udev doesn't care then
+    };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+
+    # We create a special output directory to copy it as a whole
+    with subtest("Prepare output dir"):
+        machine.succeed("mkdir systemd-analyze")
+
+
+    # Save the output into a file with given name inside the common
+    # output directory
+    def run_systemd_analyze(args, name):
+        tgt_dir = "systemd-analyze"
+        machine.succeed(
+            "systemd-analyze {} > {}/{} 2> {}/{}.err".format(
+                " ".join(args), tgt_dir, name, tgt_dir, name
+            )
+        )
+
+
+    with subtest("Print statistics"):
+        run_systemd_analyze(["blame"], "blame.txt")
+        run_systemd_analyze(["critical-chain"], "critical-chain.txt")
+        run_systemd_analyze(["dot"], "dependencies.dot")
+        run_systemd_analyze(["plot"], "systemd-analyze.svg")
+
+    # We copy the main graph into the $out (toplevel), and we also copy
+    # the entire output directory with additional data
+    with subtest("Copying the resulting data into $out"):
+        machine.copy_from_vm("systemd-analyze/", "")
+        machine.copy_from_vm("systemd-analyze/systemd-analyze.svg", "")
+  '';
+})
diff --git a/nixos/tests/systemd-timesyncd.nix b/nixos/tests/systemd-timesyncd.nix
index d12b8eb2bf7..ad5b9a47383 100644
--- a/nixos/tests/systemd-timesyncd.nix
+++ b/nixos/tests/systemd-timesyncd.nix
@@ -1,7 +1,7 @@
 # Regression test for systemd-timesync having moved the state directory without
 # upstream providing a migration path. https://github.com/systemd/systemd/issues/12131
 
-import ./make-test.nix (let
+import ./make-test-python.nix (let
   common = { lib, ... }: {
     # override the `false` value from the qemu-vm base profile
     services.timesyncd.enable = lib.mkForce true;
@@ -25,28 +25,28 @@ in {
   };
 
   testScript = ''
-    startAll;
-    $current->succeed('systemctl status systemd-timesyncd.service');
+    start_all()
+    current.succeed("systemctl status systemd-timesyncd.service")
     # on a new install with a recent systemd there should not be any
     # leftovers from the dynamic user mess
-    $current->succeed('test -e /var/lib/systemd/timesync');
-    $current->succeed('test ! -L /var/lib/systemd/timesync');
+    current.succeed("test -e /var/lib/systemd/timesync")
+    current.succeed("test ! -L /var/lib/systemd/timesync")
 
     # timesyncd should be running on the upgrading system since we fixed the
     # file bits in the activation script
-    $pre1909->succeed('systemctl status systemd-timesyncd.service');
+    pre1909.succeed("systemctl status systemd-timesyncd.service")
 
     # the path should be gone after the migration
-    $pre1909->succeed('test ! -e /var/lib/private/systemd/timesync');
+    pre1909.succeed("test ! -e /var/lib/private/systemd/timesync")
 
     # and the new path should no longer be a symlink
-    $pre1909->succeed('test -e /var/lib/systemd/timesync');
-    $pre1909->succeed('test ! -L /var/lib/systemd/timesync');
+    pre1909.succeed("test -e /var/lib/systemd/timesync")
+    pre1909.succeed("test ! -L /var/lib/systemd/timesync")
 
     # after a restart things should still work and not fail in the activation
     # scripts and cause the boot to fail..
-    $pre1909->shutdown;
-    $pre1909->start;
-    $pre1909->succeed('systemctl status systemd-timesyncd.service');
+    pre1909.shutdown()
+    pre1909.start()
+    pre1909.succeed("systemctl status systemd-timesyncd.service")
   '';
 })
diff --git a/nixos/tests/wireguard/namespaces.nix b/nixos/tests/wireguard/namespaces.nix
index 94f993d9475..c8a4e3bb52a 100644
--- a/nixos/tests/wireguard/namespaces.nix
+++ b/nixos/tests/wireguard/namespaces.nix
@@ -13,7 +13,7 @@ let
 
 in
 
-import ../make-test.nix ({ pkgs, ...} : {
+import ../make-test-python.nix ({ pkgs, ...} : {
   name = "wireguard-with-namespaces";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ asymmetric ];
@@ -65,16 +65,14 @@ import ../make-test.nix ({ pkgs, ...} : {
   };
 
   testScript = ''
-    startAll();
+    start_all()
 
-    $peer0->waitForUnit("wireguard-wg0.service");
-    $peer1->waitForUnit("wireguard-wg0.service");
-    $peer2->waitForUnit("wireguard-wg0.service");
-    $peer3->waitForUnit("wireguard-wg0.service");
+    for machine in peer0, peer1, peer2, peer3:
+        machine.wait_for_unit("wireguard-wg0.service")
 
-    $peer0->succeed("ip -n ${socketNamespace} link show wg0");
-    $peer1->succeed("ip -n ${interfaceNamespace} link show wg0");
-    $peer2->succeed("ip -n ${interfaceNamespace} link show wg0");
-    $peer3->succeed("ip link show wg0");
+    peer0.succeed("ip -n ${socketNamespace} link show wg0")
+    peer1.succeed("ip -n ${interfaceNamespace} link show wg0")
+    peer2.succeed("ip -n ${interfaceNamespace} link show wg0")
+    peer3.succeed("ip link show wg0")
   '';
 })
diff --git a/nixos/tests/xmonad.nix b/nixos/tests/xmonad.nix
index c2e5ba60d7b..ef711f8dcf6 100644
--- a/nixos/tests/xmonad.nix
+++ b/nixos/tests/xmonad.nix
@@ -4,10 +4,10 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     maintainers = [ nequissimus ];
   };
 
-  machine = { lib, pkgs, ... }: {
+  machine = { pkgs, ... }: {
     imports = [ ./common/x11.nix ./common/user-account.nix ];
     services.xserver.displayManager.auto.user = "alice";
-    services.xserver.windowManager.default = lib.mkForce "xmonad";
+    services.xserver.displayManager.defaultSession = "none+xmonad";
     services.xserver.windowManager.xmonad = {
       enable = true;
       enableContribAndExtras = true;
@@ -27,13 +27,13 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     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.send_key("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.send_key("alt-shift-ret")
     machine.wait_for_window("${user.name}.*machine")
     machine.sleep(1)
     machine.screenshot("terminal")
diff --git a/nixos/tests/zsh-history.nix b/nixos/tests/zsh-history.nix
new file mode 100644
index 00000000000..4380ec9adfd
--- /dev/null
+++ b/nixos/tests/zsh-history.nix
@@ -0,0 +1,35 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "zsh-history";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ kampka ];
+  };
+
+  nodes.default = { ... }: {
+    programs = {
+      zsh.enable = true;
+    };
+    environment.systemPackages = [ pkgs.zsh-history ];
+    programs.zsh.interactiveShellInit = ''
+      source ${pkgs.zsh-history.out}/share/zsh/init.zsh
+    '';
+    users.users.root.shell = "${pkgs.zsh}/bin/zsh";
+  };
+
+  testScript = ''
+    start_all()
+    default.wait_for_unit("multi-user.target")
+    default.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
+
+    # Login
+    default.wait_until_tty_matches(1, "login: ")
+    default.send_chars("root\n")
+    default.wait_until_tty_matches(1, "root@default>")
+
+    # Generate some history
+    default.send_chars("echo foobar\n")
+    default.wait_until_tty_matches(1, "foobar")
+
+    # Ensure that command was recorded in history
+    default.succeed("/run/current-system/sw/bin/history list | grep -q foobar")
+  '';
+})