summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorVladimír Čunát <v@cunat.cz>2019-05-11 09:36:12 +0200
committerVladimír Čunát <v@cunat.cz>2019-05-11 09:41:36 +0200
commite8f4ad0169e30d18a80a58ab714757d15173ee1a (patch)
tree6469ea06380a25b34d39549aedbf611ce658c9d5 /nixos
parent3b9e3a5a57f5bfbee59f8c142b68bba37d7d263b (diff)
parent76e3af41a5c8e82213c4c7f3f0b029c1855b2a78 (diff)
downloadnixpkgs-e8f4ad0169e30d18a80a58ab714757d15173ee1a.tar
nixpkgs-e8f4ad0169e30d18a80a58ab714757d15173ee1a.tar.gz
nixpkgs-e8f4ad0169e30d18a80a58ab714757d15173ee1a.tar.bz2
nixpkgs-e8f4ad0169e30d18a80a58ab714757d15173ee1a.tar.lz
nixpkgs-e8f4ad0169e30d18a80a58ab714757d15173ee1a.tar.xz
nixpkgs-e8f4ad0169e30d18a80a58ab714757d15173ee1a.tar.zst
nixpkgs-e8f4ad0169e30d18a80a58ab714757d15173ee1a.zip
Merge branch 'master' into staging-next
~5k rebuilds per platform.
Hydra nixpkgs: ?compare=1518759
Diffstat (limited to 'nixos')
-rw-r--r--nixos/modules/misc/ids.nix2
-rw-r--r--nixos/modules/module-list.nix2
-rw-r--r--nixos/modules/programs/zmap.nix18
-rw-r--r--nixos/modules/security/rngd.nix33
-rw-r--r--nixos/modules/services/logging/journalbeat.nix43
-rw-r--r--nixos/modules/services/misc/paperless.nix185
-rw-r--r--nixos/modules/services/monitoring/vnstat.nix19
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix5
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix9
-rw-r--r--nixos/modules/virtualisation/virtualbox-host.nix2
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/elk.nix61
-rw-r--r--nixos/tests/gitea.nix2
-rw-r--r--nixos/tests/paperless.nix29
-rw-r--r--nixos/tests/virtualbox.nix42
15 files changed, 416 insertions, 37 deletions
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index cd6bb9019b1..5198bedc138 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -339,6 +339,7 @@
       rss2email = 312;
       cockroachdb = 313;
       zoneminder = 314;
+      paperless = 315;
 
       # When adding a uid, make sure it doesn't match an existing gid. And don't use uids above 399!
 
@@ -638,6 +639,7 @@
       rss2email = 312;
       cockroachdb = 313;
       zoneminder = 314;
+      paperless = 315;
 
       # When adding a gid, make sure it doesn't match an existing
       # uid. Users and groups with the same name should have equal
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index a07461022a3..dee850f47f2 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -144,6 +144,7 @@
   ./programs/xonsh.nix
   ./programs/xss-lock.nix
   ./programs/yabar.nix
+  ./programs/zmap.nix
   ./programs/zsh/oh-my-zsh.nix
   ./programs/zsh/zsh.nix
   ./programs/zsh/zsh-autoenv.nix
@@ -435,6 +436,7 @@
   ./services/misc/octoprint.nix
   ./services/misc/osrm.nix
   ./services/misc/packagekit.nix
+  ./services/misc/paperless.nix
   ./services/misc/parsoid.nix
   ./services/misc/phd.nix
   ./services/misc/plex.nix
diff --git a/nixos/modules/programs/zmap.nix b/nixos/modules/programs/zmap.nix
new file mode 100644
index 00000000000..2e27fce4d7c
--- /dev/null
+++ b/nixos/modules/programs/zmap.nix
@@ -0,0 +1,18 @@
+{ pkgs, config, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.zmap;
+in {
+  options.programs.zmap = {
+    enable = mkEnableOption "ZMap";
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.zmap ];
+
+    environment.etc."zmap/blacklist.conf".source = "${pkgs.zmap}/etc/zmap/blacklist.conf";
+    environment.etc."zmap/zmap.conf".source = "${pkgs.zmap}/etc/zmap.conf";
+  };
+}
diff --git a/nixos/modules/security/rngd.nix b/nixos/modules/security/rngd.nix
index a54ef2e6fca..60361d9960e 100644
--- a/nixos/modules/security/rngd.nix
+++ b/nixos/modules/security/rngd.nix
@@ -2,20 +2,30 @@
 
 with lib;
 
