summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/release-notes/rl-2003.xml7
-rw-r--r--nixos/modules/config/networking.nix17
-rw-r--r--nixos/modules/i18n/input-method/ibus.nix10
-rw-r--r--nixos/modules/misc/ids.nix4
-rw-r--r--nixos/modules/module-list.nix2
-rw-r--r--nixos/modules/rename.nix1
-rw-r--r--nixos/modules/services/databases/4store-endpoint.nix74
-rw-r--r--nixos/modules/services/databases/4store.nix72
-rw-r--r--nixos/modules/services/misc/apache-kafka.nix2
-rw-r--r--nixos/modules/services/misc/gitea.nix5
-rw-r--r--nixos/modules/services/networking/yggdrasil.nix6
-rw-r--r--nixos/modules/services/torrent/transmission.nix19
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix9
-rw-r--r--nixos/modules/services/web-servers/apache-httpd/default.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix2
-rw-r--r--nixos/modules/system/boot/networkd.nix4
-rw-r--r--nixos/tests/all-tests.nix3
-rw-r--r--nixos/tests/consul.nix143
-rw-r--r--nixos/tests/dhparams.nix98
-rw-r--r--nixos/tests/docker-tools-overlay.nix15
-rw-r--r--nixos/tests/ecryptfs.nix121
-rw-r--r--nixos/tests/env.nix25
-rw-r--r--nixos/tests/gitolite-fcgiwrap.nix93
-rw-r--r--nixos/tests/installed-tests/default.nix76
-rw-r--r--nixos/tests/installed-tests/ibus.nix20
-rw-r--r--nixos/tests/ipv6.nix81
-rw-r--r--nixos/tests/munin.nix44
-rw-r--r--nixos/tests/pam-u2f.nix8
-rw-r--r--nixos/tests/resolv.nix46
-rw-r--r--nixos/tests/timezone.nix95
30 files changed, 652 insertions, 452 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2003.xml b/nixos/doc/manual/release-notes/rl-2003.xml
index b8af15f59c9..886b16ef965 100644
--- a/nixos/doc/manual/release-notes/rl-2003.xml
+++ b/nixos/doc/manual/release-notes/rl-2003.xml
@@ -208,7 +208,7 @@
    <listitem>
     <para>
      The packages <literal>openobex</literal> and <literal>obexftp</literal>
-     are no loger installed when enabling bluetooth via
+     are no longer installed when enabling Bluetooth via
      <option>hardware.bluetooth.enable</option>.
     </para>
    </listitem>
@@ -220,6 +220,11 @@
      in conjunction with an external webserver to replace this functionality.
     </para>
    </listitem>
+   <listitem>
+    <para>
+     The fourStore and fourStoreEndpoint modules have been removed.
+    </para>
+   </listitem>
   </itemizedlist>
  </section>
 
diff --git a/nixos/modules/config/networking.nix b/nixos/modules/config/networking.nix
index a89667ea221..3560e579e47 100644
--- a/nixos/modules/config/networking.nix
+++ b/nixos/modules/config/networking.nix
@@ -41,19 +41,6 @@ in
       '';
     };
 
