summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/README3
-rw-r--r--nixos/doc/manual/README.md3
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2111.section.xml33
-rw-r--r--nixos/doc/manual/release-notes/rl-2111.section.md10
-rw-r--r--nixos/lib/test-driver/test-driver.py7
-rw-r--r--nixos/modules/module-list.nix2
-rw-r--r--nixos/modules/programs/xwayland.nix22
-rw-r--r--nixos/modules/services/cluster/k3s/default.nix23
-rw-r--r--nixos/modules/services/networking/bind.nix11
-rw-r--r--nixos/modules/services/networking/ddclient.nix11
-rw-r--r--nixos/modules/services/security/vaultwarden/default.nix18
-rw-r--r--nixos/modules/services/video/unifi-video.nix265
-rw-r--r--nixos/modules/services/web-apps/discourse.nix19
-rw-r--r--nixos/modules/services/web-apps/discourse.xml43
-rw-r--r--nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix18
-rw-r--r--nixos/modules/services/web-apps/wordpress.nix126
-rw-r--r--nixos/modules/tasks/snapraid.nix230
-rw-r--r--nixos/modules/virtualisation/vmware-guest.nix2
-rw-r--r--nixos/tests/agda.nix5
-rw-r--r--nixos/tests/all-tests.nix2
-rw-r--r--nixos/tests/boot.nix36
-rw-r--r--nixos/tests/wordpress.nix66
22 files changed, 853 insertions, 102 deletions
diff --git a/nixos/doc/manual/README b/nixos/doc/manual/README
deleted file mode 100644
index 120c127d7af..00000000000
--- a/nixos/doc/manual/README
+++ /dev/null
@@ -1,3 +0,0 @@
-Moved to: ./contributing-to-this-manual.xml. Link:
-
-https://nixos.org/manual/nixos/unstable/#chap-contributing
diff --git a/nixos/doc/manual/README.md b/nixos/doc/manual/README.md
new file mode 100644
index 00000000000..bc649761df6
--- /dev/null
+++ b/nixos/doc/manual/README.md
@@ -0,0 +1,3 @@
+[Moved to ./contributing-to-this-manual.chapter.md](./contributing-to-this-manual.chapter.md). Link:
+
+https://nixos.org/manual/nixos/unstable/#chap-contributing
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
index 91acaf728e2..7ebf6c0187a 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
@@ -85,6 +85,13 @@
           <link linkend="opt-services.vikunja.enable">services.vikunja</link>.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://www.snapraid.it/">snapraid</link>, a
+          backup program for disk arrays. Available as
+          <link linkend="opt-snapraid.enable">snapraid</link>.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-21.11-incompatibilities">
@@ -486,7 +493,7 @@
         </itemizedlist>
       </listitem>
     </itemizedlist>
-    <itemizedlist spacing="compact">
+    <itemizedlist>
       <listitem>
         <para>
           <literal>yggdrasil</literal> was upgraded to a new major
@@ -495,6 +502,14 @@
           changelog</link>.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          <literal>icingaweb2</literal> was upgraded to a new release
+          which requires a manual database upgrade, see
+          <link xlink:href="https://github.com/Icinga/icingaweb2/releases/tag/v2.9.0">upstream
+          changelog</link>.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-21.11-notable-changes">
@@ -538,6 +553,22 @@
           <literal>claws-mail-gtk2</literal> package.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          The wordpress module provides a new interface which allows to
+          use different webservers with the new option
+          <link xlink:href="options.html#opt-services.wordpress.webserver"><literal>services.wordpress.webserver</literal></link>.
+          Currently <literal>httpd</literal> and
+          <literal>nginx</literal> are supported. The definitions of
+          wordpress sites should now be set in
+          <link xlink:href="options.html#opt-services.wordpress.sites"><literal>services.wordpress.sites</literal></link>.
+        </para>
+        <para>
+          Sites definitions that use the old interface are automatically
+          migrated in the new option. This backward compatibility will
+          be removed in 22.05.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
 </section>
diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md
index 1cef74510db..a0ca0ca3d0e 100644
--- a/nixos/doc/manual/release-notes/rl-2111.section.md
+++ b/nixos/doc/manual/release-notes/rl-2111.section.md
@@ -25,6 +25,10 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [vikunja](https://vikunja.io), a to-do list app. Available as [services.vikunja](#opt-services.vikunja.enable).
 
+- [snapraid](https://www.snapraid.it/), a backup program for disk arrays.
+  Available as [snapraid](#opt-snapraid.enable).
+
+
 ## Backward Incompatibilities {#sec-release-21.11-incompatibilities}
 
 - The `staticjinja` package has been upgraded from 1.0.4 to 3.0.1
@@ -122,6 +126,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - `yggdrasil` was upgraded to a new major release with breaking changes, see [upstream changelog](https://github.com/yggdrasil-network/yggdrasil-go/releases/tag/v0.4.0).
 
+- `icingaweb2` was upgraded to a new release which requires a manual database upgrade, see [upstream changelog](https://github.com/Icinga/icingaweb2/releases/tag/v2.9.0).
+
 ## Other Notable Changes {#sec-release-21.11-notable-changes}
 
 - The setting [`services.openssh.logLevel`](options.html#opt-services.openssh.logLevel) `"VERBOSE"` `"INFO"`. This brings NixOS in line with upstream and other Linux distributions, and reduces log spam on servers due to bruteforcing botnets.
@@ -131,3 +137,7 @@ In addition to numerous new and upgraded packages, this release has the followin
 - Sway: The terminal emulator `rxvt-unicode` is no longer installed by default via `programs.sway.extraPackages`. The current default configuration uses `alacritty` (and soon `foot`) so this is only an issue when using a customized configuration and not installing `rxvt-unicode` explicitly.
 
 - The `claws-mail` package now references the new GTK+ 3 release branch, major version 4. To use the GTK+ 2 releases, one can install the `claws-mail-gtk2` package.
+
+- The wordpress module provides a new interface which allows to use different webservers with the new option [`services.wordpress.webserver`](options.html#opt-services.wordpress.webserver).  Currently `httpd` and `nginx` are supported. The definitions of wordpress sites should now be set in [`services.wordpress.sites`](options.html#opt-services.wordpress.sites).
+
+  Sites definitions that use the old interface are automatically migrated in the new option. This backward compatibility will be removed in 22.05.
diff --git a/nixos/lib/test-driver/test-driver.py b/nixos/lib/test-driver/test-driver.py
index 7f4dd5963c9..15eaba88476 100644
--- a/nixos/lib/test-driver/test-driver.py
+++ b/nixos/lib/test-driver/test-driver.py
@@ -292,7 +292,12 @@ class Machine:
             net_frontend += "," + args["netFrontendArgs"]
 
         start_command = (
-            "qemu-kvm -m 384 " + net_backend + " " + net_frontend + " $QEMU_OPTS "
+            args.get("qemuBinary", "qemu-kvm")
+            + " -m 384 "
+            + net_backend
+            + " "
+            + net_frontend
+            + " $QEMU_OPTS "
         )
 
         if "hda" in args:
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 28f36ca82b9..ad1bccd5428 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -849,6 +849,7 @@
   ./services/networking/ucarp.nix
   ./services/networking/unbound.nix
   ./services/networking/unifi.nix
+  ./services/video/unifi-video.nix
   ./services/networking/v2ray.nix
   ./services/networking/vsftpd.nix
   ./services/networking/wakeonlan.nix
@@ -1104,6 +1105,7 @@
   ./tasks/network-interfaces-systemd.nix
   ./tasks/network-interfaces-scripted.nix
   ./tasks/scsi-link-power-management.nix
+  ./tasks/snapraid.nix
   ./tasks/swraid.nix
   ./tasks/trackpoint.nix
   ./tasks/powertop.nix
diff --git a/nixos/modules/programs/xwayland.nix b/nixos/modules/programs/xwayland.nix
index 7e9a424a715..cb3c9c5b156 100644
--- a/nixos/modules/programs/xwayland.nix
+++ b/nixos/modules/programs/xwayland.nix
@@ -10,14 +10,16 @@ in
 {
   options.programs.xwayland = {
 
-    enable = mkEnableOption ''
-      Xwayland X server allows running X programs on a Wayland compositor.
-    '';
+    enable = mkEnableOption "Xwayland (an X server for interfacing X11 apps with the Wayland protocol)";
 
     defaultFontPath = mkOption {
       type = types.str;
       default = optionalString config.fonts.fontDir.enable
         "/run/current-system/sw/share/X11/fonts";
+      defaultText = literalExample ''
+        optionalString config.fonts.fontDir.enable
+          "/run/current-system/sw/share/X11/fonts";
+      '';
       description = ''
         Default font path. Setting this option causes Xwayland to be rebuilt.
       '';
@@ -25,7 +27,15 @@ in
 
     package = mkOption {
       type = types.path;
-      description = "The Xwayland package";
+      default = pkgs.xwayland.override (oldArgs: {
+        inherit (cfg) defaultFontPath;
+      });
+      defaultText = literalExample ''
+        pkgs.xwayland.override (oldArgs: {
+          inherit (config.programs.xwayland) defaultFontPath;
+        });
+      '';
+      description = "The Xwayland package to use.";
     };
 
   };
@@ -37,9 +47,5 @@ in
 
     environment.systemPackages = [ cfg.package ];
 
-    programs.xwayland.package = pkgs.xwayland.override (oldArgs: {
-      inherit (cfg) defaultFontPath;
-    });
-
   };
 }
diff --git a/nixos/modules/services/cluster/k3s/default.nix b/nixos/modules/services/cluster/k3s/default.nix
index 300c182406c..d0fb8cc5098 100644
--- a/nixos/modules/services/cluster/k3s/default.nix
+++ b/nixos/modules/services/cluster/k3s/default.nix
@@ -35,10 +35,20 @@ in
 
     token = mkOption {
       type = types.str;
-      description = "The k3s token to use when connecting to the server. This option only makes sense for an agent.";
+      description = ''
+        The k3s token to use when connecting to the server. This option only makes sense for an agent.
+        WARNING: This option will expose store your token unencrypted world-readable in the nix store.
+        If this is undesired use the tokenFile option instead.
+      '';
       default = "";
     };
 
+    tokenFile = mkOption {
+      type = types.nullOr types.path;
+      description = "File path containing k3s token to use when connecting to the server. This option only makes sense for an agent.";
+      default = null;
+    };
+
     docker = mkOption {
       type = types.bool;
       default = false;
@@ -68,8 +78,8 @@ in
         message = "serverAddr should be set if role is 'agent'";
       }
       {
-        assertion = cfg.role == "agent" -> cfg.token != "";
-        message = "token should be set if role is 'agent'";
+        assertion = cfg.role == "agent" -> cfg.token != "" || cfg.tokenFile != null;
+        message = "token or tokenFile should be set if role is 'agent'";
       }
     ];
 
@@ -105,7 +115,12 @@ in
             "${cfg.package}/bin/k3s ${cfg.role}"
           ] ++ (optional cfg.docker "--docker")
           ++ (optional cfg.disableAgent "--disable-agent")
-          ++ (optional (cfg.role == "agent") "--server ${cfg.serverAddr} --token ${cfg.token}")
+          ++ (optional (cfg.role == "agent") "--server ${cfg.serverAddr} ${
+            if cfg.tokenFile != null then
+              "--token-file ${cfg.tokenFile}"
+            else
+              "--token ${cfg.token}"
+          }")
           ++ [ cfg.extraFlags ]
         );
       };
diff --git a/nixos/modules/services/networking/bind.nix b/nixos/modules/services/networking/bind.nix
index 33da4071638..480d5a184f2 100644
--- a/nixos/modules/services/networking/bind.nix
+++ b/nixos/modules/services/networking/bind.nix
@@ -61,7 +61,7 @@ let
         blackhole { badnetworks; };
         forward first;
         forwarders { ${concatMapStrings (entry: " ${entry}; ") cfg.forwarders} };
-        directory "/run/named";
+        directory "${cfg.directory}";
         pid-file "/run/named/named.pid";
         ${cfg.extraOptions}
       };
@@ -166,6 +166,12 @@ in
         ";
       };
 
+      directory = mkOption {
+        type = types.str;
+        default = "/run/named";
+        description = "Working directory of BIND.";
+      };
+
       zones = mkOption {
         default = [ ];
         type = with types; coercedTo (listOf attrs) bindZoneCoerce (attrsOf (types.submodule bindZoneOptions));
@@ -240,6 +246,9 @@ in
 
         ${pkgs.coreutils}/bin/mkdir -p /run/named
         chown ${bindUser} /run/named
+
+        ${pkgs.coreutils}/bin/mkdir -p ${cfg.directory}
+        chown ${bindUser} ${cfg.directory}
       '';
 
       serviceConfig = {
diff --git a/nixos/modules/services/networking/ddclient.nix b/nixos/modules/services/networking/ddclient.nix
index 053efe71270..7820eedd932 100644
--- a/nixos/modules/services/networking/ddclient.nix
+++ b/nixos/modules/services/networking/ddclient.nix
@@ -18,6 +18,7 @@ let
     ${lib.optionalString (cfg.zone != "")   "zone=${cfg.zone}"}
     ssl=${boolToStr cfg.ssl}
     wildcard=YES
+    ipv6=${boolToStr cfg.ipv6}
     quiet=${boolToStr cfg.quiet}
     verbose=${boolToStr cfg.verbose}
     ${cfg.extraConfig}
@@ -116,7 +117,15 @@ with lib;
         default = true;
         type = bool;
         description = ''
-          Whether to use to use SSL/TLS to connect to dynamic DNS provider.
+          Whether to use SSL/TLS to connect to dynamic DNS provider.
+        '';
+      };
+
+      ipv6 = mkOption {
+        default = false;
+        type = bool;
+        description = ''
+          Whether to use IPv6.
         '';
       };
 
diff --git a/nixos/modules/services/security/vaultwarden/default.nix b/nixos/modules/services/security/vaultwarden/default.nix
index 940ac7832da..d28ea61e66a 100644
--- a/nixos/modules/services/security/vaultwarden/default.nix
+++ b/nixos/modules/services/security/vaultwarden/default.nix
@@ -26,12 +26,12 @@ let
         if value != null then [ (nameValuePair (nameToEnvVar name) (if isBool value then boolToString value else toString value)) ] else []
       ) cfg.config));
     in { DATA_FOLDER = "/var/lib/bitwarden_rs"; } // optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