+let
+  cfg = config.security.rngd;
+in
 {
   options = {
-    security.rngd.enable = mkOption {
-      type = types.bool;
-      default = true;
-      description = ''
-        Whether to enable the rng daemon, which adds entropy from
-        hardware sources of randomness to the kernel entropy pool when
-        available.
-      '';
+    security.rngd = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to enable the rng daemon, which adds entropy from
+          hardware sources of randomness to the kernel entropy pool when
+          available.
+        '';
+      };
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable debug output (-d).";
+      };
     };
   };
 
-  config = mkIf config.security.rngd.enable {
+  config = mkIf cfg.enable {
     services.udev.extraRules = ''
       KERNEL=="random", TAG+="systemd"
       SUBSYSTEM=="cpu", ENV{MODALIAS}=="cpu:type:x86,*feature:*009E*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="rngd.service"
@@ -29,7 +39,10 @@ with lib;
 
       description = "Hardware RNG Entropy Gatherer Daemon";
 
-      serviceConfig.ExecStart = "${pkgs.rng-tools}/sbin/rngd -f";
+      serviceConfig = {
+        ExecStart = "${pkgs.rng-tools}/sbin/rngd -f"
+          + optionalString cfg.debug " -d";
+      };
     };
   };
 }
diff --git a/nixos/modules/services/logging/journalbeat.nix b/nixos/modules/services/logging/journalbeat.nix
index 8186a3b02c3..89f53b1b245 100644
--- a/nixos/modules/services/logging/journalbeat.nix
+++ b/nixos/modules/services/logging/journalbeat.nix
@@ -5,11 +5,13 @@ with lib;
 let
   cfg = config.services.journalbeat;
 
+  lt6 = builtins.compareVersions cfg.package.version "6" < 0;
+
   journalbeatYml = pkgs.writeText "journalbeat.yml" ''
     name: ${cfg.name}
     tags: ${builtins.toJSON cfg.tags}
 
-    journalbeat.cursor_state_file: ${cfg.stateDir}/cursor-state
+    ${optionalString lt6 "journalbeat.cursor_state_file: /var/lib/${cfg.stateDir}/cursor-state"}
 
     ${cfg.extraConfig}
   '';