-    networking.hostConf = lib.mkOption {
-      type = types.lines;
-      default = "multi on";
-      example = ''
-        multi on
-        reorder on
-        trim lan
-      '';
-      description = ''
-        The contents of <filename>/etc/host.conf</filename>. See also <citerefentry><refentrytitle>host.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
-      '';
-    };
-
     networking.timeServers = mkOption {
       default = [
         "0.nixos.pool.ntp.org"
@@ -186,7 +173,9 @@ in
         '';
 
         # /etc/host.conf: resolver configuration file
-        "host.conf".text = cfg.hostConf;
+        "host.conf".text = ''
+          multi on
+        '';
 
       } // optionalAttrs (pkgs.stdenv.hostPlatform.libc == "glibc") {
         # /etc/rpc: RPC program numbers.
diff --git a/nixos/modules/i18n/input-method/ibus.nix b/nixos/modules/i18n/input-method/ibus.nix
index 956c521dde0..d7857976fcc 100644
--- a/nixos/modules/i18n/input-method/ibus.nix
+++ b/nixos/modules/i18n/input-method/ibus.nix
@@ -53,9 +53,15 @@ in
   config = mkIf (config.i18n.inputMethod.enabled == "ibus") {
     i18n.inputMethod.package = ibusPackage;
 
+    environment.systemPackages = [
+      ibusAutostart
+    ];
+
     # Without dconf enabled it is impossible to use IBus
-    environment.systemPackages = with pkgs; [
-      dconf ibusAutostart
+    programs.dconf.enable = true;
+
+    services.dbus.packages = [
+      ibusAutostart
     ];
 
     environment.variables = {
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index f8b188e7b1c..bedd87a368e 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -80,8 +80,8 @@ in
       #kdm = 39; # dropped in 17.03
       #ghostone = 40; # dropped in 18.03
       git = 41;
-      fourstore = 42;
-      fourstorehttp = 43;
+      #fourstore = 42; # dropped in 20.03
+      #fourstorehttp = 43; # dropped in 20.03
       virtuoso = 44;
       rtkit = 45;
       dovecot2 = 46;
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 8e373550bb3..bb217d873bb 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -254,8 +254,6 @@
   ./services/continuous-integration/jenkins/default.nix
   ./services/continuous-integration/jenkins/job-builder.nix
   ./services/continuous-integration/jenkins/slave.nix
-  ./services/databases/4store-endpoint.nix
-  ./services/databases/4store.nix
   ./services/databases/aerospike.nix
   ./services/databases/cassandra.nix
   ./services/databases/clickhouse.nix
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index e392fef54dd..83b29613d9c 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -239,6 +239,7 @@ with lib;
     (mkRemovedOptionModule [ "systemd" "generator-packages" ] "Use systemd.packages instead.")
     (mkRemovedOptionModule [ "fonts" "enableCoreFonts" ] "Use fonts.fonts = [ pkgs.corefonts ]; instead.")
     (mkRemovedOptionModule [ "networking" "vpnc" ] "Use environment.etc.\"vpnc/service.conf\" instead.")
+    (mkRemovedOptionModule [ "networking" "hostConf" ] "Use environment.etc.\"host.conf\" instead.")
 
     # ZSH
     (mkRenamedOptionModule [ "programs" "zsh" "enableSyntaxHighlighting" ] [ "programs" "zsh" "syntaxHighlighting" "enable" ])
diff --git a/nixos/modules/services/databases/4store-endpoint.nix b/nixos/modules/services/databases/4store-endpoint.nix
deleted file mode 100644
index 59ed0e5f0af..00000000000
--- a/nixos/modules/services/databases/4store-endpoint.nix
+++ /dev/null
@@ -1,74 +0,0 @@
-{ config, lib, pkgs, ... }:
-let
-  cfg = config.services.fourStoreEndpoint;
-  endpointUser = "fourstorehttp";
-  run = "${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${endpointUser} -c";
-in
-with lib;
-{
-
-  ###### interface
-
-  options = {
-
-    services.fourStoreEndpoint = {
-
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable 4Store SPARQL endpoint.";
-      };
-
-      database = mkOption {
-        default = config.services.fourStore.database;
-        description = "RDF database name to expose via the endpoint. Defaults to local 4Store database name.";
-      };
-
-      listenAddress = mkOption {
-        default = null;
-        description = "IP address to listen on.";
-      };
-
-      port = mkOption {
-        default = 8080;
-        description = "port to listen on.";
-      };
-
-      options = mkOption {
-        default = "";
-        description = "Extra CLI options to pass to 4Store's 4s-httpd process.";
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    assertions = singleton
-      { assertion = cfg.enable -> cfg.database != "";
-        message = "Must specify 4Store database name";
-      };
-
-    users.users = singleton
-      { name = endpointUser;
-        uid = config.ids.uids.fourstorehttp;
-        description = "4Store SPARQL endpoint user";
-      };
-
-    services.avahi.enable = true;
-
-    systemd.services."4store-endpoint" = {
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-
-      script = ''
-        ${run} '${pkgs.rdf4store}/bin/4s-httpd -D ${cfg.options} ${if cfg.listenAddress!=null then "-H ${cfg.listenAddress}" else "" } -p ${toString cfg.port} ${cfg.database}'
-      '';
-    };
-
-  };
-
-}
diff --git a/nixos/modules/services/databases/4store.nix b/nixos/modules/services/databases/4store.nix
deleted file mode 100644
index be4351c1c38..00000000000
--- a/nixos/modules/services/databases/4store.nix
+++ /dev/null
@@ -1,72 +0,0 @@
-{ config, lib, pkgs, ... }:
-let
-  cfg = config.services.fourStore;
-  stateDir = "/var/lib/4store";
-  fourStoreUser = "fourstore";
-  run = "${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${fourStoreUser}";
-in
-with lib;
-{
-
-  ###### interface
-
-  options = {
-
-    services.fourStore = {
-
-      enable = mkOption {
-        default = false;
-        description = "Whether to enable 4Store RDF database server.";
-      };
-
-      database = mkOption {
-        default = "";
-        description = "RDF database name. If it doesn't exist, it will be created. Databases are stored in ${stateDir}.";
-      };
-
-      options = mkOption {
-        default = "";
-        description = "Extra CLI options to pass to 4Store.";
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf cfg.enable {
-
-    assertions = singleton
-      { assertion = cfg.enable -> cfg.database != "";
-        message = "Must specify 4Store database name.";
-      };
-
-    users.users = singleton
-      { name = fourStoreUser;
-        uid = config.ids.uids.fourstore;
-        description = "4Store database user";
-        home = stateDir;
-      };
-
-    services.avahi.enable = true;
-
-    systemd.services."4store" = {
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-
-      preStart = ''
-        mkdir -p ${stateDir}/
-        chown ${fourStoreUser} ${stateDir}
-        if ! test -e "${stateDir}/${cfg.database}"; then
-          ${run} -c '${pkgs.rdf4store}/bin/4s-backend-setup ${cfg.database}'
-        fi
-      '';
-
-      script = ''
-        ${run} -c '${pkgs.rdf4store}/bin/4s-backend -D ${cfg.options} ${cfg.database}'
-      '';
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/apache-kafka.nix b/nixos/modules/services/misc/apache-kafka.nix
index 798e902ccae..46308f74dc9 100644
--- a/nixos/modules/services/misc/apache-kafka.nix
+++ b/nixos/modules/services/misc/apache-kafka.nix
@@ -131,7 +131,7 @@ in {
       home = head cfg.logDirs;
     };
 
-    systemd.tmpfiles.rules = map (logDir: "d '${logDir} 0700 apache-kafka - - -") cfg.logDirs;
+    systemd.tmpfiles.rules = map (logDir: "d '${logDir}' 0700 apache-kafka - - -") cfg.logDirs;
 
     systemd.services.apache-kafka = {
       description = "Apache Kafka Daemon";
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index b6f4d88adbe..258476dd9fe 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -396,9 +396,7 @@ in
         Restart = "always";
 
         # Filesystem
-        ProtectSystem = "strict";
         ProtectHome = true;
-        PrivateTmp = true;
         PrivateDevices = true;
         ProtectKernelTunables = true;
         ProtectKernelModules = true;
@@ -413,7 +411,7 @@ in
         PrivateMounts = true;
         PrivateUsers = true;
         MemoryDenyWriteExecute = true;
-        SystemCallFilter = "~@chown @clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @privileged @raw-io @reboot @resources @setuid @swap";
+        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @resources @setuid @swap";
         SystemCallArchitectures = "native";
         RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
       };
@@ -475,4 +473,5 @@ in
       timerConfig.OnCalendar = cfg.dump.interval;
     };
   };
+  meta.maintainers = with lib.maintainers; [ srhb ];
 }
diff --git a/nixos/modules/services/networking/yggdrasil.nix b/nixos/modules/services/networking/yggdrasil.nix
index 5d65f8e3413..9e675ecd6f4 100644
--- a/nixos/modules/services/networking/yggdrasil.nix
+++ b/nixos/modules/services/networking/yggdrasil.nix
@@ -12,11 +12,11 @@ let
   configFileProvided = (cfg.configFile != null);
   generateConfig = (
     if configProvided && configFileProvided then
-      "${pkgs.jq}/bin/jq -s add /run/yggdrasil/configFile.json ${configAsFile}"
+      "${pkgs.jq}/bin/jq -s add ${configAsFile} ${cfg.configFile}"
     else if configProvided then
       "cat ${configAsFile}"
     else if configFileProvided then
-      "cat /run/yggdrasil/configFile.json"
+      "cat ${cfg.configFile}"
     else
       "${cfg.package}/bin/yggdrasil -genconf"
   );
@@ -147,7 +147,7 @@ in {
         RuntimeDirectory = "yggdrasil";
         RuntimeDirectoryMode = "0700";
         BindReadOnlyPaths = mkIf configFileProvided
-          [ "${cfg.configFile}:/run/yggdrasil/configFile.json" ];
+          [ "${cfg.configFile}" ];
 
         # TODO: as of yggdrasil 0.3.8 and systemd 243, yggdrasil fails
         # to set up the network adapter when DynamicUser is set.  See
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index 7409eb8cdcb..412f9180375 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -7,6 +7,7 @@ let
   apparmor = config.security.apparmor.enable;
 
   homeDir = cfg.home;
+  downloadDirPermissions = cfg.downloadDirPermissions;
   downloadDir = "${homeDir}/Downloads";
   incompleteDir = "${homeDir}/.incomplete";
 
@@ -16,16 +17,14 @@ let
   # for users in group "transmission" to have access to torrents
   fullSettings = { umask = 2; download-dir = downloadDir; incomplete-dir = incompleteDir; } // cfg.settings;
 
-  # Directories transmission expects to exist and be ug+rwx.
-  directoriesToManage = [ homeDir settingsDir fullSettings.download-dir fullSettings.incomplete-dir ];
-
   preStart = pkgs.writeScript "transmission-pre-start" ''
     #!${pkgs.runtimeShell}
     set -ex
-    for DIR in ${escapeShellArgs directoriesToManage}; do
+    for DIR in "${homeDir}" "${settingsDir}" "${fullSettings.download-dir}" "${fullSettings.incomplete-dir}"; do
       mkdir -p "$DIR"
-      chmod 770 "$DIR"
     done
+    chmod 700 "${homeDir}" "${settingsDir}"
+    chmod ${downloadDirPermissions} "${fullSettings.download-dir}" "${fullSettings.incomplete-dir}"
     cp -f ${settingsFile} ${settingsDir}/settings.json
   '';
 in
@@ -71,6 +70,16 @@ in
         '';
       };
 
+      downloadDirPermissions = mkOption {
+        type = types.string;
+        default = "770";
+        example = "775";
+        description = ''
+          The permissions to set for download-dir and incomplete-dir.
+          They will be applied on every service start.
+        '';
+      };
+
       port = mkOption {
         type = types.int;
         default = 9091;
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index b67f0880878..e3a2db398e6 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -31,8 +31,12 @@ let
   occ = pkgs.writeScriptBin "nextcloud-occ" ''
     #! ${pkgs.stdenv.shell}
     cd ${pkgs.nextcloud}
-    exec /run/wrappers/bin/sudo -u nextcloud \
-      NEXTCLOUD_CONFIG_DIR="${cfg.home}/config" \
+    sudo=exec
+    if [[ "$USER" != nextcloud ]]; then
+      sudo='exec /run/wrappers/bin/sudo -u nextcloud --preserve-env=NEXTCLOUD_CONFIG_DIR'
+    fi
+    export NEXTCLOUD_CONFIG_DIR="${cfg.home}/config"
+    $sudo \
       ${phpPackage}/bin/php \
       -c ${pkgs.writeText "php.ini" phpOptionsStr}\
       occ $*
@@ -420,6 +424,7 @@ in {
         nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable {
           serviceConfig.Type = "oneshot";
           serviceConfig.ExecStart = "${occ}/bin/nextcloud-occ app:update --all";
+          serviceConfig.User = "nextcloud";
           startAt = cfg.autoUpdateApps.startAt;
         };
       };
diff --git a/nixos/modules/services/web-servers/apache-httpd/default.nix b/nixos/modules/services/web-servers/apache-httpd/default.nix
index f5a6051b4b5..850d3052533 100644
--- a/nixos/modules/services/web-servers/apache-httpd/default.nix
+++ b/nixos/modules/services/web-servers/apache-httpd/default.nix
@@ -367,7 +367,7 @@ in
         type = types.lines;
         default = "";
         description = ''
-          Cnfiguration lines appended to the generated Apache
+          Configuration lines appended to the generated Apache
           configuration file. Note that this mechanism may not work
           when <option>configFile</option> is overridden.
         '';
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
index 25ef1cbfc67..99db5b17b64 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -163,7 +163,7 @@ in
 
     # Settings from elementary-default-settings
     environment.sessionVariables.GTK_CSD = "1";
-    environment.sessionVariables.GTK_MODULES = "pantheon-filechooser-module";
+    environment.sessionVariables.GTK3_MODULES = [ "pantheon-filechooser-module" ];
     environment.etc."gtk-3.0/settings.ini".source = "${pkgs.pantheon.elementary-default-settings}/etc/gtk-3.0/settings.ini";
 
     environment.pathsToLink = [
diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
index 226769f1059..58d914d0810 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -11,7 +11,7 @@ let
   checkLink = checkUnitConfig "Link" [
     (assertOnlyFields [
       "Description" "Alias" "MACAddressPolicy" "MACAddress" "NamePolicy" "Name" "OriginalName"
-      "MTUBytes" "BitsPerSecond" "Duplex" "AutoNegotiation" "WakeOnLan" "Port"
+      "MTUBytes" "BitsPerSecond" "Duplex" "AutoNegotiation" "WakeOnLan" "Port" "Advertise"
       "TCPSegmentationOffload" "TCP6SegmentationOffload" "GenericSegmentationOffload"
       "GenericReceiveOffload" "LargeReceiveOffload" "RxChannels" "TxChannels"
       "OtherChannels" "CombinedChannels"
@@ -276,7 +276,7 @@ let
     (assertValueOneOf "ARP" boolValues)
     (assertValueOneOf "Multicast" boolValues)
     (assertValueOneOf "Unmanaged" boolValues)
-    (assertValueOneOf "RequiredForOnline" boolValues)
+    (assertValueOneOf "RequiredForOnline" (boolValues ++ ["off" "no-carrier" "dormant" "degraded-carrier" "carrier" "degraded" "enslaved" "routable"]))
   ];
 
 
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index fbc8b511f3b..23ad22ee5a1 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -48,6 +48,7 @@ in
   clickhouse = handleTest ./clickhouse.nix {};
   cloud-init = handleTest ./cloud-init.nix {};
   codimd = handleTest ./codimd.nix {};
+  consul = handleTest ./consul.nix {};
   containers-bridge = handleTest ./containers-bridge.nix {};
   containers-ephemeral = handleTest ./containers-ephemeral.nix {};
   containers-extra_veth = handleTest ./containers-extra_veth.nix {};
@@ -93,6 +94,7 @@ in
   gitea = handleTest ./gitea.nix {};
   gitlab = handleTest ./gitlab.nix {};
   gitolite = handleTest ./gitolite.nix {};
+  gitolite-fcgiwrap = handleTest ./gitolite-fcgiwrap.nix {};
   glusterfs = handleTest ./glusterfs.nix {};
   gnome3-xorg = handleTest ./gnome3-xorg.nix {};
   gnome3 = handleTest ./gnome3.nix {};
@@ -267,6 +269,7 @@ in
   taskserver = handleTest ./taskserver.nix {};
   telegraf = handleTest ./telegraf.nix {};
   tiddlywiki = handleTest ./tiddlywiki.nix {};
+  timezone = handleTest ./timezone.nix {};
   tinydns = handleTest ./tinydns.nix {};
   tor = handleTest ./tor.nix {};
   transmission = handleTest ./transmission.nix {};
diff --git a/nixos/tests/consul.nix b/nixos/tests/consul.nix
new file mode 100644
index 00000000000..6600dae4770
--- /dev/null
+++ b/nixos/tests/consul.nix
@@ -0,0 +1,143 @@
+import ./make-test-python.nix ({pkgs, lib, ...}:
+
+let
+  # Settings for both servers and agents
+  webUi = true;
+  retry_interval = "1s";
+  raft_multiplier = 1;
+
+  defaultExtraConfig = {
+    inherit retry_interval;
+    performance = {
+      inherit raft_multiplier;
+    };
+  };
+
+  allConsensusServerHosts = [
+    "192.168.1.1"
+    "192.168.1.2"
+    "192.168.1.3"
+  ];
+
+  allConsensusClientHosts = [
+    "192.168.2.1"
+    "192.168.2.2"
+  ];
+
+  firewallSettings = {
+    # See https://www.consul.io/docs/install/ports.html
+    allowedTCPPorts = [ 8301 8302 8600 8500 8300 ];
+    allowedUDPPorts = [ 8301 8302 8600 ];
+  };
+
+  client = index: { pkgs, ... }:
+    let
+      ip = builtins.elemAt allConsensusClientHosts index;
+    in
+      {
+        environment.systemPackages = [ pkgs.consul ];
+
+        networking.interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+          { address = ip; prefixLength = 16; }
+        ];
+        networking.firewall = firewallSettings;
+
+        services.consul = {
+          enable = true;
+          inherit webUi;
+          extraConfig = defaultExtraConfig // {
+            server = false;
+            retry_join = allConsensusServerHosts;
+            bind_addr = ip;
+          };
+        };
+      };
+
+  server = index: { pkgs, ... }:
+    let
+      ip = builtins.elemAt allConsensusServerHosts index;
+    in
+      {
+        networking.interfaces.eth1.ipv4.addresses = pkgs.lib.mkOverride 0 [
+          { address = builtins.elemAt allConsensusServerHosts index; prefixLength = 16; }
+        ];
+        networking.firewall = firewallSettings;
+
+        services.consul =
+          let
+            thisConsensusServerHost = builtins.elemAt allConsensusServerHosts index;
+          in
+          assert builtins.elem thisConsensusServerHost allConsensusServerHosts;
+          {
+            enable = true;
+            inherit webUi;
+            extraConfig = defaultExtraConfig // {
+              server = true;
+              bootstrap_expect = builtins.length allConsensusServerHosts;
+              retry_join =
+                # If there's only 1 node in the network, we allow self-join;
+                # otherwise, the node must not try to join itself, and join only the other servers.
+                # See https://github.com/hashicorp/consul/issues/2868
+                if builtins.length allConsensusServerHosts == 1
+                  then allConsensusServerHosts
+                  else builtins.filter (h: h != thisConsensusServerHost) allConsensusServerHosts;
+              bind_addr = ip;
+            };
+          };
+      };
+in {
+  name = "consul";
+
+  nodes = {
+    server1 = server 0;
+    server2 = server 1;
+    server3 = server 2;
+
+    client1 = client 0;
+    client2 = client 1;
+  };
+
+  testScript = ''
+    servers = [server1, server2, server3]
+    machines = [server1, server2, server3, client1, client2]
+
+    for m in machines:
+        m.wait_for_unit("consul.service")
+
+    for m in machines:
+        m.wait_until_succeeds("[ $(consul members | grep -o alive | wc -l) == 5 ]")
+
+    client1.succeed("consul kv put testkey 42")
+    client2.succeed("[ $(consul kv get testkey) == 42 ]")
+
+    # Test that the cluster can tolearate failures of any single server:
+    for server in servers:
+        server.crash()
+
+        # For each client, wait until they have connection again
+        # using `kv get -recurse` before issuing commands.
+        client1.wait_until_succeeds("consul kv get -recurse")
+        client2.wait_until_succeeds("consul kv get -recurse")
+
+        # Do some consul actions while one server is down.
+        client1.succeed("consul kv put testkey 43")
+        client2.succeed("[ $(consul kv get testkey) == 43 ]")
+        client2.succeed("consul kv delete testkey")
+
+        # Restart crashed machine.
+        server.start()
+
+        # Wait for recovery.
+        for m in machines:
+            m.wait_until_succeeds("[ $(consul members | grep -o alive | wc -l) == 5 ]")
+
+        # Wait for client connections.
+        client1.wait_until_succeeds("consul kv get -recurse")
+        client2.wait_until_succeeds("consul kv get -recurse")
+
+        # Do some consul actions with server back up.
+        client1.succeed("consul kv put testkey 44")
+        client2.succeed("[ $(consul kv get testkey) == 44 ]")
+        client2.succeed("consul kv delete testkey")
+  '';
+})
diff --git a/nixos/tests/dhparams.nix b/nixos/tests/dhparams.nix
index d11dfeec5d0..a0de2911777 100644
--- a/nixos/tests/dhparams.nix
+++ b/nixos/tests/dhparams.nix
@@ -4,7 +4,7 @@ let
     environment.systemPackages = [ pkgs.openssl ];
   };
 
-in import ./make-test.nix {
+in import ./make-test-python.nix {
   name = "dhparams";
 
   nodes.generation1 = { pkgs, config, ... }: {
@@ -66,79 +66,77 @@ in import ./make-test.nix {
       node = "generation${toString gen}";
     in nodes.${node}.config.security.dhparams.params.${name}.path;
 
-    assertParamBits = gen: name: bits: let
-      path = getParamPath gen name;
-    in ''
-      $machine->nest('check bit size of ${path}', sub {
-        my $out = $machine->succeed('openssl dhparam -in ${path} -text');
-        $out =~ /^\s*DH Parameters:\s+\((\d+)\s+bit\)\s*$/m;
-        die "bit size should be ${toString bits} but it is $1 instead."
-          if $1 != ${toString bits};
-      });
-    '';
-
     switchToGeneration = gen: let
       node = "generation${toString gen}";
       inherit (nodes.${node}.config.system.build) toplevel;
       switchCmd = "${toplevel}/bin/switch-to-configuration test";
     in ''
-      $machine->nest('switch to generation ${toString gen}', sub {
-        $machine->succeed('${switchCmd}');
-        $main::machine = ''$${node};
-      });
+      with machine.nested("switch to generation ${toString gen}"):
+          machine.succeed(
+              "${switchCmd}"
+          )
+          machine = ${node}
     '';
 
   in ''
-    my $machine = $generation1;
+    import re
 
-    $machine->waitForUnit('multi-user.target');
 
-    subtest "verify startup order", sub {
-      $machine->succeed('systemctl is-active foo.service');
-    };
+    def assert_param_bits(path, bits):
+        with machine.nested(f"check bit size of {path}"):
+            output = machine.succeed(f"openssl dhparam -in {path} -text")
+            pattern = re.compile(r"^\s*DH Parameters:\s+\((\d+)\s+bit\)\s*$", re.M)
+            match = pattern.match(output)
+            if match is None:
+                raise Exception("bla")
+            if match[1] != str(bits):
+                raise Exception(f"bit size should be {bits} but it is {match[1]} instead.")
 
-    subtest "check bit sizes of dhparam files", sub {
-      ${assertParamBits 1 "foo" 16}
-      ${assertParamBits 1 "bar" 17}
-    };
+
+    machine = generation1
+
+    machine.wait_for_unit("multi-user.target")
+
+    with subtest("verify startup order"):
+        machine.succeed("systemctl is-active foo.service")
+
+    with subtest("check bit sizes of dhparam files"):
+        assert_param_bits("${getParamPath 1 "foo"}", 16)
+        assert_param_bits("${getParamPath 1 "bar"}", 17)
 
     ${switchToGeneration 2}
 
-    subtest "check whether bit size has changed", sub {
-      ${assertParamBits 2 "foo" 18}
-    };
+    with subtest("check whether bit size has changed"):
+        assert_param_bits("${getParamPath 2 "foo"}", 18)
 
-    subtest "ensure that dhparams file for 'bar' was deleted", sub {
-      $machine->fail('test -e ${getParamPath 1 "bar"}');
-    };
+    with subtest("ensure that dhparams file for 'bar' was deleted"):
+        machine.fail("test -e ${getParamPath 1 "bar"}")
 
     ${switchToGeneration 3}
 
-    subtest "ensure that 'security.dhparams.path' has been deleted", sub {
-      $machine->fail(
-        'test -e ${nodes.generation3.config.security.dhparams.path}'
-      );
-    };
+    with subtest("ensure that 'security.dhparams.path' has been deleted"):
+        machine.fail("test -e ${nodes.generation3.config.security.dhparams.path}")
 
     ${switchToGeneration 4}
 
-    subtest "check bit sizes dhparam files", sub {
-      ${assertParamBits 4 "foo2" 18}
-      ${assertParamBits 4 "bar2" 19}
-    };
+    with subtest("check bit sizes dhparam files"):
+        assert_param_bits(
+            "${getParamPath 4 "foo2"}", 18
+        )
+        assert_param_bits(
+            "${getParamPath 4 "bar2"}", 19
+        )
 
-    subtest "check whether dhparam files are in the Nix store", sub {
-      $machine->succeed(
-        'expr match ${getParamPath 4 "foo2"} ${builtins.storeDir}',
-        'expr match ${getParamPath 4 "bar2"} ${builtins.storeDir}',
-      );
-    };
+    with subtest("check whether dhparam files are in the Nix store"):
+        machine.succeed(
+            "expr match ${getParamPath 4 "foo2"} ${builtins.storeDir}",
+            "expr match ${getParamPath 4 "bar2"} ${builtins.storeDir}",
+        )
 
     ${switchToGeneration 5}
 
-    subtest "check whether defaultBitSize works as intended", sub {
-      ${assertParamBits 5 "foo3" 30}
-      ${assertParamBits 5 "bar3" 30}
-    };
+    with subtest("check whether defaultBitSize works as intended"):
+        assert_param_bits("${getParamPath 5 "foo3"}", 30)
+        assert_param_bits("${getParamPath 5 "bar3"}", 30)
   '';
 }
diff --git a/nixos/tests/docker-tools-overlay.nix b/nixos/tests/docker-tools-overlay.nix
index 637957bd3e8..1a0e0ea6775 100644
--- a/nixos/tests/docker-tools-overlay.nix
+++ b/nixos/tests/docker-tools-overlay.nix
@@ -1,6 +1,6 @@
 # this test creates a simple GNU image with docker tools and sees if it executes
 
-import ./make-test.nix ({ pkgs, ... }:
+import ./make-test-python.nix ({ pkgs, ... }:
 {
   name = "docker-tools-overlay";
   meta = with pkgs.stdenv.lib.maintainers; {
@@ -16,17 +16,18 @@ import ./make-test.nix ({ pkgs, ... }:
       };
   };
 
-  testScript =
-    ''
-      $docker->waitForUnit("sockets.target");
+  testScript = ''
+      docker.wait_for_unit("sockets.target")
 
-      $docker->succeed("docker load --input='${pkgs.dockerTools.examples.bash}'");
-      $docker->succeed("docker run --rm ${pkgs.dockerTools.examples.bash.imageName} bash --version");
+      docker.succeed(
+          "docker load --input='${pkgs.dockerTools.examples.bash}'",
+          "docker run --rm ${pkgs.dockerTools.examples.bash.imageName} bash --version",
+      )
 
       # Check if the nix store has correct user permissions depending on what
       # storage driver is used, incorrectly built images can show up as readonly.
       # drw-------  3 0 0   3 Apr 14 11:36 /nix
       # drw------- 99 0 0 100 Apr 14 11:36 /nix/store
-      $docker->succeed("docker run --rm -u 1000:1000 ${pkgs.dockerTools.examples.bash.imageName} bash --version");
+      docker.succeed("docker run --rm -u 1000:1000 ${pkgs.dockerTools.examples.bash.imageName} bash --version")
     '';
 })
diff --git a/nixos/tests/ecryptfs.nix b/nixos/tests/ecryptfs.nix
index 3f02cecb866..ef7bd13eb92 100644
--- a/nixos/tests/ecryptfs.nix
+++ b/nixos/tests/ecryptfs.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ ... }:
+import ./make-test-python.nix ({ ... }:
 {
   name = "ecryptfs";
 
@@ -10,75 +10,76 @@ import ./make-test.nix ({ ... }:
   };
 
   testScript = ''
-    $machine->waitForUnit("default.target");
+    def login_as_alice():
+        machine.wait_until_tty_matches(1, "login: ")
+        machine.send_chars("alice\n")
+        machine.wait_until_tty_matches(1, "Password: ")
+        machine.send_chars("foobar\n")
+        machine.wait_until_tty_matches(1, "alice\@machine")
 
-    # Set alice up with a password and a home
-    $machine->succeed("(echo foobar; echo foobar) | passwd alice");
-    $machine->succeed("chown -R alice.users ~alice");
 
-    # Migrate alice's home
-    my $out = $machine->succeed("echo foobar | ecryptfs-migrate-home -u alice");
-    $machine->log("ecryptfs-migrate-home said: $out");
+    def logout():
+        machine.send_chars("logout\n")
+        machine.wait_until_tty_matches(1, "login: ")
 
-    # Log alice in (ecryptfs passwhrase is wrapped during first login)
-    $machine->waitUntilTTYMatches(1, "login: ");
-    $machine->sendChars("alice\n");
-    $machine->waitUntilTTYMatches(1, "Password: ");
-    $machine->sendChars("foobar\n");
-    $machine->waitUntilTTYMatches(1, "alice\@machine");
-    $machine->sendChars("logout\n");
-    $machine->waitUntilTTYMatches(1, "login: ");
+
+    machine.wait_for_unit("default.target")
+
+    with subtest("Set alice up with a password and a home"):
+        machine.succeed("(echo foobar; echo foobar) | passwd alice")
+        machine.succeed("chown -R alice.users ~alice")
+
+    with subtest("Migrate alice's home"):
+        out = machine.succeed("echo foobar | ecryptfs-migrate-home -u alice")
+        machine.log(f"ecryptfs-migrate-home said: {out}")
+
+    with subtest("Log alice in (ecryptfs passwhrase is wrapped during first login)"):
+        login_as_alice()
+        machine.send_chars("logout\n")
+        machine.wait_until_tty_matches(1, "login: ")
 
     # Why do I need to do this??
-    $machine->succeed("su alice -c ecryptfs-umount-private || true");
-    $machine->sleep(1);
-    $machine->fail("mount | grep ecryptfs"); # check that encrypted home is not mounted
+    machine.succeed("su alice -c ecryptfs-umount-private || true")
+    machine.sleep(1)
+
+    with subtest("check that encrypted home is not mounted"):
+        machine.fail("mount | grep ecryptfs")
 
-    # Show contents of the user keyring
-    my $out = $machine->succeed("su - alice -c 'keyctl list \@u'");
-    $machine->log("keyctl unlink said: " . $out);
+    with subtest("Show contents of the user keyring"):
+        out = machine.succeed("su - alice -c 'keyctl list \@u'")
+        machine.log(f"keyctl unlink said: {out}")
 
-    # Log alice again
-    $machine->waitUntilTTYMatches(1, "login: ");
-    $machine->sendChars("alice\n");
-    $machine->waitUntilTTYMatches(1, "Password: ");
-    $machine->sendChars("foobar\n");
-    $machine->waitUntilTTYMatches(1, "alice\@machine");
+    with subtest("Log alice again"):
+        login_as_alice()
 
-    # Create some files in encrypted home
-    $machine->succeed("su alice -c 'touch ~alice/a'");
-    $machine->succeed("su alice -c 'echo c > ~alice/b'");
+    with subtest("Create some files in encrypted home"):
+        machine.succeed("su alice -c 'touch ~alice/a'")
+        machine.succeed("su alice -c 'echo c > ~alice/b'")
 
-    # Logout
-    $machine->sendChars("logout\n");
-    $machine->waitUntilTTYMatches(1, "login: ");
+    with subtest("Logout"):
+        logout()
 
     # Why do I need to do this??
-    $machine->succeed("su alice -c ecryptfs-umount-private || true");
-    $machine->sleep(1);
-
-    # Check that the filesystem is not accessible
-    $machine->fail("mount | grep ecryptfs");
-    $machine->succeed("su alice -c 'test \! -f ~alice/a'");
-    $machine->succeed("su alice -c 'test \! -f ~alice/b'");
-
-    # Log alice once more
-    $machine->waitUntilTTYMatches(1, "login: ");
-    $machine->sendChars("alice\n");
-    $machine->waitUntilTTYMatches(1, "Password: ");
-    $machine->sendChars("foobar\n");
-    $machine->waitUntilTTYMatches(1, "alice\@machine");
-
-    # Check that the files are there
-    $machine->sleep(1);
-    $machine->succeed("su alice -c 'test -f ~alice/a'");
-    $machine->succeed("su alice -c 'test -f ~alice/b'");
-    $machine->succeed(qq%test "\$(cat ~alice/b)" = "c"%);
-
-    # Catch https://github.com/NixOS/nixpkgs/issues/16766
-    $machine->succeed("su alice -c 'ls -lh ~alice/'");
-
-    $machine->sendChars("logout\n");
-    $machine->waitUntilTTYMatches(1, "login: ");
+    machine.succeed("su alice -c ecryptfs-umount-private || true")
+    machine.sleep(1)
+
+    with subtest("Check that the filesystem is not accessible"):
+        machine.fail("mount | grep ecryptfs")
+        machine.succeed("su alice -c 'test \! -f ~alice/a'")
+        machine.succeed("su alice -c 'test \! -f ~alice/b'")
+
+    with subtest("Log alice once more"):
+        login_as_alice()
+
+    with subtest("Check that the files are there"):
+        machine.sleep(1)
+        machine.succeed("su alice -c 'test -f ~alice/a'")
+        machine.succeed("su alice -c 'test -f ~alice/b'")
+        machine.succeed('test "$(cat ~alice/b)" = "c"')
+
+    with subtest("Catch https://github.com/NixOS/nixpkgs/issues/16766"):
+        machine.succeed("su alice -c 'ls -lh ~alice/'")
+
+    logout()
   '';
 })
diff --git a/nixos/tests/env.nix b/nixos/tests/env.nix
index 6c681905b19..e603338e489 100644
--- a/nixos/tests/env.nix
+++ b/nixos/tests/env.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "environment";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ nequissimus ];
@@ -20,16 +20,17 @@ import ./make-test.nix ({ pkgs, ...} : {
       };
     };
 
-  testScript =
-    ''
-      $machine->succeed('[ -L "/etc/plainFile" ]');
-      $machine->succeed('cat "/etc/plainFile" | grep "Hello World"');
-      $machine->succeed('[ -d "/etc/folder" ]');
-      $machine->succeed('[ -d "/etc/folder/with" ]');
-      $machine->succeed('[ -L "/etc/folder/with/file" ]');
-      $machine->succeed('cat "/etc/plainFile" | grep "Hello World"');
+  testScript = ''
+    machine.succeed('[ -L "/etc/plainFile" ]')
+    assert "Hello World" in machine.succeed('cat "/etc/plainFile"')
+    machine.succeed('[ -d "/etc/folder" ]')
+    machine.succeed('[ -d "/etc/folder/with" ]')
+    machine.succeed('[ -L "/etc/folder/with/file" ]')
+    assert "Hello World" in machine.succeed('cat "/etc/plainFile"')
 
-      $machine->succeed('echo ''${TERMINFO_DIRS} | grep "/run/current-system/sw/share/terminfo"');
-      $machine->succeed('echo ''${NIXCON} | grep "awesome"');
-    '';
+    assert "/run/current-system/sw/share/terminfo" in machine.succeed(
+        "echo ''${TERMINFO_DIRS}"
+    )
+    assert "awesome" in machine.succeed("echo ''${NIXCON}")
+  '';
 })
diff --git a/nixos/tests/gitolite-fcgiwrap.nix b/nixos/tests/gitolite-fcgiwrap.nix
new file mode 100644
index 00000000000..414b7d6fe7e
--- /dev/null
+++ b/nixos/tests/gitolite-fcgiwrap.nix
@@ -0,0 +1,93 @@
+import ./make-test-python.nix (
+  { pkgs, ... }:
+
+    let
+      user = "gitolite-admin";
+      password = "some_password";
+
+      # not used but needed to setup gitolite
+      adminPublicKey = ''
+        ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO7urFhAA90BTpGuEHeWWTY3W/g9PBxXNxfWhfbrm4Le root@client
+      '';
+    in
+      {
+        name = "gitolite-fcgiwrap";
+
+        meta = with pkgs.stdenv.lib.maintainers; {
+          maintainers = [ bbigras ];
+        };
+
+        nodes = {
+
+          server =
+            { ... }:
+              {
+                networking.firewall.allowedTCPPorts = [ 80 ];
+
+                services.fcgiwrap.enable = true;
+                services.gitolite = {
+                  enable = true;
+                  adminPubkey = adminPublicKey;
+                };
+
+                services.nginx = {
+                  enable = true;
+                  recommendedProxySettings = true;
+                  virtualHosts."server".locations."/git".extraConfig = ''
+                    # turn off gzip as git objects are already well compressed
+                    gzip off;
+
+                    # use file based basic authentication
+                    auth_basic "Git Repository Authentication";
+                    auth_basic_user_file /etc/gitolite/htpasswd;
+
+                    # common FastCGI parameters are required
+                    include ${pkgs.nginx}/conf/fastcgi_params;
+
+                    # strip the CGI program prefix
+                    fastcgi_split_path_info ^(/git)(.*)$;
+                    fastcgi_param PATH_INFO $fastcgi_path_info;
+
+                    # pass authenticated user login(mandatory) to Gitolite
+                    fastcgi_param REMOTE_USER $remote_user;
+
+                    # pass git repository root directory and hosting user directory
+                    # these env variables can be set in a wrapper script
+                    fastcgi_param GIT_HTTP_EXPORT_ALL "";
+                    fastcgi_param GIT_PROJECT_ROOT /var/lib/gitolite/repositories;
+                    fastcgi_param GITOLITE_HTTP_HOME /var/lib/gitolite;
+                    fastcgi_param SCRIPT_FILENAME ${pkgs.gitolite}/bin/gitolite-shell;
+
+                    # use Unix domain socket or inet socket
+                    fastcgi_pass unix:/run/fcgiwrap.sock;
+                  '';
+                };
+
+                # WARNING: DON'T DO THIS IN PRODUCTION!
+                # This puts unhashed secrets directly into the Nix store for ease of testing.
+                environment.etc."gitolite/htpasswd".source = pkgs.runCommand "htpasswd" {} ''
+                  ${pkgs.apacheHttpd}/bin/htpasswd -bc "$out" ${user} ${password}
+                '';
+              };
+
+          client =
+            { pkgs, ... }:
+              {
+                environment.systemPackages = [ pkgs.git ];
+              };
+        };
+
+        testScript = ''
+          start_all()
+
+          server.wait_for_unit("gitolite-init.service")
+          server.wait_for_unit("nginx.service")
+          server.wait_for_file("/run/fcgiwrap.sock")
+
+          client.wait_for_unit("multi-user.target")
+          client.succeed(
+              "git clone http://${user}:${password}@server/git/gitolite-admin.git"
+          )
+        '';
+      }
+)
diff --git a/nixos/tests/installed-tests/default.nix b/nixos/tests/installed-tests/default.nix
index f4780bdcfc9..8e997ee4aeb 100644
--- a/nixos/tests/installed-tests/default.nix
+++ b/nixos/tests/installed-tests/default.nix
@@ -29,36 +29,51 @@ let
 
       # Extra flags to pass to gnome-desktop-testing-runner.
     , testRunnerFlags ? ""
-    }:
-    makeTest rec {
-      name = tested.name;
-
-      meta = {
-        maintainers = tested.meta.maintainers;
-      };
-
-      machine = { ... }: {
-        imports = [
-          testConfig
-        ] ++ optional withX11 ../common/x11.nix;
-
-        environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
-
-      };
-
-      testScript =
-        optionalString withX11 ''
-          machine.wait_for_x()
-        '' +
-        optionalString (preTestScript != "") ''
-          ${preTestScript}
-        '' +
-        ''
-          machine.succeed(
-              "gnome-desktop-testing-runner ${testRunnerFlags} -d '${tested.installedTests}/share'"
-          )
-        '';
-    };
+
+      # Extra attributes to pass to makeTest.
+      # They will be recursively merged into the attrset created by this function.
+    , ...
+    }@args:
+    makeTest
+      (recursiveUpdate
+        rec {
+          name = tested.name;
+
+          meta = {
+            maintainers = tested.meta.maintainers;
+          };
+
+          machine = { ... }: {
+            imports = [
+              testConfig
+            ] ++ optional withX11 ../common/x11.nix;
+
+            environment.systemPackages = with pkgs; [ gnome-desktop-testing ];
+
+          };
+
+          testScript =
+            optionalString withX11 ''
+              machine.wait_for_x()
+            '' +
+            optionalString (preTestScript != "") ''
+              ${preTestScript}
+            '' +
+            ''
+              machine.succeed(
+                  "gnome-desktop-testing-runner ${testRunnerFlags} -d '${tested.installedTests}/share'"
+              )
+            '';
+        }
+
+        (removeAttrs args [
+          "tested"
+          "testConfig"
+          "preTestScript"
+          "withX11"
+          "testRunnerFlags"
+        ])
+      );
 
 in
 
@@ -73,6 +88,7 @@ in
   glib-networking = callInstalledTest ./glib-networking.nix {};
   gnome-photos = callInstalledTest ./gnome-photos.nix {};
   graphene = callInstalledTest ./graphene.nix {};
+  ibus = callInstalledTest ./ibus.nix {};
   libgdata = callInstalledTest ./libgdata.nix {};
   libxmlb = callInstalledTest ./libxmlb.nix {};
   ostree = callInstalledTest ./ostree.nix {};
diff --git a/nixos/tests/installed-tests/ibus.nix b/nixos/tests/installed-tests/ibus.nix
new file mode 100644
index 00000000000..af54b612b50
--- /dev/null
+++ b/nixos/tests/installed-tests/ibus.nix
@@ -0,0 +1,20 @@
+{ pkgs, makeInstalledTest, ... }:
+
+makeInstalledTest {
+  tested = pkgs.ibus;
+
+  testConfig = {
+    i18n.inputMethod.enabled = "ibus";
+  };
+
+  preTestScript = ''
+    # ibus has ibus-desktop-testing-runner but it tries to manage desktop session so we just spawn ibus-daemon ourselves
+    machine.succeed("ibus-daemon --daemonize --verbose")
+  '';
+
+  withX11 = true;
+
+  # TODO: ibus-daemon is currently crashing or something
+  # maybe make ibus systemd service that auto-restarts?
+  meta.broken = true;
+}
diff --git a/nixos/tests/ipv6.nix b/nixos/tests/ipv6.nix
index d11eba764da..ba464b57447 100644
--- a/nixos/tests/ipv6.nix
+++ b/nixos/tests/ipv6.nix
@@ -1,7 +1,7 @@
 # Test of IPv6 functionality in NixOS, including whether router
 # solicication/advertisement using radvd works.
 
-import ./make-test.nix ({ pkgs, lib, ...} : {
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
   name = "ipv6";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ eelco ];
@@ -35,51 +35,56 @@ import ./make-test.nix ({ pkgs, lib, ...} : {
 
   testScript =
     ''
+      import re
+
       # Start the router first so that it respond to router solicitations.
-      $router->waitForUnit("radvd");
+      router.wait_for_unit("radvd")
 
-      startAll;
+      start_all()
 
-      $client->waitForUnit("network.target");
-      $server->waitForUnit("network.target");
-      $server->waitForUnit("httpd.service");
+      client.wait_for_unit("network.target")
+      server.wait_for_unit("network.target")
+      server.wait_for_unit("httpd.service")
 
       # Wait until the given interface has a non-tentative address of
       # the desired scope (i.e. has completed Duplicate Address
       # Detection).
-      sub waitForAddress {
-          my ($machine, $iface, $scope) = @_;
-          $machine->waitUntilSucceeds("[ `ip -o -6 addr show dev $iface scope $scope | grep -v tentative | wc -l` -ge 1 ]");
-          my $ip = (split /[ \/]+/, $machine->succeed("ip -o -6 addr show dev $iface scope $scope"))[3];
-          $machine->log("$scope address on $iface is $ip");
-          return $ip;
-      }
-
-      subtest "loopback address", sub {
-          $client->succeed("ping -c 1 ::1 >&2");
-          $client->fail("ping -c 1 ::2 >&2");
-      };
-
-      subtest "local link addressing", sub {
-          my $clientIp = waitForAddress $client, "eth1", "link";
-          my $serverIp = waitForAddress $server, "eth1", "link";
-          $client->succeed("ping -c 1 $clientIp%eth1 >&2");
-          $client->succeed("ping -c 1 $serverIp%eth1 >&2");
-      };
-
-      subtest "global addressing", sub {
-          my $clientIp = waitForAddress $client, "eth1", "global";
-          my $serverIp = waitForAddress $server, "eth1", "global";
-          $client->succeed("ping -c 1 $clientIp >&2");
-          $client->succeed("ping -c 1 $serverIp >&2");
-          $client->succeed("curl --fail -g http://[$serverIp]");
-          $client->fail("curl --fail -g http://[$clientIp]");
-      };
-      subtest "privacy extensions", sub {
-          my $ip = waitForAddress $client, "eth1", "global temporary";
+      def wait_for_address(machine, iface, scope, temporary=False):
+          temporary_flag = "temporary" if temporary else "-temporary"
+          cmd = f"ip -o -6 addr show dev {iface} scope {scope} -tentative {temporary_flag}"
+
+          machine.wait_until_succeeds(f"[ `{cmd} | wc -l` -eq 1 ]")
+          output = machine.succeed(cmd)
+          ip = re.search(r"inet6 ([0-9a-f:]{2,})/", output).group(1)
+
+          if temporary:
+              scope = scope + " temporary"
+          machine.log(f"{scope} address on {iface} is {ip}")
+          return ip
+
+
+      with subtest("Loopback address can be pinged"):
+          client.succeed("ping -c 1 ::1 >&2")
+          client.fail("ping -c 1 ::2 >&2")
+
+      with subtest("Local link addresses can be obtained and pinged"):
+          client_ip = wait_for_address(client, "eth1", "link")
+          server_ip = wait_for_address(server, "eth1", "link")
+          client.succeed(f"ping -c 1 {client_ip}%eth1 >&2")
+          client.succeed(f"ping -c 1 {server_ip}%eth1 >&2")
+
+      with subtest("Global addresses can be obtained, pinged, and reached via http"):
+          client_ip = wait_for_address(client, "eth1", "global")
+          server_ip = wait_for_address(server, "eth1", "global")
+          client.succeed(f"ping -c 1 {client_ip} >&2")
+          client.succeed(f"ping -c 1 {server_ip} >&2")
+          client.succeed(f"curl --fail -g http://[{server_ip}]")
+          client.fail(f"curl --fail -g http://[{client_ip}]")
+
+      with subtest("Privacy extensions: Global temporary address can be obtained and pinged"):
+          ip = wait_for_address(client, "eth1", "global", temporary=True)
           # Default route should have "src <temporary address>" in it
-          $client->succeed("ip r g ::2 | grep $ip");
-      };
+          client.succeed(f"ip r g ::2 | grep {ip}")
 
       # TODO: test reachability of a machine on another network.
     '';
diff --git a/nixos/tests/munin.nix b/nixos/tests/munin.nix
index 31374aaf77e..7b674db7768 100644
--- a/nixos/tests/munin.nix
+++ b/nixos/tests/munin.nix
@@ -1,7 +1,7 @@
 # This test runs basic munin setup with node and cron job running on the same
 # machine.
 
-import ./make-test.nix ({ pkgs, ...} : {
+import ./make-test-python.nix ({ pkgs, ...} : {
   name = "munin";
   meta = with pkgs.stdenv.lib.maintainers; {
     maintainers = [ domenkozar eelco ];
@@ -12,33 +12,33 @@ import ./make-test.nix ({ pkgs, ...} : {
       { config, ... }:
         {
           services = {
-           munin-node = {
+            munin-node = {
+              enable = true;
+              # disable a failing plugin to prevent irrelevant error message, see #23049
+              disabledPlugins = [ "apc_nis" ];
+            };
+            munin-cron = {
              enable = true;
-             # disable a failing plugin to prevent irrelevant error message, see #23049
-             disabledPlugins = [ "apc_nis" ];
-           };
-           munin-cron = {
-            enable = true;
-            hosts = ''
-              [${config.networking.hostName}]
-              address localhost
-            '';
-           };
+             hosts = ''
+               [${config.networking.hostName}]
+               address localhost
+             '';
+            };
           };
-          # long timeout to prevent hydra failure on high load
-          systemd.services.munin-node.serviceConfig.TimeoutStartSec = "10min";
+
+          # increase the systemd timer interval so it fires more often
+          systemd.timers.munin-cron.timerConfig.OnCalendar = pkgs.stdenv.lib.mkForce "*:*:0/10";
         };
     };
 
   testScript = ''
-    startAll;
+    start_all()
 
-    $one->waitForUnit("munin-node.service");
-    # make sure the node is actually listening
-    $one->waitForOpenPort(4949);
-    $one->succeed('systemctl start munin-cron');
-    # wait for munin-cron output
-    $one->waitForFile("/var/lib/munin/one/one-uptime-uptime-g.rrd");
-    $one->waitForFile("/var/www/munin/one/index.html");
+    with subtest("ensure munin-node starts and listens on 4949"):
+        one.wait_for_unit("munin-node.service")
+        one.wait_for_open_port(4949)
+    with subtest("ensure munin-cron output is correct"):
+        one.wait_for_file("/var/lib/munin/one/one-uptime-uptime-g.rrd")
+        one.wait_for_file("/var/www/munin/one/index.html")
   '';
 })
diff --git a/nixos/tests/pam-u2f.nix b/nixos/tests/pam-u2f.nix
index 1052a2f3b91..f492baa9e13 100644
--- a/nixos/tests/pam-u2f.nix
+++ b/nixos/tests/pam-u2f.nix
@@ -1,4 +1,4 @@
-import ./make-test.nix ({ ... }:
+import ./make-test-python.nix ({ ... }:
 
 {
   name = "pam-u2f";
@@ -17,7 +17,9 @@ import ./make-test.nix ({ ... }:
 
   testScript =
     ''
-      $machine->waitForUnit('multi-user.target');
-      $machine->succeed('egrep "auth required .*/lib/security/pam_u2f.so.*debug.*interactive.*cue" /etc/pam.d/ -R');
+      machine.wait_for_unit("multi-user.target")
+      machine.succeed(
+          'egrep "auth required .*/lib/security/pam_u2f.so.*debug.*interactive.*cue" /etc/pam.d/ -R'
+      )
     '';
 })
diff --git a/nixos/tests/resolv.nix b/nixos/tests/resolv.nix
new file mode 100644
index 00000000000..b506f87451e
--- /dev/null
+++ b/nixos/tests/resolv.nix
@@ -0,0 +1,46 @@
+# Test whether DNS resolving returns multiple records and all address families.
+import ./make-test-python.nix ({ pkgs, ... } : {
+  name = "resolv";
+  meta = with pkgs.stdenv.lib.maintainers; {
+    maintainers = [ ckauhaus ];
+  };
+
+  nodes.resolv = { ... }: {
+    networking.extraHosts = ''
+      # IPv4 only
+      192.0.2.1 host-ipv4.example.net
+      192.0.2.2 host-ipv4.example.net
+      # IP6 only
+      2001:db8::2:1 host-ipv6.example.net
+      2001:db8::2:2 host-ipv6.example.net
+      # dual stack
+      192.0.2.1 host-dual.example.net
+      192.0.2.2 host-dual.example.net
+      2001:db8::2:1 host-dual.example.net
+      2001:db8::2:2 host-dual.example.net
+    '';
+  };
+
+  testScript = ''
+    def addrs_in(hostname, addrs):
+        res = resolv.succeed("getent ahosts {}".format(hostname))
+        for addr in addrs:
+            assert addr in res, "Expected output '{}' not found in\n{}".format(addr, res)
+
+
+    start_all()
+    resolv.wait_for_unit("nscd")
+
+    ipv4 = ["192.0.2.1", "192.0.2.2"]
+    ipv6 = ["2001:db8::2:1", "2001:db8::2:2"]
+
+    with subtest("IPv4 resolves"):
+        addrs_in("host-ipv4.example.net", ipv4)
+
+    with subtest("IPv6 resolves"):
+        addrs_in("host-ipv6.example.net", ipv6)
+
+    with subtest("Dual stack resolves"):
+        addrs_in("host-dual.example.net", ipv4 + ipv6)
+  '';
+})
diff --git a/nixos/tests/timezone.nix b/nixos/tests/timezone.nix
index 2204649a3fc..7fc9a5058ee 100644
--- a/nixos/tests/timezone.nix
+++ b/nixos/tests/timezone.nix
@@ -1,45 +1,50 @@
-{
-  timezone-static = import ./make-test.nix ({ pkgs, ... }: {
-    name = "timezone-static";
-    meta.maintainers = with pkgs.lib.maintainers; [ lheckemann ];
-
-    machine.time.timeZone = "Europe/Amsterdam";
-
-    testScript = ''
-      $machine->waitForUnit("dbus.socket");
-      $machine->fail("timedatectl set-timezone Asia/Tokyo");
-      my @dateResult = $machine->execute('date -d @0 "+%Y-%m-%d %H:%M:%S"');
-      $dateResult[1] eq "1970-01-01 01:00:00\n" or die "Timezone seems to be wrong";
-    '';
-  });
-
-  timezone-imperative = import ./make-test.nix ({ pkgs, ... }: {
-    name = "timezone-imperative";
-    meta.maintainers = with pkgs.lib.maintainers; [ lheckemann ];
-
-    machine.time.timeZone = null;
-
-    testScript = ''
-      $machine->waitForUnit("dbus.socket");
-
-      # Should default to UTC
-      my @dateResult = $machine->execute('date -d @0 "+%Y-%m-%d %H:%M:%S"');
-      print $dateResult[1];
-      $dateResult[1] eq "1970-01-01 00:00:00\n" or die "Timezone seems to be wrong";
-
-      $machine->succeed("timedatectl set-timezone Asia/Tokyo");
-
-      # Adjustment should be taken into account
-      my @dateResult = $machine->execute('date -d @0 "+%Y-%m-%d %H:%M:%S"');
-      print $dateResult[1];
-      $dateResult[1] eq "1970-01-01 09:00:00\n" or die "Timezone was not adjusted";
-
-      # Adjustment should persist across a reboot
-      $machine->shutdown;
-      $machine->waitForUnit("dbus.socket");
-      my @dateResult = $machine->execute('date -d @0 "+%Y-%m-%d %H:%M:%S"');
-      print $dateResult[1];
-      $dateResult[1] eq "1970-01-01 09:00:00\n" or die "Timezone adjustment was not persisted";
-    '';
-  });
-}
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "timezone";
+  meta.maintainers = with pkgs.lib.maintainers; [ lheckemann ];
+
+  nodes = {
+    node_eutz = { pkgs, ... }: {
+      time.timeZone = "Europe/Amsterdam";
+    };
+
+    node_nulltz = { pkgs, ... }: {
+      time.timeZone = null;
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+      node_eutz.wait_for_unit("dbus.socket")
+
+      with subtest("static - Ensure timezone change gives the correct result"):
+          node_eutz.fail("timedatectl set-timezone Asia/Tokyo")
+          date_result = node_eutz.succeed('date -d @0 "+%Y-%m-%d %H:%M:%S"')
+          assert date_result == "1970-01-01 01:00:00\n", "Timezone seems to be wrong"
+
+      node_nulltz.wait_for_unit("dbus.socket")
+
+      with subtest("imperative - Ensure timezone defaults to UTC"):
+          date_result = node_nulltz.succeed('date -d @0 "+%Y-%m-%d %H:%M:%S"')
+          print(date_result)
+          assert (
+              date_result == "1970-01-01 00:00:00\n"
+          ), "Timezone seems to be wrong (not UTC)"
+
+      with subtest("imperative - Ensure timezone adjustment produces expected result"):
+          node_nulltz.succeed("timedatectl set-timezone Asia/Tokyo")
+
+          # Adjustment should be taken into account
+          date_result = node_nulltz.succeed('date -d @0 "+%Y-%m-%d %H:%M:%S"')
+          print(date_result)
+          assert date_result == "1970-01-01 09:00:00\n", "Timezone was not adjusted"
+
+      with subtest("imperative - Ensure timezone adjustment persists across reboot"):
+          # Adjustment should persist across a reboot
+          node_nulltz.shutdown()
+          node_nulltz.wait_for_unit("dbus.socket")
+          date_result = node_nulltz.succeed('date -d @0 "+%Y-%m-%d %H:%M:%S"')
+          print(date_result)
+          assert (
+              date_result == "1970-01-01 09:00:00\n"
+          ), "Timezone adjustment was not persisted"
+  '';
+})