-      WEB_VAULT_FOLDER = "${pkgs.vaultwarden-vault}/share/vaultwarden/vault";
+      WEB_VAULT_FOLDER = "${cfg.webVaultPackage}/share/vaultwarden/vault";
     } // configEnv;
 
   configFile = pkgs.writeText "vaultwarden.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
 
-  vaultwarden = pkgs.vaultwarden.override { inherit (cfg) dbBackend; };
+  vaultwarden = cfg.package.override { inherit (cfg) dbBackend; };
 
 in {
   imports = [
@@ -102,6 +102,20 @@ in {
         <literal>vaultwarden</literal> is running.
       '';
     };
+
+    package = mkOption {
+      type = package;
+      default = pkgs.vaultwarden;
+      defaultText = "pkgs.vaultwarden";
+      description = "Vaultwarden package to use.";
+    };
+
+    webVaultPackage = mkOption {
+      type = package;
+      default = pkgs.vaultwarden-vault;
+      defaultText = "pkgs.vaultwarden-vault";
+      description = "Web vault package to use.";
+    };
   };
 
   config = mkIf cfg.enable {
diff --git a/nixos/modules/services/video/unifi-video.nix b/nixos/modules/services/video/unifi-video.nix
new file mode 100644
index 00000000000..d4c0268ed66
--- /dev/null
+++ b/nixos/modules/services/video/unifi-video.nix
@@ -0,0 +1,265 @@
+{ config, lib, pkgs, utils, ... }:
+with lib;
+let
+  cfg = config.services.unifi-video;
+  mainClass = "com.ubnt.airvision.Main";
+  cmd = ''
+    ${pkgs.jsvc}/bin/jsvc \
+    -cwd ${stateDir} \
+    -debug \
+    -verbose:class \
+    -nodetach \
+    -user unifi-video \
+    -home ${cfg.jrePackage}/lib/openjdk \
+    -cp ${pkgs.commonsDaemon}/share/java/commons-daemon-1.2.4.jar:${stateDir}/lib/airvision.jar \
+    -pidfile ${cfg.pidFile} \
+    -procname unifi-video \
+    -Djava.security.egd=file:/dev/./urandom \
+    -Xmx${cfg.maximumJavaHeapSize}M \
+    -Xss512K \
+    -XX:+UseG1GC \
+    -XX:+UseStringDeduplication \
+    -XX:MaxMetaspaceSize=768M \
+    -Djava.library.path=${stateDir}/lib \
+    -Djava.awt.headless=true \
+    -Djavax.net.ssl.trustStore=${stateDir}/etc/ufv-truststore \
+    -Dfile.encoding=UTF-8 \
+    -Dav.tempdir=/var/cache/unifi-video
+  '';
+
+  mongoConf = pkgs.writeTextFile {
+    name = "mongo.conf";
+    executable = false;
+    text = ''
+      # for documentation of all options, see http://docs.mongodb.org/manual/reference/configuration-options/
+
+      storage:
+         dbPath: ${cfg.dataDir}/db
+         journal:
+            enabled: true
+         syncPeriodSecs: 60
+
+      systemLog:
+         destination: file
+         logAppend: true
+         path: ${stateDir}/logs/mongod.log
+
+      net:
+         port: 7441
+         bindIp: 127.0.0.1
+         http:
+            enabled: false
+
+      operationProfiling:
+         slowOpThresholdMs: 500
+         mode: off
+    '';
+  };
+
+
+  mongoWtConf = pkgs.writeTextFile {
+    name = "mongowt.conf";
+    executable = false;
+    text = ''
+      # for documentation of all options, see:
+      #   http://docs.mongodb.org/manual/reference/configuration-options/
+
+      storage:
+         dbPath: ${cfg.dataDir}/db-wt
+         journal:
+            enabled: true
+         wiredTiger:
+            engineConfig:
+               cacheSizeGB: 1
+
+      systemLog:
+         destination: file
+         logAppend: true
+         path: logs/mongod.log
+
+      net:
+         port: 7441
+         bindIp: 127.0.0.1
+
+      operationProfiling:
+         slowOpThresholdMs: 500
+         mode: off
+    '';
+  };
+
+  stateDir = "/var/lib/unifi-video";
+
+in
+  {
+
+    options.services.unifi-video = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether or not to enable the unifi-video service.
+        '';
+      };
+
+      jrePackage = mkOption {
+        type = types.package;
+        default = pkgs.jre8;
+        defaultText = "pkgs.jre8";
+        description = ''
+          The JRE package to use. Check the release notes to ensure it is supported.
+        '';
+      };
+
+      unifiVideoPackage = mkOption {
+        type = types.package;
+        default = pkgs.unifi-video;
+        defaultText = "pkgs.unifi-video";
+        description = ''
+          The unifi-video package to use.
+        '';
+      };
+
+      mongodbPackage = mkOption {
+        type = types.package;
+        default = pkgs.mongodb-4_0;
+        defaultText = "pkgs.mongodb";
+        description = ''
+          The mongodb package to use.
+        '';
+      };
+
+      logDir = mkOption {
+        type = types.str;
+        default = "${stateDir}/logs";
+        description = ''
+          Where to store the logs.
+        '';
+      };
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "${stateDir}/data";
+        description = ''
+          Where to store the database and other data.
+        '';
+      };
+
+      openPorts = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether or not to open the required ports on the firewall.
+        '';
+      };
+
+      maximumJavaHeapSize = mkOption {
+        type = types.nullOr types.int;
+        default = 1024;
+        example = 4096;
+        description = ''
+          Set the maximimum heap size for the JVM in MB.
+        '';
+      };
+
+      pidFile = mkOption {
+        type = types.path;
+        default = "${cfg.dataDir}/unifi-video.pid";
+        description = "Location of unifi-video pid file.";
+      };
+
+};
+
+config = mkIf cfg.enable {
+  users = {
+    users.unifi-video = {
+      description = "UniFi Video controller daemon user";
+      home = stateDir;
+      group = "unifi-video";
+      isSystemUser = true;
+    };
+    groups.unifi-video = {};
+  };
+
+  networking.firewall = mkIf cfg.openPorts {
+      # https://help.ui.com/hc/en-us/articles/217875218-UniFi-Video-Ports-Used
+      allowedTCPPorts = [
+        7080 # HTTP portal
+        7443 # HTTPS portal
+        7445 # Video over HTTP (mobile app)
+        7446 # Video over HTTPS (mobile app)
+        7447 # RTSP via the controller
+        7442 # Camera management from cameras to NVR over WAN
+      ];
+      allowedUDPPorts = [
+        6666 # Inbound camera streams sent over WAN
+      ];
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${stateDir}' 0700 unifi-video unifi-video - -"
+      "d '/var/cache/unifi-video' 0700 unifi-video unifi-video - -"
+
+      "d '${stateDir}/logs' 0700 unifi-video unifi-video - -"
+      "C '${stateDir}/etc' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/etc"
+      "C '${stateDir}/webapps' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/webapps"
+      "C '${stateDir}/email' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/email"
+      "C '${stateDir}/fw' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/fw"
+      "C '${stateDir}/lib' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/lib"
+
+      "d '${stateDir}/data' 0700 unifi-video unifi-video - -"
+      "d '${stateDir}/data/db' 0700 unifi-video unifi-video - -"
+      "C '${stateDir}/data/system.properties' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/etc/system.properties"
+
+      "d '${stateDir}/bin' 0700 unifi-video unifi-video - -"
+      "f '${stateDir}/bin/evostreamms' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/evostreamms"
+      "f '${stateDir}/bin/libavcodec.so.54' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/libavcodec.so.54"
+      "f '${stateDir}/bin/libavformat.so.54' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/libavformat.so.54"
+      "f '${stateDir}/bin/libavutil.so.52' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/libavutil.so.52"
+      "f '${stateDir}/bin/ubnt.avtool' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/ubnt.avtool"
+      "f '${stateDir}/bin/ubnt.updater' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/bin/ubnt.updater"
+      "C '${stateDir}/bin/mongo' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongo"
+      "C '${stateDir}/bin/mongod' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongod"
+      "C '${stateDir}/bin/mongoperf' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongoperf"
+      "C '${stateDir}/bin/mongos' 0700 unifi-video unifi-video - ${cfg.mongodbPackage}/bin/mongos"
+
+      "d '${stateDir}/conf' 0700 unifi-video unifi-video - -"
+      "C '${stateDir}/conf/evostream' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/evostream"
+      "Z '${stateDir}/conf/evostream' 0700 unifi-video unifi-video - -"
+      "L+ '${stateDir}/conf/mongodv3.0+.conf' 0700 unifi-video unifi-video - ${mongoConf}"
+      "L+ '${stateDir}/conf/mongodv3.6+.conf' 0700 unifi-video unifi-video - ${mongoConf}"
+      "L+ '${stateDir}/conf/mongod-wt.conf' 0700 unifi-video unifi-video - ${mongoWtConf}"
+      "L+ '${stateDir}/conf/catalina.policy' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/catalina.policy"
+      "L+ '${stateDir}/conf/catalina.properties' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/catalina.properties"
+      "L+ '${stateDir}/conf/context.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/context.xml"
+      "L+ '${stateDir}/conf/logging.properties' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/logging.properties"
+      "L+ '${stateDir}/conf/server.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/server.xml"
+      "L+ '${stateDir}/conf/tomcat-users.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/tomcat-users.xml"
+      "L+ '${stateDir}/conf/web.xml' 0700 unifi-video unifi-video - ${pkgs.unifi-video}/lib/unifi-video/conf/web.xml"
+
+    ];
+
+    systemd.services.unifi-video = {
+      description = "UniFi Video NVR daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ] ;
+      unitConfig.RequiresMountsFor = stateDir;
+      # Make sure package upgrades trigger a service restart
+      restartTriggers = [ cfg.unifiVideoPackage cfg.mongodbPackage ];
+      path = with pkgs; [ gawk coreutils busybox which jre8 lsb-release libcap util-linux ];
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${(removeSuffix "\n" cmd)} ${mainClass} start";
+        ExecStop = "${(removeSuffix "\n" cmd)} stop ${mainClass} stop";
+        Restart = "on-failure";
+        UMask = "0077";
+        User = "unifi-video";
+        WorkingDirectory = "${stateDir}";
+      };
+    };
+
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ rsynnest ];
+  };
+}
diff --git a/nixos/modules/services/web-apps/discourse.nix b/nixos/modules/services/web-apps/discourse.nix
index d3ae072f86a..8d5302ba267 100644
--- a/nixos/modules/services/web-apps/discourse.nix
+++ b/nixos/modules/services/web-apps/discourse.nix
@@ -475,21 +475,16 @@ in
       plugins = lib.mkOption {
         type = lib.types.listOf lib.types.package;
         default = [];
-        example = ''
-          [
-            (pkgs.fetchFromGitHub {
-              owner = "discourse";
-              repo = "discourse-spoiler-alert";
-              rev = "e200cfa571d252cab63f3d30d619b370986e4cee";
-              sha256 = "0ya69ix5g77wz4c9x9gmng6l25ghb5xxlx3icr6jam16q14dzc33";
-            })
+        example = lib.literalExample ''
+          with config.services.discourse.package.plugins; [
+            discourse-canned-replies
+            discourse-github
           ];
         '';
         description = ''
-          <productname>Discourse</productname> plugins to install as a
-          list of derivations. As long as a plugin supports the
-          standard install method, packaging it should only require
-          fetching its source with an appropriate fetcher.
+          Plugins to install as part of
+          <productname>Discourse</productname>, expressed as a list of
+          derivations.
         '';
       };
 
diff --git a/nixos/modules/services/web-apps/discourse.xml b/nixos/modules/services/web-apps/discourse.xml
index bae56242321..1d6866e7b35 100644
--- a/nixos/modules/services/web-apps/discourse.xml
+++ b/nixos/modules/services/web-apps/discourse.xml
@@ -262,9 +262,31 @@ services.discourse = {
     <para>
       You can install <productname>Discourse</productname> plugins
       using the <xref linkend="opt-services.discourse.plugins" />
-      option. As long as a plugin supports the standard install
-      method, packaging it should only require fetching its source
-      with an appropriate fetcher.
+      option. Pre-packaged plugins are provided in
+      <literal>&lt;your_discourse_package_here&gt;.plugins</literal>. If
+      you want the full suite of plugins provided through
+      <literal>nixpkgs</literal>, you can also set the <xref
+      linkend="opt-services.discourse.package" /> option to
+      <literal>pkgs.discourseAllPlugins</literal>.
+    </para>
+
+    <para>
+      Plugins can be built with the
+      <literal>&lt;your_discourse_package_here&gt;.mkDiscoursePlugin</literal>
+      function. Normally, it should suffice to provide a
+      <literal>name</literal> and <literal>src</literal> attribute. If
+      the plugin has Ruby dependencies, however, they need to be
+      packaged in accordance with the <link
+      xlink:href="https://nixos.org/manual/nixpkgs/stable/#developing-with-ruby">Developing
+      with Ruby</link> section of the Nixpkgs manual and the
+      appropriate gem options set in <literal>bundlerEnvArgs</literal>
+      (normally <literal>gemdir</literal> is sufficient). A plugin's
+      Ruby dependencies are listed in its
+      <filename>plugin.rb</filename> file as function calls to
+      <literal>gem</literal>. To construct the corresponding
+      <filename>Gemfile</filename>, run <command>bundle
+      init</command>, then add the <literal>gem</literal> lines to it
+      verbatim.
     </para>
 
     <para>
@@ -280,7 +302,10 @@ services.discourse = {
     <para>
       For example, to add the <link
       xlink:href="https://github.com/discourse/discourse-spoiler-alert">discourse-spoiler-alert</link>
-      plugin and disable it by default:
+      and <link
+      xlink:href="https://github.com/discourse/discourse-solved">discourse-solved</link>
+      plugins, and disable <literal>discourse-spoiler-alert</literal>
+      by default:
 
 <programlisting>
 services.discourse = {
@@ -301,13 +326,9 @@ services.discourse = {
     <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
   };
   <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
-  <link linkend="opt-services.discourse.mail.incoming.enable">plugins</link> = [
-    (pkgs.fetchFromGitHub {
-      owner = "discourse";
-      repo = "discourse-spoiler-alert";
-      rev = "e200cfa571d252cab63f3d30d619b370986e4cee";
-      sha256 = "0ya69ix5g77wz4c9x9gmng6l25ghb5xxlx3icr6jam16q14dzc33";
-    })
+  <link linkend="opt-services.discourse.mail.incoming.enable">plugins</link> = with config.services.discourse.package.plugins; [
+    discourse-spoiler-alert
+    discourse-solved
   ];
   <link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
     plugins = {
diff --git a/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix b/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
index eea49bda283..f8f0854f1bc 100644
--- a/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
+++ b/nixos/modules/services/web-apps/icingaweb2/icingaweb2.nix
@@ -23,6 +23,16 @@ in {
       '';
     };
 
+    libraryPaths = mkOption {
+      type = attrsOf package;
+      default = { };
+      description = ''
+        Libraries to add to the Icingaweb2 library path.
+        The name of the attribute is the name of the library, the value
+        is the package to add.
+      '';
+    };
+
     virtualHost = mkOption {
       type = nullOr str;
       default = "icingaweb2";
@@ -167,6 +177,9 @@ in {
     services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") {
       ${poolName} = {
         user = "icingaweb2";
+        phpEnv = {
+          ICINGAWEB_LIBDIR = toString (pkgs.linkFarm "icingaweb2-libdir" (mapAttrsToList (name: path: { inherit name path; }) cfg.libraryPaths));
+        };
         phpPackage = pkgs.php.withExtensions ({ enabled, all }: [ all.imagick ] ++ enabled);
         phpOptions = ''
           date.timezone = "${cfg.timezone}"
@@ -184,6 +197,11 @@ in {
       };
     };
 
+    services.icingaweb2.libraryPaths = {
+      ipl = pkgs.icingaweb2-ipl;
+      thirdparty = pkgs.icingaweb2-thirdparty;
+    };
+
     systemd.services."phpfpm-${poolName}".serviceConfig.ReadWritePaths = [ "/etc/icingaweb2" ];
 
     services.nginx = {
diff --git a/nixos/modules/services/web-apps/wordpress.nix b/nixos/modules/services/web-apps/wordpress.nix
index 775ecb3acaf..6f1ef815bc4 100644
--- a/nixos/modules/services/web-apps/wordpress.nix
+++ b/nixos/modules/services/web-apps/wordpress.nix
@@ -3,13 +3,18 @@
 let
   inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types;
   inherit (lib) any attrValues concatMapStringsSep flatten literalExample;
-  inherit (lib) mapAttrs mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString;
+  inherit (lib) filterAttrs mapAttrs mapAttrs' mapAttrsToList nameValuePair optional optionalAttrs optionalString;
 
-  eachSite = config.services.wordpress;
+  cfg = migrateOldAttrs config.services.wordpress;
+  eachSite = cfg.sites;
   user = "wordpress";
-  group = config.services.httpd.group;
+  webserver = config.services.${cfg.webserver};
   stateDir = hostName: "/var/lib/wordpress/${hostName}";
 
+  # Migrate config.services.wordpress.<hostName> to config.services.wordpress.sites.<hostName>
+  oldSites = filterAttrs (o: _: o != "sites" && o != "webserver");
+  migrateOldAttrs = cfg: cfg // { sites = cfg.sites // oldSites cfg; };
+
   pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
     pname = "wordpress-${hostName}";
     version = src.version;
@@ -261,21 +266,48 @@ in
   # interface
   options = {
     services.wordpress = mkOption {
-      type = types.attrsOf (types.submodule siteOpts);
+      type = types.submodule {
+        # Used to support old interface
+        freeformType = types.attrsOf (types.submodule siteOpts);
+
+        # New interface
+        options.sites = mkOption {
+          type = types.attrsOf (types.submodule siteOpts);
+          default = {};
+          description = "Specification of one or more WordPress sites to serve";
+        };
+
+        options.webserver = mkOption {
+          type = types.enum [ "httpd" "nginx" ];
+          default = "httpd";
+          description = ''
+            Whether to use apache2 or nginx for virtual host management.
+
+            Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.&lt;name&gt;</literal>.
+            See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
+
+            Further apache2 configuration can be done by adapting <literal>services.httpd.virtualHosts.&lt;name&gt;</literal>.
+            See <xref linkend="opt-services.httpd.virtualHosts"/> for further information.
+          '';
+        };
+      };
       default = {};
-      description = "Specification of one or more WordPress sites to serve via Apache.";
+      description = "Wordpress configuration";
     };
+
   };
 
   # implementation
-  config = mkIf (eachSite != {}) {
+  config = mkIf (eachSite != {}) (mkMerge [{
 
     assertions = mapAttrsToList (hostName: cfg:
       { assertion = cfg.database.createLocally -> cfg.database.user == user;
-        message = "services.wordpress.${hostName}.database.user must be ${user} if the database is to be automatically provisioned";
+        message = ''services.wordpress.sites."${hostName}".database.user must be ${user} if the database is to be automatically provisioned'';
       }
     ) eachSite;
 
+    warnings = mapAttrsToList (hostName: _: ''services.wordpress."${hostName}" is deprecated use services.wordpress.sites."${hostName}"'') (oldSites cfg);
+
     services.mysql = mkIf (any (v: v.database.createLocally) (attrValues eachSite)) {
       enable = true;
       package = mkDefault pkgs.mariadb;
@@ -289,14 +321,18 @@ in
 
     services.phpfpm.pools = mapAttrs' (hostName: cfg: (
       nameValuePair "wordpress-${hostName}" {
-        inherit user group;
+        inherit user;
+        group = webserver.group;
         settings = {
-          "listen.owner" = config.services.httpd.user;
-          "listen.group" = config.services.httpd.group;
+          "listen.owner" = webserver.user;
+          "listen.group" = webserver.group;
         } // cfg.poolConfig;
       }
     )) eachSite;
 
+  }
+
+  (mkIf (cfg.webserver == "httpd") {
     services.httpd = {
       enable = true;
       extraModules = [ "proxy_fcgi" ];
@@ -332,11 +368,13 @@ in
         '';
       } ]) eachSite;
     };
+  })
 
+  {
     systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [
-      "d '${stateDir hostName}' 0750 ${user} ${group} - -"
-      "d '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
-      "Z '${cfg.uploadsDir}' 0750 ${user} ${group} - -"
+      "d '${stateDir hostName}' 0750 ${user} ${webserver.group} - -"
+      "d '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -"
+      "Z '${cfg.uploadsDir}' 0750 ${user} ${webserver.group} - -"
     ]) eachSite);
 
     systemd.services = mkMerge [
@@ -350,7 +388,7 @@ in
           serviceConfig = {
             Type = "oneshot";
             User = user;
-            Group = group;
+            Group = webserver.group;
           };
       })) eachSite)
 
@@ -360,9 +398,65 @@ in
     ];
 
     users.users.${user} = {
-      group = group;
+      group = webserver.group;
       isSystemUser = true;
     };
+  }
 
-  };
+  (mkIf (cfg.webserver == "nginx") {
+    services.nginx = {
+      enable = true;
+      virtualHosts = mapAttrs (hostName: cfg: {
+        serverName = mkDefault hostName;
+        root = "${pkg hostName cfg}/share/wordpress";
+        extraConfig = ''
+          index index.php;
+        '';
+        locations = {
+          "/" = {
+            priority = 200;
+            extraConfig = ''
+              try_files $uri $uri/ /index.php$is_args$args;
+            '';
+          };
+          "~ \\.php$" = {
+            priority = 500;
+            extraConfig = ''
+              fastcgi_split_path_info ^(.+\.php)(/.+)$;
+              fastcgi_pass unix:${config.services.phpfpm.pools."wordpress-${hostName}".socket};
+              fastcgi_index index.php;
+              include "${config.services.nginx.package}/conf/fastcgi.conf";
+              fastcgi_param PATH_INFO $fastcgi_path_info;
+              fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
+              # Mitigate https://httpoxy.org/ vulnerabilities
+              fastcgi_param HTTP_PROXY "";
+              fastcgi_intercept_errors off;
+              fastcgi_buffer_size 16k;
+              fastcgi_buffers 4 16k;
+              fastcgi_connect_timeout 300;
+              fastcgi_send_timeout 300;
+              fastcgi_read_timeout 300;
+            '';
+          };
+          "~ /\\." = {
+            priority = 800;
+            extraConfig = "deny all;";
+          };
+          "~* /(?:uploads|files)/.*\\.php$" = {
+            priority = 900;
+            extraConfig = "deny all;";
+          };
+          "~* \\.(js|css|png|jpg|jpeg|gif|ico)$" = {
+            priority = 1000;
+            extraConfig = ''
+              expires max;
+              log_not_found off;
+            '';
+          };
+        };
+      }) eachSite;
+    };
+  })
+
+  ]);
 }