@@ -22,6 +24,16 @@ in
 
       enable = mkEnableOption "journalbeat";
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.journalbeat;
+        defaultText = "pkgs.journalbeat";
+        example = literalExample "pkgs.journalbeat7";
+        description = ''
+          The journalbeat package to use
+        '';
+      };
+
       name = mkOption {
         type = types.str;
         default = "journalbeat";
@@ -36,13 +48,17 @@ in
 
       stateDir = mkOption {
         type = types.str;
-        default = "/var/lib/journalbeat";
-        description = "The state directory. Journalbeat's own logs and other data are stored here.";
+        default = "journalbeat";
+        description = ''
+          Directory below <literal>/var/lib/</literal> to store journalbeat's
+          own logs and other data. This directory will be created automatically
+          using systemd's StateDirectory mechanism.
+        '';
       };
 
       extraConfig = mkOption {
         type = types.lines;
-        default = ''
+        default = optionalString lt6 ''
           journalbeat:
             seek_position: cursor
             cursor_seek_fallback: tail
@@ -61,7 +77,16 @@ in
 
   config = mkIf cfg.enable {
 
-    systemd.services.journalbeat = with pkgs; {
+    assertions = [
+      {
+        assertion = !hasPrefix "/" cfg.stateDir;
+        message =
+          "The option services.journalbeat.stateDir shouldn't be an absolute directory." +
+          " It should be a directory relative to /var/lib/.";
+      }
+    ];
+
+    systemd.services.journalbeat = {
       description = "Journalbeat log shipper";
       wantedBy = [ "multi-user.target" ];
       preStart = ''
@@ -69,7 +94,13 @@ in
         mkdir -p ${cfg.stateDir}/logs
       '';
       serviceConfig = {
-        ExecStart = "${pkgs.journalbeat}/bin/journalbeat -c ${journalbeatYml} -path.data ${cfg.stateDir}/data -path.logs ${cfg.stateDir}/logs";
+        StateDirectory = cfg.stateDir;
+        ExecStart = ''
+          ${cfg.package}/bin/journalbeat \
+            -c ${journalbeatYml} \
+            -path.data /var/lib/${cfg.stateDir}/data \
+            -path.logs /var/lib/${cfg.stateDir}/logs'';
+        Restart = "always";
       };
     };
   };
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
new file mode 100644
index 00000000000..4e6cd80e242
--- /dev/null
+++ b/nixos/modules/services/misc/paperless.nix
@@ -0,0 +1,185 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.paperless;
+
+  defaultUser = "paperless";
+
+  manage = cfg.package.withConfig {
+    config = {
+      PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
+      PAPERLESS_INLINE_DOC = "true";
+      PAPERLESS_DISABLE_LOGIN = "true";
+    } // cfg.extraConfig;
+    inherit (cfg) dataDir ocrLanguages;
+    paperlessPkg = cfg.package;
+  };
+in
+{
+  options.services.paperless = {
+    enable = mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Enable Paperless.
+
+        When started, the Paperless database is automatically created if it doesn't
+        exist and updated if the Paperless package has changed.
+        Both tasks are achieved by running a Django migration.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/paperless";
+      description = "Directory to store the Paperless data.";
+    };
+
+    consumptionDir = mkOption {
+      type = types.str;
+      default = "${cfg.dataDir}/consume";
+      defaultText = "\${dataDir}/consume";
+      description = "Directory from which new documents are imported.";
+    };
+
+    consumptionDirIsPublic = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Whether all users can write to the consumption dir.";
+    };
+
+    ocrLanguages = mkOption {
+      type = with types; nullOr (listOf string);
+      default = null;
+      description = ''
+        Languages available for OCR via Tesseract, specified as
+        <literal>ISO 639-2/T</literal> language codes.
+        If unset, defaults to all available languages.
+      '';
+      example = [ "eng" "spa" "jpn" ];
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = "Server listening address.";
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = 28981;
+      description = "Server port to listen on.";
+    };
+
+    extraConfig = mkOption {
+      type = types.attrs;
+      default = {};
+      description = ''
+        Extra paperless config options.
+
+        The config values are evaluated as double-quoted Bash string literals.
+
+        See <literal>paperless-src/paperless.conf.example</literal> for available options.
+
+        To enable user authentication, set <literal>PAPERLESS_DISABLE_LOGIN = "false"</literal>
+        and run the shell command <literal>$dataDir/paperless-manage createsuperuser</literal>.
+
+        To define secret options without storing them in /nix/store, use the following pattern:
+        <literal>PAPERLESS_PASSPHRASE = "$(&lt; /etc/my_passphrase_file)"</literal>
+      '';
+      example = literalExample ''
+        {
+          PAPERLESS_OCR_LANGUAGE = "deu";
+        }
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = defaultUser;
+      description = "User under which Paperless runs.";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.paperless;
+      defaultText = "pkgs.paperless";
+      description = "The Paperless package to use.";
+    };
+
+    manage = mkOption {
+      type = types.package;
+      readOnly = true;
+      default = manage;
+      description = ''
+        A script to manage the Paperless instance.
+        It wraps Django's manage.py and is also available at
+        <literal>$dataDir/manage-paperless</literal>
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' - ${cfg.user} ${cfg.user} - -"
+    ] ++ (optional cfg.consumptionDirIsPublic
+      "d '${cfg.consumptionDir}' 777 ${cfg.user} ${cfg.user} - -"
+      # If the consumption dir is not created here, it's automatically created by
+      # 'manage' with the default permissions.
+    );
+
+    systemd.services.paperless-consumer = {
+      description = "Paperless document consumer";
+      serviceConfig = {
+        User = cfg.user;
+        ExecStart = "${manage} document_consumer";
+        Restart = "always";
+      };
+      after = [ "systemd-tmpfiles-setup.service" ];
+      wantedBy = [ "multi-user.target" ];
+      preStart = ''
+        if [[ $(readlink ${cfg.dataDir}/paperless-manage) != ${manage} ]]; then
+          ln -sf ${manage} ${cfg.dataDir}/paperless-manage
+        fi
+
+        ${manage.setupEnv}
+        # Auto-migrate on first run or if the package has changed
+        versionFile="$PAPERLESS_DBDIR/src-version"
+        if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
+          python $paperlessSrc/manage.py migrate
+          echo ${cfg.package} > "$versionFile"
+        fi
+      '';
+    };
+
+    systemd.services.paperless-server = {
+      description = "Paperless document server";
+      serviceConfig = {
+        User = cfg.user;
+        ExecStart = "${manage} runserver --noreload ${cfg.address}:${toString cfg.port}";
+        Restart = "always";
+      };
+      # Bind to `paperless-consumer` so that the server never runs
+      # during migrations
+      bindsTo = [ "paperless-consumer.service" ];
+      after = [ "paperless-consumer.service" ];
+      wantedBy = [ "multi-user.target" ];
+    };
+
+    users = optionalAttrs (cfg.user == defaultUser) {
+      users = [{
+        name = defaultUser;
+        group = defaultUser;
+        uid = config.ids.uids.paperless;
+        home = cfg.dataDir;
+      }];
+
+      groups = [{
+        name = defaultUser;
+        gid = config.ids.gids.paperless;
+      }];
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/vnstat.nix b/nixos/modules/services/monitoring/vnstat.nix
index cb2f8c07edb..e9bedb704a4 100644
--- a/nixos/modules/services/monitoring/vnstat.nix
+++ b/nixos/modules/services/monitoring/vnstat.nix
@@ -28,14 +28,29 @@ in {
       path = [ pkgs.coreutils ];
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      unitConfig.documentation = "man:vnstatd(1) man:vnstat(1) man:vnstat.conf(5)";
+      documentation = [
+        "man:vnstatd(1)"
+        "man:vnstat(1)"
+        "man:vnstat.conf(5)"
+      ];
       preStart = "chmod 755 /var/lib/vnstat";
       serviceConfig = {
         ExecStart = "${pkgs.vnstat}/bin/vnstatd -n";
         ExecReload = "${pkgs.procps}/bin/kill -HUP $MAINPID";
-        ProtectHome = true;
+
+        # Hardening (from upstream example service)
+        ProtectSystem = "strict";
+        StateDirectory = "vnstat";
         PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectKernelModules = true;
         PrivateTmp = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+
         User = "vnstatd";
       };
     };
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
index 3ab4f26399f..afa0cebbc52 100644
--- a/nixos/modules/services/x11/display-managers/lightdm.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -189,6 +189,11 @@ in
   config = mkIf cfg.enable {
 
     assertions = [
+      { assertion = xcfg.enable;
+        message = ''
+          LightDM requires services.xserver.enable to be true
+        '';
+      }
       { assertion = cfg.autoLogin.enable -> cfg.autoLogin.user != null;
         message = ''
           LightDM auto-login requires services.xserver.displayManager.lightdm.autoLogin.user to be set
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
index 05830e325d5..d1ed345ac57 100644
--- a/nixos/modules/services/x11/display-managers/sddm.nix
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -195,6 +195,11 @@ in
   config = mkIf cfg.enable {
 
     assertions = [
+      { assertion = xcfg.enable;
+        message = ''
+          SDDM requires services.xserver.enable to be true
+        '';
+      }
       { assertion = cfg.autoLogin.enable -> cfg.autoLogin.user != null;
         message = ''
           SDDM auto-login requires services.xserver.displayManager.sddm.autoLogin.user to be set
@@ -264,8 +269,8 @@ in
     };
 
     environment.etc."sddm.conf".source = cfgFile;
-    environment.pathsToLink = [ 
-      "/share/sddm" 
+    environment.pathsToLink = [
+      "/share/sddm"
     ];
 
     users.groups.sddm.gid = config.ids.gids.sddm;
diff --git a/nixos/modules/virtualisation/virtualbox-host.nix b/nixos/modules/virtualisation/virtualbox-host.nix
index 6f737018174..41bcb909fb5 100644
--- a/nixos/modules/virtualisation/virtualbox-host.nix
+++ b/nixos/modules/virtualisation/virtualbox-host.nix
@@ -104,7 +104,7 @@ in
       "VBoxNetNAT"
       "VBoxSDL"
       "VBoxVolInfo"
-      "VirtualBox"
+      "VirtualBoxVM"
     ]));
 
     users.groups.vboxusers.gid = config.ids.gids.vboxusers;
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 95654b67960..d495b2fa633 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -189,6 +189,7 @@ in
   pam-oath-login = handleTest ./pam-oath-login.nix {};
   pam-u2f = handleTest ./pam-u2f.nix {};
   pantheon = handleTest ./pantheon.nix {};
+  paperless = handleTest ./paperless.nix {};
   peerflix = handleTest ./peerflix.nix {};
   pgjwt = handleTest ./pgjwt.nix {};
   pgmanage = handleTest ./pgmanage.nix {};
diff --git a/nixos/tests/elk.nix b/nixos/tests/elk.nix
index 3b3fbd73dd5..95371ef4443 100644
--- a/nixos/tests/elk.nix
+++ b/nixos/tests/elk.nix
@@ -12,6 +12,11 @@ with pkgs.lib;
 let
   esUrl = "http://localhost:9200";
 
+  totalHits = message :
+    "curl --silent --show-error '${esUrl}/_search' -H 'Content-Type: application/json' " +
+    ''-d '{\"query\" : { \"match\" : { \"message\" : \"${message}\"}}}' '' +
+    "| jq .hits.total";
+
   mkElkTest = name : elk :
    let elasticsearchGe7 = builtins.compareVersions elk.elasticsearch.version "7" >= 0;
    in makeTest {
@@ -21,7 +26,7 @@ let
     };
     nodes = {
       one =
-        { pkgs, ... }: {
+        { pkgs, lib, ... }: {
             # Not giving the machine at least 2060MB results in elasticsearch failing with the following error:
             #
             #   OpenJDK 64-Bit Server VM warning:
@@ -40,6 +45,26 @@ let
             environment.systemPackages = [ pkgs.jq ];
 
             services = {
+
+              journalbeat = let lt6 = builtins.compareVersions
+                                        elk.journalbeat.version "6" < 0; in {
+                enable = true;
+                package = elk.journalbeat;
+                extraConfig = mkOptionDefault (''
+                  logging:
+                    to_syslog: true
+                    level: warning
+                    metrics.enabled: false
+                  output.elasticsearch:
+                    hosts: [ "127.0.0.1:9200" ]
+                    ${optionalString lt6 "template.enabled: false"}
+                '' + optionalString (!lt6) ''
+                  journalbeat.inputs:
+                  - paths: []
+                    seek: cursor
+                '');
+              };
+
               logstash = {
                 enable = true;
                 package = elk.logstash;
@@ -107,14 +132,19 @@ let
     testScript = ''
       startAll;
 
+      # Wait until elasticsearch is listening for connections.
       $one->waitForUnit("elasticsearch.service");
+      $one->waitForOpenPort(9200);
 
       # Continue as long as the status is not "red". The status is probably
       # "yellow" instead of "green" because we are using a single elasticsearch
       # node which elasticsearch considers risky.
       #
-      # TODO: extend this test with multiple elasticsearch nodes and see if the status turns "green".
-      $one->waitUntilSucceeds("curl --silent --show-error '${esUrl}/_cluster/health' | jq .status | grep -v red");
+      # TODO: extend this test with multiple elasticsearch nodes
+      #       and see if the status turns "green".
+      $one->waitUntilSucceeds(
+        "curl --silent --show-error '${esUrl}/_cluster/health' " .
+        "| jq .status | grep -v red");
 
       # Perform some simple logstash tests.
       $one->waitForUnit("logstash.service");
@@ -123,16 +153,28 @@ let
 
       # See if kibana is healthy.
       $one->waitForUnit("kibana.service");
-      $one->waitUntilSucceeds("curl --silent --show-error 'http://localhost:5601/api/status' | jq .status.overall.state | grep green");
+      $one->waitUntilSucceeds(
+        "curl --silent --show-error 'http://localhost:5601/api/status' " .
+        "| jq .status.overall.state | grep green");
 
       # See if logstash messages arive in elasticsearch.
-      $one->waitUntilSucceeds("curl --silent --show-error '${esUrl}/_search' -H 'Content-Type: application/json' -d '{\"query\" : { \"match\" : { \"message\" : \"flowers\"}}}' | jq .hits.total | grep -v 0");
-      $one->waitUntilSucceeds("curl --silent --show-error '${esUrl}/_search' -H 'Content-Type: application/json' -d '{\"query\" : { \"match\" : { \"message\" : \"dragons\"}}}' | jq .hits.total | grep 0");
+      $one->waitUntilSucceeds("${totalHits "flowers"} | grep -v 0");
+      $one->waitUntilSucceeds("${totalHits "dragons"} | grep 0");
+
+      # Test if a message logged to the journal
+      # is ingested by elasticsearch via journalbeat.
+      $one->waitForUnit("journalbeat.service");
+      $one->execute("echo 'Supercalifragilisticexpialidocious' | systemd-cat");
+      $one->waitUntilSucceeds(
+        "${totalHits "Supercalifragilisticexpialidocious"} | grep -v 0");
+
     '' + optionalString (!elasticsearchGe7) ''
       # Test elasticsearch-curator.
       $one->systemctl("stop logstash");
       $one->systemctl("start elasticsearch-curator");
-      $one->waitUntilSucceeds("! curl --silent --show-error '${esUrl}/_cat/indices' | grep logstash | grep -q ^$1");
+      $one->waitUntilSucceeds(
+        "! curl --silent --show-error '${esUrl}/_cat/indices' " .
+        "| grep logstash | grep -q ^$1");
     '';
   };
 in mapAttrs mkElkTest {
@@ -140,6 +182,7 @@ in mapAttrs mkElkTest {
     elasticsearch = pkgs.elasticsearch5;
     logstash      = pkgs.logstash5;
     kibana        = pkgs.kibana5;
+    journalbeat   = pkgs.journalbeat5;
   };
   "ELK-6" =
     if enableUnfree
@@ -147,11 +190,13 @@ in mapAttrs mkElkTest {
       elasticsearch = pkgs.elasticsearch6;
       logstash      = pkgs.logstash6;
       kibana        = pkgs.kibana6;
+      journalbeat   = pkgs.journalbeat6;
     }
     else {
       elasticsearch = pkgs.elasticsearch6-oss;
       logstash      = pkgs.logstash6-oss;
       kibana        = pkgs.kibana6-oss;
+      journalbeat   = pkgs.journalbeat6;
     };
   "ELK-7" =
     if enableUnfree
@@ -159,10 +204,12 @@ in mapAttrs mkElkTest {
       elasticsearch = pkgs.elasticsearch7;
       logstash      = pkgs.logstash7;
       kibana        = pkgs.kibana7;
+      journalbeat   = pkgs.journalbeat7;
     }
     else {
       elasticsearch = pkgs.elasticsearch7-oss;
       logstash      = pkgs.logstash7-oss;
       kibana        = pkgs.kibana7-oss;
+      journalbeat   = pkgs.journalbeat7;
     };
 }
diff --git a/nixos/tests/gitea.nix b/nixos/tests/gitea.nix
index d43efc3687a..cccf8c7cd44 100644
--- a/nixos/tests/gitea.nix
+++ b/nixos/tests/gitea.nix
@@ -9,7 +9,7 @@ with pkgs.lib;
 {
   mysql = makeTest {
     name = "gitea-mysql";
-    meta.maintainers = [ maintainers.aanderse ];
+    meta.maintainers = with maintainers; [ aanderse kolaente ];
 
     machine =
       { config, pkgs, ... }:
diff --git a/nixos/tests/paperless.nix b/nixos/tests/paperless.nix
new file mode 100644
index 00000000000..860ad0a6218
--- /dev/null
+++ b/nixos/tests/paperless.nix
@@ -0,0 +1,29 @@
+import ./make-test.nix ({ lib, ... } : {
+  name = "paperless";
+  meta = with lib.maintainers; {
+    maintainers = [ earvstedt ];
+  };
+
+  machine = { pkgs, ... }: {
+    environment.systemPackages = with pkgs; [ imagemagick jq ];
+    services.paperless = {
+      enable = true;
+      ocrLanguages = [ "eng" ];
+    };
+  };
+
+  testScript = ''
+    $machine->waitForUnit("paperless-consumer.service");
+    # Create test doc
+    $machine->succeed('convert -size 400x40 xc:white -font "DejaVu-Sans" -pointsize 20 -fill black \
+      -annotate +5+20 "hello world 16-10-2005" /var/lib/paperless/consume/doc.png');
+
+    $machine->waitForUnit("paperless-server.service");
+    # Wait until server accepts connections
+    $machine->waitUntilSucceeds("curl -s localhost:28981");
+    # Wait until document is consumed
+    $machine->waitUntilSucceeds('(($(curl -s localhost:28981/api/documents/ | jq .count) == 1))');
+    $machine->succeed("curl -s localhost:28981/api/documents/ | jq '.results | .[0] | .created'")
+      =~ /2005-10-16/ or die;
+  '';
+})
diff --git a/nixos/tests/virtualbox.nix b/nixos/tests/virtualbox.nix
index 84d5f3e1530..844ce47d743 100644
--- a/nixos/tests/virtualbox.nix
+++ b/nixos/tests/virtualbox.nix
@@ -2,9 +2,26 @@
   config ? {},
   pkgs ? import ../.. { inherit system config; },
   debug ? false,
-  enableUnfree ? false
+  enableUnfree ? false,
+  # Nested KVM virtualization (https://www.linux-kvm.org/page/Nested_Guests)
+  # requires a modprobe flag on the build machine: (kvm-amd for AMD CPUs)
+  #   boot.extraModprobeConfig = "options kvm-intel nested=Y";
+  # Without this VirtualBox will use SW virtualization and will only be able
+  # to run 32-bit guests.
+  useKvmNestedVirt ? false,
+  # Whether to run 64-bit guests instead of 32-bit. Requires nested KVM.
+  use64bitGuest ? false,
+  # Whether to enable the virtual UART in VirtualBox guests, allowing to see
+  # the guest console. There is currently a bug in VirtualBox where this will
+  # cause a crash if running with SW virtualization
+  # (https://www.virtualbox.org/ticket/18632). If you need to debug the tests
+  # then enable this and nested KVM to work around the crash (see above).
+  enableVBoxUART ? false
 }:
 
+assert use64bitGuest -> useKvmNestedVirt;
+assert enableVBoxUART -> useKvmNestedVirt; # VirtualBox bug, see above
+
 with import ../lib/testing.nix { inherit system pkgs; };
 with pkgs.lib;
 
@@ -94,7 +111,7 @@ let
 
   testVM = vmName: vmScript: let
     cfg = (import ../lib/eval-config.nix {
-      system = "i686-linux";
+      system = if use64bitGuest then "x86_64-linux" else "i686-linux";
       modules = [
         ../modules/profiles/minimal.nix
         (testVMConfig vmName vmScript)
@@ -141,13 +158,15 @@ let
     sharePath = "/home/alice/vboxshare-${name}";
 
     createFlags = mkFlags [
-      "--ostype Linux26"
+      "--ostype ${if use64bitGuest then "Linux26_64" else "Linux26"}"
       "--register"
     ];
 
-    vmFlags = mkFlags ([
-      "--uart1 0x3F8 4"
-      "--uartmode1 client /run/virtualbox-log-${name}.sock"
+    vmFlags = mkFlags (
+      (optionals enableVBoxUART [
+        "--uart1 0x3F8 4"
+        "--uartmode1 client /run/virtualbox-log-${name}.sock"
+      ]) ++ [
       "--memory 768"
       "--audio none"
     ] ++ (attrs.vmFlags or []));
@@ -180,7 +199,7 @@ let
     ];
   in {
     machine = {
-      systemd.sockets."vboxtestlog-${name}" = {
+      systemd.sockets."vboxtestlog-${name}" = mkIf enableVBoxUART {
         description = "VirtualBox Test Machine Log Socket For ${name}";
         wantedBy = [ "sockets.target" ];
         before = [ "multi-user.target" ];
@@ -188,7 +207,7 @@ let
         socketConfig.Accept = true;
       };
 
-      systemd.services."vboxtestlog-${name}@" = {
+      systemd.services."vboxtestlog-${name}@" = mkIf enableVBoxUART {
         description = "VirtualBox Test Machine Log For ${name}";
         serviceConfig.StandardInput = "socket";
         serviceConfig.StandardOutput = "syslog";
@@ -346,6 +365,8 @@ let
         vmConfigs = mapAttrsToList mkVMConf vms;
       in [ ./common/user-account.nix ./common/x11.nix ] ++ vmConfigs;
       virtualisation.memorySize = 2048;
+      virtualisation.qemu.options =
+        if useKvmNestedVirt then ["-cpu" "kvm64,vmx=on"] else [];
       virtualisation.virtualbox.host.enable = true;
       services.xserver.displayManager.auto.user = "alice";
       users.users.alice.extraGroups = let
@@ -412,9 +433,14 @@ in mapAttrs (mkVBoxTest false vboxVMs) {
     );
     $machine->sleep(5);
     $machine->screenshot("gui_manager_started");
+    # Home to select Tools, down to move to the VM, enter to start it.
+    $machine->sendKeys("home");
+    $machine->sendKeys("down");
     $machine->sendKeys("ret");
     $machine->screenshot("gui_manager_sent_startup");
     waitForStartup_simple (sub {
+      $machine->sendKeys("home");
+      $machine->sendKeys("down");
       $machine->sendKeys("ret");
     });
     $machine->screenshot("gui_started");