diff --git a/nixos/modules/tasks/snapraid.nix b/nixos/modules/tasks/snapraid.nix
new file mode 100644
index 00000000000..4529009930f
--- /dev/null
+++ b/nixos/modules/tasks/snapraid.nix
@@ -0,0 +1,230 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let cfg = config.snapraid;
+in
+{
+  options.snapraid = with types; {
+    enable = mkEnableOption "SnapRAID";
+    dataDisks = mkOption {
+      default = { };
+      example = {
+        d1 = "/mnt/disk1/";
+        d2 = "/mnt/disk2/";
+        d3 = "/mnt/disk3/";
+      };
+      description = "SnapRAID data disks.";
+      type = attrsOf str;
+    };
+    parityFiles = mkOption {
+      default = [ ];
+      example = [
+        "/mnt/diskp/snapraid.parity"
+        "/mnt/diskq/snapraid.2-parity"
+        "/mnt/diskr/snapraid.3-parity"
+        "/mnt/disks/snapraid.4-parity"
+        "/mnt/diskt/snapraid.5-parity"
+        "/mnt/disku/snapraid.6-parity"
+      ];
+      description = "SnapRAID parity files.";
+      type = listOf str;
+    };
+    contentFiles = mkOption {
+      default = [ ];
+      example = [
+        "/var/snapraid.content"
+        "/mnt/disk1/snapraid.content"
+        "/mnt/disk2/snapraid.content"
+      ];
+      description = "SnapRAID content list files.";
+      type = listOf str;
+    };
+    exclude = mkOption {
+      default = [ ];
+      example = [ "*.unrecoverable" "/tmp/" "/lost+found/" ];
+      description = "SnapRAID exclude directives.";
+      type = listOf str;
+    };
+    touchBeforeSync = mkOption {
+      default = true;
+      example = false;
+      description =
+        "Whether <command>snapraid touch</command> should be run before <command>snapraid sync</command>.";
+      type = bool;
+    };
+    sync.interval = mkOption {
+      default = "01:00";
+      example = "daily";
+      description = "How often to run <command>snapraid sync</command>.";
+      type = str;
+    };
+    scrub = {
+      interval = mkOption {
+        default = "Mon *-*-* 02:00:00";
+        example = "weekly";
+        description = "How often to run <command>snapraid scrub</command>.";
+        type = str;
+      };
+      plan = mkOption {
+        default = 8;
+        example = 5;
+        description =
+          "Percent of the array that should be checked by <command>snapraid scrub</command>.";
+        type = int;
+      };
+      olderThan = mkOption {
+        default = 10;
+        example = 20;
+        description =
+          "Number of days since data was last scrubbed before it can be scrubbed again.";
+        type = int;
+      };
+    };
+    extraConfig = mkOption {
+      default = "";
+      example = ''
+        nohidden
+        blocksize 256
+        hashsize 16
+        autosave 500
+        pool /pool
+      '';
+      description = "Extra config options for SnapRAID.";
+      type = lines;
+    };
+  };
+
+  config =
+    let
+      nParity = builtins.length cfg.parityFiles;
+      mkPrepend = pre: s: pre + s;
+    in
+    mkIf cfg.enable {
+      assertions = [
+        {
+          assertion = nParity <= 6;
+          message = "You can have no more than six SnapRAID parity files.";
+        }
+        {
+          assertion = builtins.length cfg.contentFiles >= nParity + 1;
+          message =
+            "There must be at least one SnapRAID content file for each SnapRAID parity file plus one.";
+        }
+      ];
+
+      environment = {
+        systemPackages = with pkgs; [ snapraid ];
+
+        etc."snapraid.conf" = {
+          text = with cfg;
+            let
+              prependData = mkPrepend "data ";
+              prependContent = mkPrepend "content ";
+              prependExclude = mkPrepend "exclude ";
+            in
+            concatStringsSep "\n"
+              (map prependData
+                ((mapAttrsToList (name: value: name + " " + value)) dataDisks)
+              ++ zipListsWith (a: b: a + b)
+                ([ "parity " ] ++ map (i: toString i + "-parity ") (range 2 6))
+                parityFiles ++ map prependContent contentFiles
+              ++ map prependExclude exclude) + "\n" + extraConfig;
+        };
+      };
+
+      systemd.services = with cfg; {
+        snapraid-scrub = {
+          description = "Scrub the SnapRAID array";
+          startAt = scrub.interval;
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${pkgs.snapraid}/bin/snapraid scrub -p ${
+              toString scrub.plan
+            } -o ${toString scrub.olderThan}";
+            Nice = 19;
+            IOSchedulingPriority = 7;
+            CPUSchedulingPolicy = "batch";
+
+            LockPersonality = true;
+            MemoryDenyWriteExecute = true;
+            NoNewPrivileges = true;
+            PrivateDevices = true;
+            PrivateTmp = true;
+            ProtectClock = true;
+            ProtectControlGroups = true;
+            ProtectHostname = true;
+            ProtectKernelLogs = true;
+            ProtectKernelModules = true;
+            ProtectKernelTunables = true;
+            RestrictAddressFamilies = "none";
+            RestrictNamespaces = true;
+            RestrictRealtime = true;
+            RestrictSUIDSGID = true;
+            SystemCallArchitectures = "native";
+            SystemCallFilter = "@system-service";
+            SystemCallErrorNumber = "EPERM";
+            CapabilityBoundingSet = "CAP_DAC_OVERRIDE";
+
+            ProtectSystem = "strict";
+            ProtectHome = "read-only";
+            ReadWritePaths =
+              # scrub requires access to directories containing content files
+              # to remove them if they are stale
+              let
+                contentDirs = map dirOf contentFiles;
+              in
+              unique (
+                attrValues dataDisks ++ contentDirs
+              );
+          };
+          unitConfig.After = "snapraid-sync.service";
+        };
+        snapraid-sync = {
+          description = "Synchronize the state of the SnapRAID array";
+          startAt = sync.interval;
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${pkgs.snapraid}/bin/snapraid sync";
+            Nice = 19;
+            IOSchedulingPriority = 7;
+            CPUSchedulingPolicy = "batch";
+
+            LockPersonality = true;
+            MemoryDenyWriteExecute = true;
+            NoNewPrivileges = true;
+            PrivateDevices = true;
+            PrivateTmp = true;
+            ProtectClock = true;
+            ProtectControlGroups = true;
+            ProtectHostname = true;
+            ProtectKernelLogs = true;
+            ProtectKernelModules = true;
+            ProtectKernelTunables = true;
+            RestrictAddressFamilies = "none";
+            RestrictNamespaces = true;
+            RestrictRealtime = true;
+            RestrictSUIDSGID = true;
+            SystemCallArchitectures = "native";
+            SystemCallFilter = "@system-service";
+            SystemCallErrorNumber = "EPERM";
+            CapabilityBoundingSet = "CAP_DAC_OVERRIDE";
+
+            ProtectSystem = "strict";
+            ProtectHome = "read-only";
+            ReadWritePaths =
+              # sync requires access to directories containing content files
+              # to remove them if they are stale
+              let
+                contentDirs = map dirOf contentFiles;
+              in
+              unique (
+                attrValues dataDisks ++ parityFiles ++ contentDirs
+              );
+          } // optionalAttrs touchBeforeSync {
+            ExecStartPre = "${pkgs.snapraid}/bin/snapraid touch";
+          };
+        };
+      };
+    };
+}
diff --git a/nixos/modules/virtualisation/vmware-guest.nix b/nixos/modules/virtualisation/vmware-guest.nix
index 962a9059ea4..9465a8d6800 100644
--- a/nixos/modules/virtualisation/vmware-guest.nix
+++ b/nixos/modules/virtualisation/vmware-guest.nix
@@ -56,5 +56,7 @@ in
           ${open-vm-tools}/bin/vmware-user-suid-wrapper
         '';
     };
+
+    services.udev.packages = [ open-vm-tools ];
   };
 }
diff --git a/nixos/tests/agda.nix b/nixos/tests/agda.nix
index f282788519c..ec61af2afe7 100644
--- a/nixos/tests/agda.nix
+++ b/nixos/tests/agda.nix
@@ -2,6 +2,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
 
 let
   hello-world = pkgs.writeText "hello-world" ''
+    {-# OPTIONS --guardedness #-}
     open import IO
     open import Level
 
@@ -35,10 +36,6 @@ in
     machine.succeed("touch TestEmpty.agda")
     machine.succeed("agda TestEmpty.agda")
 
-    # Minimal script that actually uses the standard library
-    machine.succeed('echo "import IO" > TestIO.agda')
-    machine.succeed("agda -l standard-library -i . TestIO.agda")
-
     # Hello world
     machine.succeed(
         "cp ${hello-world} HelloWorld.agda"
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index e364bbe1bfa..ef721dabb58 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -43,7 +43,7 @@ in
   bitcoind = handleTest ./bitcoind.nix {};
   bittorrent = handleTest ./bittorrent.nix {};
   blockbook-frontend = handleTest ./blockbook-frontend.nix {};
-  boot = handleTestOn ["x86_64-linux"] ./boot.nix {}; # syslinux is unsupported on aarch64
+  boot = handleTestOn ["x86_64-linux" "aarch64-linux"] ./boot.nix {};
   boot-stage1 = handleTest ./boot-stage1.nix {};
   borgbackup = handleTest ./borgbackup.nix {};
   botamusique = handleTest ./botamusique.nix {};
diff --git a/nixos/tests/boot.nix b/nixos/tests/boot.nix
index c5040f3b31f..bdae6341ec9 100644
--- a/nixos/tests/boot.nix
+++ b/nixos/tests/boot.nix
@@ -4,6 +4,7 @@
 }:
 
 with import ../lib/testing-python.nix { inherit system pkgs; };
+with import ../lib/qemu-flags.nix { inherit pkgs; };
 with pkgs.lib;
 
 let
@@ -21,7 +22,10 @@ let
 
   makeBootTest = name: extraConfig:
     let
-      machineConfig = pythonDict ({ qemuFlags = "-m 768"; } // extraConfig);
+      machineConfig = pythonDict ({
+        qemuBinary = qemuBinary pkgs.qemu_test;
+        qemuFlags = "-m 768";
+      } // extraConfig);
     in
       makeTest {
         inherit iso;
@@ -61,6 +65,7 @@ let
         ];
       };
       machineConfig = pythonDict ({
+        qemuBinary = qemuBinary pkgs.qemu_test;
         qemuFlags = "-boot order=n -m 2000";
         netBackendArgs = "tftp=${ipxeBootDir},bootfile=netboot.ipxe";
       } // extraConfig);
@@ -75,31 +80,34 @@ let
             machine.shutdown()
           '';
       };
+  uefiBinary = {
+    x86_64-linux = "${pkgs.OVMF.fd}/FV/OVMF.fd";
+    aarch64-linux = "${pkgs.OVMF.fd}/FV/QEMU_EFI.fd";
+  }.${pkgs.stdenv.hostPlatform.system};
 in {
-
-    biosCdrom = makeBootTest "bios-cdrom" {
+    uefiCdrom = makeBootTest "uefi-cdrom" {
       cdrom = "${iso}/iso/${iso.isoName}";
+      bios = uefiBinary;
     };
 
-    biosUsb = makeBootTest "bios-usb" {
+    uefiUsb = makeBootTest "uefi-usb" {
       usb = "${iso}/iso/${iso.isoName}";
+      bios = uefiBinary;
     };
 
-    uefiCdrom = makeBootTest "uefi-cdrom" {
+    uefiNetboot = makeNetbootTest "uefi" {
+      bios = uefiBinary;
+      # Custom ROM is needed for EFI PXE boot. I failed to understand exactly why, because QEMU should still use iPXE for EFI.
+      netFrontendArgs = "romfile=${pkgs.ipxe}/ipxe.efirom";
+    };
+} // optionalAttrs (pkgs.stdenv.hostPlatform.system == "x86_64-linux") {
+    biosCdrom = makeBootTest "bios-cdrom" {
       cdrom = "${iso}/iso/${iso.isoName}";
-      bios = "${pkgs.OVMF.fd}/FV/OVMF.fd";
     };
 
-    uefiUsb = makeBootTest "uefi-usb" {
+    biosUsb = makeBootTest "bios-usb" {
       usb = "${iso}/iso/${iso.isoName}";
-      bios = "${pkgs.OVMF.fd}/FV/OVMF.fd";
     };
 
     biosNetboot = makeNetbootTest "bios" {};
-
-    uefiNetboot = makeNetbootTest "uefi" {
-      bios = "${pkgs.OVMF.fd}/FV/OVMF.fd";
-      # Custom ROM is needed for EFI PXE boot. I failed to understand exactly why, because QEMU should still use iPXE for EFI.
-      netFrontendArgs = "romfile=${pkgs.ipxe}/ipxe.efirom";
-    };
 }
diff --git a/nixos/tests/wordpress.nix b/nixos/tests/wordpress.nix
index a5c10c2de74..45c58b5b65c 100644
--- a/nixos/tests/wordpress.nix
+++ b/nixos/tests/wordpress.nix
@@ -10,48 +10,68 @@ import ./make-test-python.nix ({ pkgs, ... }:
     ];
   };
 
-  machine =
-    { ... }:
-    { services.httpd.adminAddr = "webmaster@site.local";
+  nodes = {
+    wp_httpd = { ... }: {
+      services.httpd.adminAddr = "webmaster@site.local";
       services.httpd.logPerVirtualHost = true;
 
-      services.wordpress."site1.local" = {
-        database.tablePrefix = "site1_";
+      services.wordpress = {
+        # Test support for old interface
+        "site1.local" = {
+          database.tablePrefix = "site1_";
+        };
+        sites = {
+          "site2.local" = {
+            database.tablePrefix = "site2_";
+          };
+        };
       };
 
-      services.wordpress."site2.local" = {
-        database.tablePrefix = "site2_";
+      networking.firewall.allowedTCPPorts = [ 80 ];
+      networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
+    };
+
+    wp_nginx = { ... }: {
+      services.wordpress.webserver = "nginx";
+      services.wordpress.sites = {
+        "site1.local" = {
+          database.tablePrefix = "site1_";
+        };
+        "site2.local" = {
+          database.tablePrefix = "site2_";
+        };
       };
 
+      networking.firewall.allowedTCPPorts = [ 80 ];
       networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
     };
+  };
 
   testScript = ''
     import re
 
     start_all()
 
-    machine.wait_for_unit("httpd")
-
-    machine.wait_for_unit("phpfpm-wordpress-site1.local")
-    machine.wait_for_unit("phpfpm-wordpress-site2.local")
+    wp_httpd.wait_for_unit("httpd")
+    wp_nginx.wait_for_unit("nginx")
 
     site_names = ["site1.local", "site2.local"]
 
-    with subtest("website returns welcome screen"):
+    for machine in (wp_httpd, wp_nginx):
         for site_name in site_names:
-            assert "Welcome to the famous" in machine.succeed(f"curl -fL {site_name}")
+            machine.wait_for_unit(f"phpfpm-wordpress-{site_name}")
 
-    with subtest("wordpress-init went through"):
-        for site_name in site_names:
-            info = machine.get_unit_info(f"wordpress-init-{site_name}")
-            assert info["Result"] == "success"
+            with subtest("website returns welcome screen"):
+                assert "Welcome to the famous" in machine.succeed(f"curl -L {site_name}")
 
-    with subtest("secret keys are set"):
-        pattern = re.compile(r"^define.*NONCE_SALT.{64,};$", re.MULTILINE)
-        for site_name in site_names:
-            assert pattern.search(
-                machine.succeed(f"cat /var/lib/wordpress/{site_name}/secret-keys.php")
-            )
+            with subtest("wordpress-init went through"):
+                info = machine.get_unit_info(f"wordpress-init-{site_name}")
+                assert info["Result"] == "success"
+
+            with subtest("secret keys are set"):
+                pattern = re.compile(r"^define.*NONCE_SALT.{64,};$", re.MULTILINE)
+                assert pattern.search(
+                    machine.succeed(f"cat /var/lib/wordpress/{site_name}/secret-keys.php")
+                )
   '';
 })