about summary refs log tree commit diff
path: root/nixos/modules
diff options
context:
space:
mode:
authorMatthew Bauer <mjbauer95@gmail.com>2019-02-09 12:14:06 -0500
committerMatthew Bauer <mjbauer95@gmail.com>2019-02-09 12:14:06 -0500
commit5c09d977c794d9243ddac17f6c429b5432431f8f (patch)
tree1acbd7e7f0c01546d955d47c6a8fddfec8e632b6 /nixos/modules
parent21d991b1fd78214023551a0dada17b129cbd5cd5 (diff)
parent18d059a4ac001fbaa9c2abc750a2830a52e1dae5 (diff)
downloadnixlib-5c09d977c794d9243ddac17f6c429b5432431f8f.tar
nixlib-5c09d977c794d9243ddac17f6c429b5432431f8f.tar.gz
nixlib-5c09d977c794d9243ddac17f6c429b5432431f8f.tar.bz2
nixlib-5c09d977c794d9243ddac17f6c429b5432431f8f.tar.lz
nixlib-5c09d977c794d9243ddac17f6c429b5432431f8f.tar.xz
nixlib-5c09d977c794d9243ddac17f6c429b5432431f8f.tar.zst
nixlib-5c09d977c794d9243ddac17f6c429b5432431f8f.zip
Merge remote-tracking branch 'origin/master' into staging
Diffstat (limited to 'nixos/modules')
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix6
-rw-r--r--nixos/modules/installer/tools/nixos-generate-config.pl4
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/programs/iotop.nix17
-rw-r--r--nixos/modules/programs/sway-beta.nix3
-rw-r--r--nixos/modules/services/games/minecraft-server.nix201
-rw-r--r--nixos/modules/services/mail/roundcube.nix20
-rw-r--r--nixos/modules/services/misc/docker-registry.nix2
-rw-r--r--nixos/modules/services/misc/gitlab.nix7
-rw-r--r--nixos/modules/services/misc/home-assistant.nix81
-rw-r--r--nixos/modules/services/misc/matrix-synapse.nix11
-rw-r--r--nixos/modules/services/misc/redmine.nix37
-rw-r--r--nixos/modules/services/misc/weechat.xml4
-rw-r--r--nixos/modules/services/monitoring/grafana.nix184
-rw-r--r--nixos/modules/services/monitoring/munin.nix209
-rw-r--r--nixos/modules/services/networking/ndppd.nix170
-rw-r--r--nixos/modules/services/networking/syncthing.nix2
-rw-r--r--nixos/modules/services/networking/unifi.nix3
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix2
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix35
-rw-r--r--nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix2
-rw-r--r--nixos/modules/tasks/auto-upgrade.nix2
-rw-r--r--nixos/modules/tasks/encrypted-devices.nix6
-rw-r--r--nixos/modules/tasks/filesystems.nix2
24 files changed, 914 insertions, 97 deletions
diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index 9475da23b1ff..e78e290e7438 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -339,11 +339,11 @@ let
     #   dates (cp -p, touch, mcopy -m, faketime for label), IDs (mkfs.vfat -i)
     ''
       mkdir ./contents && cd ./contents
-      cp -rp "${efiDir}"/* .
+      cp -rp "${efiDir}"/EFI .
       mkdir ./boot
       cp -p "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}" \
         "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}" ./boot/
-      touch --date=@0 ./*
+      touch --date=@0 ./EFI ./boot
 
       usage_size=$(du -sb --apparent-size . | tr -cd '[:digit:]')
       # Make the image 110% as big as the files need to make up for FAT overhead
@@ -355,7 +355,7 @@ let
       echo "Image size: $image_size"
       truncate --size=$image_size "$out"
       ${pkgs.libfaketime}/bin/faketime "2000-01-01 00:00:00" ${pkgs.dosfstools}/sbin/mkfs.vfat -i 12345678 -n EFIBOOT "$out"
-      mcopy -psvm -i "$out" ./* ::
+      mcopy -psvm -i "$out" ./EFI ./boot ::
       # Verify the FAT partition.
       ${pkgs.dosfstools}/sbin/fsck.vfat -vn "$out"
     ''; # */
diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl
index e58392ad05b4..3bcf90258d79 100644
--- a/nixos/modules/installer/tools/nixos-generate-config.pl
+++ b/nixos/modules/installer/tools/nixos-generate-config.pl
@@ -340,6 +340,8 @@ foreach my $fs (read_file("/proc/self/mountinfo")) {
     chomp $fs;
     my @fields = split / /, $fs;
     my $mountPoint = $fields[4];
+    $mountPoint =~ s/\\040/ /g; # account for mount points with spaces in the name (\040 is the escape character)
+    $mountPoint =~ s/\\011/\t/g; # account for mount points with tabs in the name (\011 is the escape character)
     next unless -d $mountPoint;
     my @mountOptions = split /,/, $fields[5];
 
@@ -355,6 +357,8 @@ foreach my $fs (read_file("/proc/self/mountinfo")) {
     my $fsType = $fields[$n];
     my $device = $fields[$n + 1];
     my @superOptions = split /,/, $fields[$n + 2];
+    $device =~ s/\\040/ /g; # account for devices with spaces in the name (\040 is the escape character)
+    $device =~ s/\\011/\t/g; # account for mount points with tabs in the name (\011 is the escape character)
 
     # Skip the read-only bind-mount on /nix/store.
     next if $mountPoint eq "/nix/store" && (grep { $_ eq "rw" } @superOptions) && (grep { $_ eq "ro" } @mountOptions);
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 915281cf863e..003b9df95d72 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -101,6 +101,7 @@
   ./programs/gnupg.nix
   ./programs/gphoto2.nix
   ./programs/iftop.nix
+  ./programs/iotop.nix
   ./programs/java.nix
   ./programs/kbdlight.nix
   ./programs/less.nix
diff --git a/nixos/modules/programs/iotop.nix b/nixos/modules/programs/iotop.nix
new file mode 100644
index 000000000000..5512dbc62f72
--- /dev/null
+++ b/nixos/modules/programs/iotop.nix
@@ -0,0 +1,17 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.iotop;
+in {
+  options = {
+    programs.iotop.enable = mkEnableOption "iotop + setcap wrapper";
+  };
+  config = mkIf cfg.enable {
+    security.wrappers.iotop = {
+      source = "${pkgs.iotop}/bin/iotop";
+      capabilities = "cap_net_admin+p";
+    };
+  };
+}
diff --git a/nixos/modules/programs/sway-beta.nix b/nixos/modules/programs/sway-beta.nix
index 7fc5979a38aa..3c235de0ce63 100644
--- a/nixos/modules/programs/sway-beta.nix
+++ b/nixos/modules/programs/sway-beta.nix
@@ -60,10 +60,11 @@ in {
     extraPackages = mkOption {
       type = with types; listOf package;
       default = with pkgs; [
+        swaylock swayidle
         xwayland rxvt_unicode dmenu
       ];
       defaultText = literalExample ''
-        with pkgs; [ xwayland rxvt_unicode dmenu ];
+        with pkgs; [ swaylock swayidle xwayland rxvt_unicode dmenu ];
       '';
       example = literalExample ''
         with pkgs; [
diff --git a/nixos/modules/services/games/minecraft-server.nix b/nixos/modules/services/games/minecraft-server.nix
index f50d2897843a..7d26d1501650 100644
--- a/nixos/modules/services/games/minecraft-server.nix
+++ b/nixos/modules/services/games/minecraft-server.nix
@@ -4,8 +4,41 @@ with lib;
 
 let
   cfg = config.services.minecraft-server;
-in
-{
+
+  # We don't allow eula=false anyways
+  eulaFile = builtins.toFile "eula.txt" ''
+    # eula.txt managed by NixOS Configuration
+    eula=true
+  '';
+
+  whitelistFile = pkgs.writeText "whitelist.json"
+    (builtins.toJSON
+      (mapAttrsToList (n: v: { name = n; uuid = v; }) cfg.whitelist));
+
+  cfgToString = v: if builtins.isBool v then boolToString v else toString v;
+
+  serverPropertiesFile = pkgs.writeText "server.properties" (''
+    # server.properties managed by NixOS configuration
+  '' + concatStringsSep "\n" (mapAttrsToList
+    (n: v: "${n}=${cfgToString v}") cfg.serverProperties));
+
+
+  # To be able to open the firewall, we need to read out port values in the
+  # server properties, but fall back to the defaults when those don't exist.
+  # These defaults are from https://minecraft.gamepedia.com/Server.properties#Java_Edition_3
+  defaultServerPort = 25565;
+
+  serverPort = cfg.serverProperties.server-port or defaultServerPort;
+
+  rconPort = if cfg.serverProperties.enable-rcon or false
+    then cfg.serverProperties."rcon.port" or 25575
+    else null;
+
+  queryPort = if cfg.serverProperties.enable-query or false
+    then cfg.serverProperties."query.port" or 25565
+    else null;
+
+in {
   options = {
     services.minecraft-server = {
 
@@ -13,10 +46,32 @@ in
         type = types.bool;
         default = false;
         description = ''
-          If enabled, start a Minecraft Server. The listening port for
-          the server is always <literal>25565</literal>. The server
+          If enabled, start a Minecraft Server. The server
           data will be loaded from and saved to
-          <literal>${cfg.dataDir}</literal>.
+          <option>services.minecraft-server.dataDir</option>.
+        '';
+      };
+
+      declarative = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to use a declarative Minecraft server configuration.
+          Only if set to <literal>true</literal>, the options
+          <option>services.minecraft-server.whitelist</option> and
+          <option>services.minecraft-server.serverProperties</option> will be
+          applied.
+        '';
+      };
+
+      eula = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether you agree to
+          <link xlink:href="https://account.mojang.com/documents/minecraft_eula">
+          Mojangs EULA</link>. This option must be set to
+          <literal>true</literal> to run Minecraft server.
         '';
       };
 
@@ -24,7 +79,7 @@ in
         type = types.path;
         default = "/var/lib/minecraft";
         description = ''
-          Directory to store minecraft database and other state/data files.
+          Directory to store Minecraft database and other state/data files.
         '';
       };
 
@@ -32,21 +87,84 @@ in
         type = types.bool;
         default = false;
         description = ''
-          Whether to open ports in the firewall (if enabled) for the server.
+          Whether to open ports in the firewall for the server.
+        '';
+      };
+
+      whitelist = mkOption {
+        type = let
+          minecraftUUID = types.strMatching
+            "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" // {
+              description = "Minecraft UUID";
+            };
+          in types.attrsOf minecraftUUID;
+        default = {};
+        description = ''
+          Whitelisted players, only has an effect when
+          <option>services.minecraft-server.declarative</option> is
+          <literal>true</literal> and the whitelist is enabled
+          via <option>services.minecraft-server.serverProperties</option> by
+          setting <literal>white-list</literal> to <literal>true</literal>.
+          This is a mapping from Minecraft usernames to UUIDs.
+          You can use <link xlink:href="https://mcuuid.net/"/> to get a
+          Minecraft UUID for a username.
+        '';
+        example = literalExample ''
+          {
+            username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+            username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
+          };
+        '';
+      };
+
+      serverProperties = mkOption {
+        type = with types; attrsOf (either bool (either int str));
+        default = {};
+        example = literalExample ''
+          {
+            server-port = 43000;
+            difficulty = 3;
+            gamemode = 1;
+            max-players = 5;
+            motd = "NixOS Minecraft server!";
+            white-list = true;
+            enable-rcon = true;
+            "rcon.password" = "hunter2";
+          }
+        '';
+        description = ''
+          Minecraft server properties for the server.properties file. Only has
+          an effect when <option>services.minecraft-server.declarative</option>
+          is set to <literal>true</literal>. See
+          <link xlink:href="https://minecraft.gamepedia.com/Server.properties#Java_Edition_3"/>
+          for documentation on these values.
         '';
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.minecraft-server;
+        defaultText = "pkgs.minecraft-server";
+        example = literalExample "pkgs.minecraft-server_1_12_2";
+        description = "Version of minecraft-server to run.";
+      };
+
       jvmOpts = mkOption {
-        type = types.str;
+        type = types.separatedString " ";
         default = "-Xmx2048M -Xms2048M";
-        description = "JVM options for the Minecraft Service.";
+        # Example options from https://minecraft.gamepedia.com/Tutorials/Server_startup_script
+        example = "-Xmx2048M -Xms4092M -XX:+UseG1GC -XX:+CMSIncrementalPacing "
+          + "-XX:+CMSClassUnloadingEnabled -XX:ParallelGCThreads=2 "
+          + "-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10";
+        description = "JVM options for the Minecraft server.";
       };
     };
   };
 
   config = mkIf cfg.enable {
+
     users.users.minecraft = {
-      description     = "Minecraft Server Service user";
+      description     = "Minecraft server service user";
       home            = cfg.dataDir;
       createHome      = true;
       uid             = config.ids.uids.minecraft;
@@ -57,17 +175,60 @@ in
       wantedBy      = [ "multi-user.target" ];
       after         = [ "network.target" ];
 
-      serviceConfig.Restart = "always";
-      serviceConfig.User    = "minecraft";
-      script = ''
-        cd ${cfg.dataDir}
-        exec ${pkgs.minecraft-server}/bin/minecraft-server ${cfg.jvmOpts}
-      '';
-    };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/minecraft-server ${cfg.jvmOpts}";
+        Restart = "always";
+        User = "minecraft";
+        WorkingDirectory = cfg.dataDir;
+      };
+
+      preStart = ''
+        ln -sf ${eulaFile} eula.txt
+      '' + (if cfg.declarative then ''
+
+        if [ -e .declarative ]; then
+
+          # Was declarative before, no need to back up anything
+          ln -sf ${whitelistFile} whitelist.json
+          cp -f ${serverPropertiesFile} server.properties
+
+        else
 
-    networking.firewall = mkIf cfg.openFirewall {
-      allowedUDPPorts = [ 25565 ];
-      allowedTCPPorts = [ 25565 ];
+          # Declarative for the first time, backup stateful files
+          ln -sb --suffix=.stateful ${whitelistFile} whitelist.json
+          cp -b --suffix=.stateful ${serverPropertiesFile} server.properties
+
+          # server.properties must have write permissions, because every time
+          # the server starts it first parses the file and then regenerates it..
+          chmod +w server.properties
+          echo "Autogenerated file that signifies that this server configuration is managed declaratively by NixOS" \
+            > .declarative
+
+        fi
+      '' else ''
+        if [ -e .declarative ]; then
+          rm .declarative
+        fi
+      '');
     };
+
+    networking.firewall = mkIf cfg.openFirewall (if cfg.declarative then {
+      allowedUDPPorts = [ serverPort ];
+      allowedTCPPorts = [ serverPort ]
+        ++ optional (! isNull queryPort) queryPort
+        ++ optional (! isNull rconPort) rconPort;
+    } else {
+      allowedUDPPorts = [ defaultServerPort ];
+      allowedTCPPorts = [ defaultServerPort ];
+    });
+
+    assertions = [
+      { assertion = cfg.eula;
+        message = "You must agree to Mojangs EULA to run minecraft-server."
+          + " Read https://account.mojang.com/documents/minecraft_eula and"
+          + " set `services.minecraft-server.eula` to `true` if you agree.";
+      }
+    ];
+
   };
 }
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index 6d81c7374f4d..66b1c1e3e6f9 100644
--- a/nixos/modules/services/mail/roundcube.nix
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -25,6 +25,20 @@ in
       description = "Hostname to use for the nginx vhost";
     };
 
+    package = mkOption {
+      type = types.package;
+      default = pkgs.roundcube;
+
+      example = literalExample ''
+        roundcube.withPlugins (plugins: [ plugins.persistent_login ])
+      '';
+
+      description = ''
+        The package which contains roundcube's sources. Can be overriden to create
+        an environment which contains roundcube and third-party plugins.
+      '';
+    };
+
     database = {
       username = mkOption {
         type = types.str;
@@ -86,7 +100,7 @@ in
           forceSSL = mkDefault true;
           enableACME = mkDefault true;
           locations."/" = {
-            root = pkgs.roundcube;
+            root = cfg.package;
             index = "index.php";
             extraConfig = ''
               location ~* \.php$ {
@@ -140,12 +154,12 @@ in
             ${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql postgres -c "create database ${cfg.database.dbname} with owner ${cfg.database.username}";
           fi
           PGPASSWORD=${cfg.database.password} ${pkgs.postgresql}/bin/psql -U ${cfg.database.username} \
-            -f ${pkgs.roundcube}/SQL/postgres.initial.sql \
+            -f ${cfg.package}/SQL/postgres.initial.sql \
             -h ${cfg.database.host} ${cfg.database.dbname}
           touch /var/lib/roundcube/db-created
         fi
 
-        ${pkgs.php}/bin/php ${pkgs.roundcube}/bin/update.sh
+        ${pkgs.php}/bin/php ${cfg.package}/bin/update.sh
       '';
       serviceConfig.Type = "oneshot";
     };
diff --git a/nixos/modules/services/misc/docker-registry.nix b/nixos/modules/services/misc/docker-registry.nix
index 9a3966ab30aa..f3d90e532c88 100644
--- a/nixos/modules/services/misc/docker-registry.nix
+++ b/nixos/modules/services/misc/docker-registry.nix
@@ -18,7 +18,7 @@ let
       delete.enabled = cfg.enableDelete;
     };
     http = {
-      addr = ":${builtins.toString cfg.port}";
+      addr = "${cfg.listenAddress}:${builtins.toString cfg.port}";
       headers.X-Content-Type-Options = ["nosniff"];
     };
     health.storagedriver = {
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index 769a9526cf64..25c258ebe134 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -497,7 +497,12 @@ in {
     systemd.services.gitaly = {
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
-      path = with pkgs; [ gitAndTools.git cfg.packages.gitaly.rubyEnv cfg.packages.gitaly.rubyEnv.wrappedRuby ];
+      path = with pkgs; [
+        openssh
+        gitAndTools.git
+        cfg.packages.gitaly.rubyEnv
+        cfg.packages.gitaly.rubyEnv.wrappedRuby
+      ];
       serviceConfig = {
         Type = "simple";
         User = cfg.user;
diff --git a/nixos/modules/services/misc/home-assistant.nix b/nixos/modules/services/misc/home-assistant.nix
index 2e9aa33aeeee..4ccfa22c89e7 100644
--- a/nixos/modules/services/misc/home-assistant.nix
+++ b/nixos/modules/services/misc/home-assistant.nix
@@ -6,9 +6,18 @@ let
   cfg = config.services.home-assistant;
 
   # cfg.config != null can be assumed here
-  configFile = pkgs.writeText "configuration.json"
+  configJSON = pkgs.writeText "configuration.json"
     (builtins.toJSON (if cfg.applyDefaultConfig then
-    (lib.recursiveUpdate defaultConfig cfg.config) else cfg.config));
+    (recursiveUpdate defaultConfig cfg.config) else cfg.config));
+  configFile = pkgs.runCommand "configuration.yaml" { } ''
+    ${pkgs.remarshal}/bin/json2yaml -i ${configJSON} -o $out
+  '';
+
+  lovelaceConfigJSON = pkgs.writeText "ui-lovelace.json"
+    (builtins.toJSON cfg.lovelaceConfig);
+  lovelaceConfigFile = pkgs.runCommand "ui-lovelace.yaml" { } ''
+    ${pkgs.remarshal}/bin/json2yaml -i ${lovelaceConfigJSON} -o $out
+  '';
 
   availableComponents = pkgs.home-assistant.availableComponents;
 
@@ -44,7 +53,9 @@ let
   # If you are changing this, please update the description in applyDefaultConfig
   defaultConfig = {
     homeassistant.time_zone = config.time.timeZone;
-    http.server_port = (toString cfg.port);
+    http.server_port = cfg.port;
+  } // optionalAttrs (cfg.lovelaceConfig != null) {
+    lovelace.mode = "yaml";
   };
 
 in {
@@ -99,6 +110,53 @@ in {
       '';
     };
 
+    configWritable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to make <filename>configuration.yaml</filename> writable.
+        This only has an effect if <option>config</option> is set.
+        This will allow you to edit it from Home Assistant's web interface.
+        However, bear in mind that it will be overwritten at every start of the service.
+      '';
+    };
+
+    lovelaceConfig = mkOption {
+      default = null;
+      type = with types; nullOr attrs;
+      # from https://www.home-assistant.io/lovelace/yaml-mode/
+      example = literalExample ''
+        {
+          title = "My Awesome Home";
+          views = [ {
+            title = "Example";
+            cards = [ {
+              type = "markdown";
+              title = "Lovelace";
+              content = "Welcome to your **Lovelace UI**.";
+            } ];
+          } ];
+        }
+      '';
+      description = ''
+        Your <filename>ui-lovelace.yaml</filename> as a Nix attribute set.
+        Setting this option will automatically add
+        <literal>lovelace.mode = "yaml";</literal> to your <option>config</option>.
+        Beware that setting this option will delete your previous <filename>ui-lovelace.yaml</filename>
+      '';
+    };
+
+    lovelaceConfigWritable = mkOption {
+      default = false;
+      type = types.bool;
+      description = ''
+        Whether to make <filename>ui-lovelace.yaml</filename> writable.
+        This only has an effect if <option>lovelaceConfig</option> is set.
+        This will allow you to edit it from Home Assistant's web interface.
+        However, bear in mind that it will be overwritten at every start of the service.
+      '';
+    };
+
     package = mkOption {
       default = pkgs.home-assistant;
       defaultText = "pkgs.home-assistant";
@@ -144,12 +202,17 @@ in {
     systemd.services.home-assistant = {
       description = "Home Assistant";
       after = [ "network.target" ];
-      preStart = lib.optionalString (cfg.config != null) ''
-        config=${cfg.configDir}/configuration.yaml
-        rm -f $config
-        ${pkgs.remarshal}/bin/json2yaml -i ${configFile} -o $config
-        chmod 444 $config
-      '';
+      preStart = optionalString (cfg.config != null) (if cfg.configWritable then ''
+        cp --no-preserve=mode ${configFile} "${cfg.configDir}/configuration.yaml"
+      '' else ''
+        rm -f "${cfg.configDir}/configuration.yaml"
+        ln -s ${configFile} "${cfg.configDir}/configuration.yaml"
+      '') + optionalString (cfg.lovelaceConfig != null) (if cfg.lovelaceConfigWritable then ''
+        cp --no-preserve=mode ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
+      '' else ''
+        rm -f "${cfg.configDir}/ui-lovelace.yaml"
+        ln -s ${lovelaceConfigFile} "${cfg.configDir}/ui-lovelace.yaml"
+      '');
       serviceConfig = {
         ExecStart = "${package}/bin/hass --config '${cfg.configDir}'";
         User = "hass";
diff --git a/nixos/modules/services/misc/matrix-synapse.nix b/nixos/modules/services/misc/matrix-synapse.nix
index 18e13f6ac030..a01e34d73629 100644
--- a/nixos/modules/services/misc/matrix-synapse.nix
+++ b/nixos/modules/services/misc/matrix-synapse.nix
@@ -651,12 +651,16 @@ in {
 
     services.postgresql.enable = mkIf usePostgresql (mkDefault true);
 
-    systemd.services.matrix-synapse = {
+    systemd.services.matrix-synapse =
+    let
+      python = (pkgs.python3.withPackages (ps: with ps; [ (ps.toPythonModule cfg.package) ]));
+    in
+    {
       description = "Synapse Matrix homeserver";
       after = [ "network.target" "postgresql.service" ];
       wantedBy = [ "multi-user.target" ];
       preStart = ''
-        ${cfg.package}/bin/homeserver \
+        ${python.interpreter} -m synapse.app.homeserver \
           --config-path ${configFile} \
           --keys-directory ${cfg.dataDir} \
           --generate-keys
@@ -687,10 +691,11 @@ in {
         WorkingDirectory = cfg.dataDir;
         PermissionsStartOnly = true;
         ExecStart = ''
-          ${cfg.package}/bin/homeserver \
+          ${python.interpreter} -m synapse.app.homeserver \
             ${ concatMapStringsSep "\n  " (x: "--config-path ${x} \\") ([ configFile ] ++ cfg.extraConfigFiles) }
             --keys-directory ${cfg.dataDir}
         '';
+        ExecReload = "${pkgs.utillinux}/bin/kill -HUP $MAINPID";
         Restart = "on-failure";
       };
     };
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 3c322ba1c3e6..98e9c8953c84 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -30,6 +30,13 @@ let
     ${cfg.extraConfig}
   '';
 
+  additionalEnvironment = pkgs.writeText "additional_environment.rb" ''
+    config.logger = Logger.new("${cfg.stateDir}/log/production.log", 14, 1048576)
+    config.logger.level = Logger::INFO
+
+    ${cfg.extraEnv}
+  '';
+
   unpackTheme = unpack "theme";
   unpackPlugin = unpack "plugin";
   unpack = id: (name: source:
@@ -54,12 +61,20 @@ in
         description = "Enable the Redmine service.";
       };
 
+      # default to the 4.x series not forcing major version upgrade of those on the 3.x series
       package = mkOption {
         type = types.package;
-        default = pkgs.redmine;
+        default = if versionAtLeast config.system.stateVersion "19.03"
+          then pkgs.redmine_4
+          else pkgs.redmine
+        ;
         defaultText = "pkgs.redmine";
-        description = "Which Redmine package to use.";
-        example = "pkgs.redmine.override { ruby = pkgs.ruby_2_3; }";
+        description = ''
+          Which Redmine package to use. This defaults to version 3.x if
+          <literal>system.stateVersion &lt; 19.03</literal> and version 4.x
+          otherwise.
+        '';
+        example = "pkgs.redmine_4.override { ruby = pkgs.ruby_2_4; }";
       };
 
       user = mkOption {
@@ -103,6 +118,19 @@ in
         '';
       };
 
+      extraEnv = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration in additional_environment.rb.
+
+          See https://svn.redmine.org/redmine/trunk/config/additional_environment.rb.example
+        '';
+        example = literalExample ''
+          config.logger.level = Logger::DEBUG
+        '';
+      };
+
       themes = mkOption {
         type = types.attrsOf types.path;
         default = {};
@@ -249,6 +277,9 @@ in
         # link in the application configuration
         ln -fs ${configurationYml} "${cfg.stateDir}/config/configuration.yml"
 
+        # link in the additional environment configuration
+        ln -fs ${additionalEnvironment} "${cfg.stateDir}/config/additional_environment.rb"
+
 
         # link in all user specified themes
         rm -rf "${cfg.stateDir}/public/themes/"*
diff --git a/nixos/modules/services/misc/weechat.xml b/nixos/modules/services/misc/weechat.xml
index b7f755bbc5c7..7255edfb9da3 100644
--- a/nixos/modules/services/misc/weechat.xml
+++ b/nixos/modules/services/misc/weechat.xml
@@ -8,7 +8,7 @@
   <link xlink:href="https://weechat.org/">WeeChat</link> is a fast and
   extensible IRC client.
  </para>
- <section>
+ <section xml:id="module-services-weechat-basic-usage">
   <title>Basic Usage</title>
 
   <para>
@@ -35,7 +35,7 @@
    in the state directory <literal>/var/lib/weechat</literal>.
   </para>
  </section>
- <section>
+ <section xml:id="module-services-weechat-reattach">
   <title>Re-attaching to WeeChat</title>
 
   <para>
diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix
index 5fb3e3771221..85879cfe0b33 100644
--- a/nixos/modules/services/monitoring/grafana.nix
+++ b/nixos/modules/services/monitoring/grafana.nix
@@ -50,6 +50,158 @@ let
     SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
   } // cfg.extraOptions;
 
+  datasourceConfiguration = {
+    apiVersion = 1;
+    datasources = cfg.provision.datasources;
+  };
+
+  datasourceFile = pkgs.writeText "datasource.yaml" (builtins.toJSON datasourceConfiguration);
+
+  dashboardConfiguration = {
+    apiVersion = 1;
+    providers = cfg.provision.dashboards;
+  };
+
+  dashboardFile = pkgs.writeText "dashboard.yaml" (builtins.toJSON dashboardConfiguration);
+
+  provisionConfDir =  pkgs.runCommand "grafana-provisioning" { } ''
+    mkdir -p $out/{datasources,dashboards}
+    ln -sf ${datasourceFile} $out/datasources/datasource.yaml
+    ln -sf ${dashboardFile} $out/dashboards/dashboard.yaml
+  '';
+
+  # Get a submodule without any embedded metadata:
+  _filter = x: filterAttrs (k: v: k != "_module") x;
+
+  # http://docs.grafana.org/administration/provisioning/#datasources
+  grafanaTypes.datasourceConfig = types.submodule {
+    options = {
+      name = mkOption {
+        type = types.str;
+        description = "Name of the datasource. Required";
+      };
+      type = mkOption {
+        type = types.enum ["graphite" "prometheus" "cloudwatch" "elasticsearch" "influxdb" "opentsdb" "mysql" "mssql" "postgres" "loki"];
+        description = "Datasource type. Required";
+      };
+      access = mkOption {
+        type = types.enum ["proxy" "direct"];
+        default = "proxy";
+        description = "Access mode. proxy or direct (Server or Browser in the UI). Required";
+      };
+      orgId = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Org id. will default to orgId 1 if not specified";
+      };
+      url = mkOption {
+        type = types.str;
+        description = "Url of the datasource";
+      };
+      password = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Database password, if used";
+      };
+      user = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Database user, if used";
+      };
+      database = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Database name, if used";
+      };
+      basicAuth = mkOption {
+        type = types.nullOr types.bool;
+        default = null;
+        description = "Enable/disable basic auth";
+      };
+      basicAuthUser = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Basic auth username";
+      };
+      basicAuthPassword = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "Basic auth password";
+      };
+      withCredentials = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable/disable with credentials headers";
+      };
+      isDefault = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Mark as default datasource. Max one per org";
+      };
+      jsonData = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = "Datasource specific configuration";
+      };
+      secureJsonData = mkOption {
+        type = types.nullOr types.attrs;
+        default = null;
+        description = "Datasource specific secure configuration";
+      };
+      version = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Version";
+      };
+      editable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Allow users to edit datasources from the UI.";
+      };
+    };
+  };
+
+  # http://docs.grafana.org/administration/provisioning/#dashboards
+  grafanaTypes.dashboardConfig = types.submodule {
+    options = {
+      name = mkOption {
+        type = types.str;
+        default = "default";
+        description = "Provider name";
+      };
+      orgId = mkOption {
+        type = types.int;
+        default = 1;
+        description = "Organization ID";
+      };
+      folder = mkOption {
+        type = types.str;
+        default = "";
+        description = "Add dashboards to the speciied folder";
+      };
+      type = mkOption {
+        type = types.str;
+        default = "file";
+        description = "Dashboard provider type";
+      };
+      disableDeletion = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Disable deletion when JSON file is removed";
+      };
+      updateIntervalSeconds = mkOption {
+        type = types.int;
+        default = 10;
+        description = "How often Grafana will scan for changed dashboards";
+      };
+      options = {
+        path = mkOption {
+          type = types.path;
+          description = "Path grafana will watch for dashboards";
+        };
+      };
+    };
+  };
 in {
   options.services.grafana = {
     enable = mkEnableOption "grafana";
@@ -175,6 +327,22 @@ in {
       };
     };
 
+    provision = {
+      enable = mkEnableOption "provision";
+      datasources = mkOption {
+        description = "Grafana datasources configuration";
+        default = [];
+        type = types.listOf grafanaTypes.datasourceConfig;
+        apply = x: map _filter x;
+      };
+      dashboards = mkOption {
+        description = "Grafana dashboard configuration";
+        default = [];
+        type = types.listOf grafanaTypes.dashboardConfig;
+        apply = x: map _filter x;
+      };
+    };
+
     security = {
       adminUser = mkOption {
         description = "Default admin username.";
@@ -313,10 +481,15 @@ in {
   };
 
   config = mkIf cfg.enable {
-    warnings = optional (
-      cfg.database.password != opt.database.password.default ||
-      cfg.security.adminPassword != opt.security.adminPassword.default
-    ) "Grafana passwords will be stored as plaintext in the Nix store!";
+    warnings = flatten [
+      (optional (
+        cfg.database.password != opt.database.password.default ||
+        cfg.security.adminPassword != opt.security.adminPassword.default
+      ) "Grafana passwords will be stored as plaintext in the Nix store!")
+      (optional (
+        any (x: x.password != null || x.basicAuthPassword != null || x.secureJsonData != null) cfg.provision.datasources
+      ) "Datasource passwords will be stored as plaintext in the Nix store!")
+    ];
 
     environment.systemPackages = [ cfg.package ];
 
@@ -359,6 +532,9 @@ in {
         ${optionalString (cfg.smtp.passwordFile != null) ''
           export GF_SMTP_PASSWORD="$(cat ${escapeShellArg cfg.smtp.passwordFile})"
         ''}
+        ${optionalString cfg.provision.enable ''
+          export GF_PATHS_PROVISIONING=${provisionConfDir};
+        ''}
         exec ${cfg.package.bin}/bin/grafana-server -homepath ${cfg.dataDir}
       '';
       serviceConfig = {
diff --git a/nixos/modules/services/monitoring/munin.nix b/nixos/modules/services/monitoring/munin.nix
index 2b265d5b5a90..f67986327245 100644
--- a/nixos/modules/services/monitoring/munin.nix
+++ b/nixos/modules/services/monitoring/munin.nix
@@ -4,7 +4,7 @@
 # TODO: LWP/Pg perl libs aren't recognized
 
 # TODO: support fastcgi
-# http://munin-monitoring.org/wiki/CgiHowto2
+# http://guide.munin-monitoring.org/en/latest/example/webserver/apache-cgi.html
 # spawn-fcgi -s /run/munin/fastcgi-graph.sock -U www-data   -u munin -g munin /usr/lib/munin/cgi/munin-cgi-graph
 # spawn-fcgi -s /run/munin/fastcgi-html.sock  -U www-data   -u munin -g munin /usr/lib/munin/cgi/munin-cgi-html
 # https://paste.sh/vofcctHP#-KbDSXVeWoifYncZmLfZzgum
@@ -24,6 +24,8 @@ let
       logdir    /var/log/munin
       rundir    /run/munin
 
+      ${lib.optionalString (cronCfg.extraCSS != "") "staticdir ${customStaticDir}"}
+
       ${cronCfg.extraGlobalConfig}
 
       ${cronCfg.hosts}
@@ -63,6 +65,11 @@ let
       [ipmi*]
       user root
       group root
+
+      [munin*]
+      env.UPDATE_STATSFILE /var/lib/munin/munin-update.stats
+
+      ${nodeCfg.extraPluginConfig}
     '';
 
   pluginConfDir = pkgs.stdenv.mkDerivation {
@@ -72,6 +79,54 @@ let
       ln -s ${pluginConf} $out/nixos-config
     '';
   };
+
+  # Copy one Munin plugin into the Nix store with a specific name.
+  # This is suitable for use with plugins going directly into /etc/munin/plugins,
+  # i.e. munin.extraPlugins.
+  internOnePlugin = name: path:
+    "cp -a '${path}' '${name}'";
+
+  # Copy an entire tree of Munin plugins into a single directory in the Nix
+  # store, with no renaming.
+  # This is suitable for use with munin-node-configure --suggest, i.e.
+  # munin.extraAutoPlugins.
+  internManyPlugins = name: path:
+    "find '${path}' -type f -perm /a+x -exec cp -a -t . '{}' '+'";
+
+  # Use the appropriate intern-fn to copy the plugins into the store and patch
+  # them afterwards in an attempt to get them to run on NixOS.
+  internAndFixPlugins = name: intern-fn: paths:
+    pkgs.runCommand name {} ''
+      mkdir -p "$out"
+      cd "$out"
+      ${lib.concatStringsSep "\n"
+          (lib.attrsets.mapAttrsToList intern-fn paths)}
+      chmod -R u+w .
+      find . -type f -exec sed -E -i '
+        s,(/usr)?/s?bin/,/run/current-system/sw/bin/,g
+      ' '{}' '+'
+    '';
+
+  # TODO: write a derivation for munin-contrib, so that for contrib plugins
+  # you can just refer to them by name rather than needing to include a copy
+  # of munin-contrib in your nixos configuration.
+  extraPluginDir = internAndFixPlugins "munin-extra-plugins.d"
+    internOnePlugin nodeCfg.extraPlugins;
+
+  extraAutoPluginDir = internAndFixPlugins "munin-extra-auto-plugins.d"
+    internManyPlugins
+    (builtins.listToAttrs
+      (map
+        (path: { name = baseNameOf path; value = path; })
+        nodeCfg.extraAutoPlugins));
+
+  customStaticDir = pkgs.runCommand "munin-custom-static-data" {} ''
+    cp -a "${pkgs.munin}/etc/opt/munin/static" "$out"
+    cd "$out"
+    chmod -R u+w .
+    echo "${cronCfg.extraCSS}" >> style.css
+    echo "${cronCfg.extraCSS}" >> style-new.css
+  '';
 in
 
 {
@@ -82,11 +137,12 @@ in
 
       enable = mkOption {
         default = false;
+        type = types.bool;
         description = ''
           Enable Munin Node agent. Munin node listens on 0.0.0.0 and
           by default accepts connections only from 127.0.0.1 for security reasons.
 
-          See <link xlink:href='http://munin-monitoring.org/wiki/munin-node.conf' />.
+          See <link xlink:href='http://guide.munin-monitoring.org/en/latest/architecture/index.html' />.
         '';
       };
 
@@ -95,18 +151,108 @@ in
         type = types.lines;
         description = ''
           <filename>munin-node.conf</filename> extra configuration. See
-          <link xlink:href='http://munin-monitoring.org/wiki/munin-node.conf' />
+          <link xlink:href='http://guide.munin-monitoring.org/en/latest/reference/munin-node.conf.html' />
+        '';
+      };
+
+      extraPluginConfig = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          <filename>plugin-conf.d</filename> extra plugin configuration. See
+          <link xlink:href='http://guide.munin-monitoring.org/en/latest/plugin/use.html' />
+        '';
+        example = ''
+          [fail2ban_*]
+          user root
         '';
       };
 
-      # TODO: add option to add additional plugins
+      extraPlugins = mkOption {
+        default = {};
+        type = with types; attrsOf path;
+        description = ''
+          Additional Munin plugins to activate. Keys are the name of the plugin
+          symlink, values are the path to the underlying plugin script. You
+          can use the same plugin script multiple times (e.g. for wildcard
+          plugins).
+
+          Note that these plugins do not participate in autoconfiguration. If
+          you want to autoconfigure additional plugins, use
+          <option>services.munin-node.extraAutoPlugins</option>.
+
+          Plugins enabled in this manner take precedence over autoconfigured
+          plugins.
+
+          Plugins will be copied into the Nix store, and it will attempt to
+          modify them to run properly by fixing hardcoded references to
+          <literal>/bin</literal>, <literal>/usr/bin</literal>,
+          <literal>/sbin</literal>, and <literal>/usr/sbin</literal>.
+        '';
+        example = literalExample ''
+          {
+            zfs_usage_bigpool = /src/munin-contrib/plugins/zfs/zfs_usage_;
+            zfs_usage_smallpool = /src/munin-contrib/plugins/zfs/zfs_usage_;
+            zfs_list = /src/munin-contrib/plugins/zfs/zfs_list;
+          };
+        '';
+      };
 
+      extraAutoPlugins = mkOption {
+        default = [];
+        type = with types; listOf path;
+        description = ''
+          Additional Munin plugins to autoconfigure, using
+          <literal>munin-node-configure --suggest</literal>. These should be
+          the actual paths to the plugin files (or directories containing them),
+          not just their names.
+
+          If you want to manually enable individual plugins instead, use
+          <option>services.munin-node.extraPlugins</option>.
+
+          Note that only plugins that have the 'autoconfig' capability will do
+          anything if listed here, since plugins that cannot autoconfigure
+          won't be automatically enabled by
+          <literal>munin-node-configure</literal>.
+
+          Plugins will be copied into the Nix store, and it will attempt to
+          modify them to run properly by fixing hardcoded references to
+          <literal>/bin</literal>, <literal>/usr/bin</literal>,
+          <literal>/sbin</literal>, and <literal>/usr/sbin</literal>.
+        '';
+        example = literalExample ''
+          [
+            /src/munin-contrib/plugins/zfs
+            /src/munin-contrib/plugins/ssh
+          ];
+        '';
+      };
+
+      disabledPlugins = mkOption {
+        # TODO: figure out why Munin isn't writing the log file and fix it.
+        # In the meantime this at least suppresses a useless graph full of
+        # NaNs in the output.
+        default = [ "munin_stats" ];
+        type = with types; listOf string;
+        description = ''
+          Munin plugins to disable, even if
+          <literal>munin-node-configure --suggest</literal> tries to enable
+          them. To disable a wildcard plugin, use an actual wildcard, as in
+          the example.
+
+          munin_stats is disabled by default as it tries to read
+          <literal>/var/log/munin/munin-update.log</literal> for timing
+          information, and the NixOS build of Munin does not write this file.
+        '';
+        example = [ "diskstats" "zfs_usage_*" ];
+      };
     };
 
     services.munin-cron = {
 
       enable = mkOption {
         default = false;
+        type = types.bool;
         description = ''
           Enable munin-cron. Takes care of all heavy lifting to collect data from
           nodes and draws graphs to html. Runs munin-update, munin-limits,
@@ -119,11 +265,12 @@ in
 
       extraGlobalConfig = mkOption {
         default = "";
+        type = types.lines;
         description = ''
           <filename>munin.conf</filename> extra global configuration.
-          See <link xlink:href='http://munin-monitoring.org/wiki/munin.conf' />.
+          See <link xlink:href='http://guide.munin-monitoring.org/en/latest/reference/munin.conf.html' />.
           Useful to setup notifications, see
-          <link xlink:href='http://munin-monitoring.org/wiki/HowToContact' />
+          <link xlink:href='http://guide.munin-monitoring.org/en/latest/tutorial/alert.html' />
         '';
         example = ''
           contact.email.command mail -s "Munin notification for ''${var:host}" someone@example.com
@@ -131,14 +278,34 @@ in
       };
 
       hosts = mkOption {
+        default = "";
+        type = types.lines;
+        description = ''
+          Definitions of hosts of nodes to collect data from. Needs at least one
+          host for cron to succeed. See
+          <link xlink:href='http://guide.munin-monitoring.org/en/latest/reference/munin.conf.html' />
+        '';
         example = ''
           [''${config.networking.hostName}]
           address localhost
         '';
+      };
+
+      extraCSS = mkOption {
+        default = "";
+        type = types.lines;
         description = ''
-          Definitions of hosts of nodes to collect data from. Needs at least one
-          hosts for cron to succeed. See
-          <link xlink:href='http://munin-monitoring.org/wiki/munin.conf' />
+          Custom styling for the HTML that munin-cron generates. This will be
+          appended to the CSS files used by munin-cron and will thus take
+          precedence over the builtin styles.
+        '';
+        example = ''
+          /* A simple dark theme. */
+          html, body { background: #222222; }
+          #header, #footer { background: #333333; }
+          img.i, img.iwarn, img.icrit, img.iunkn {
+            filter: invert(100%) hue-rotate(-30deg);
+          }
         '';
       };
 
@@ -155,6 +322,7 @@ in
       description = "Munin monitoring user";
       group = "munin";
       uid = config.ids.uids.munin;
+      home = "/var/lib/munin";
     }];
 
     users.groups = [{
@@ -173,14 +341,27 @@ in
       environment.MUNIN_PLUGSTATE = "/run/munin";
       environment.MUNIN_LOGDIR = "/var/log/munin";
       preStart = ''
-        echo "updating munin plugins..."
+        echo "Updating munin plugins..."
 
         mkdir -p /etc/munin/plugins
         rm -rf /etc/munin/plugins/*
+
+        # Autoconfigure builtin plugins
         ${pkgs.munin}/bin/munin-node-configure --suggest --shell --families contrib,auto,manual --config ${nodeConf} --libdir=${pkgs.munin}/lib/plugins --servicedir=/etc/munin/plugins --sconfdir=${pluginConfDir} 2>/dev/null | ${pkgs.bash}/bin/bash
 
-        # NOTE: we disable disktstats because plugin seems to fail and it hangs html generation (100% CPU + memory leak)
-        rm /etc/munin/plugins/diskstats || true
+        # Autoconfigure extra plugins
+        ${pkgs.munin}/bin/munin-node-configure --suggest --shell --families contrib,auto,manual --config ${nodeConf} --libdir=${extraAutoPluginDir} --servicedir=/etc/munin/plugins --sconfdir=${pluginConfDir} 2>/dev/null | ${pkgs.bash}/bin/bash
+
+        ${lib.optionalString (nodeCfg.extraPlugins != {}) ''
+            # Link in manually enabled plugins
+            ln -f -s -t /etc/munin/plugins ${extraPluginDir}/*
+          ''}
+
+        ${lib.optionalString (nodeCfg.disabledPlugins != []) ''
+            # Disable plugins
+            cd /etc/munin/plugins
+            rm -f ${toString nodeCfg.disabledPlugins}
+          ''}
       '';
       serviceConfig = {
         ExecStart = "${pkgs.munin}/sbin/munin-node --config ${nodeConf} --servicedir /etc/munin/plugins/ --sconfdir=${pluginConfDir}";
@@ -192,6 +373,10 @@ in
 
   }) (mkIf cronCfg.enable {
 
+    # Munin is hardcoded to use DejaVu Mono and the graphs come out wrong if
+    # it's not available.
+    fonts.fonts = [ pkgs.dejavu_fonts ];
+
     systemd.timers.munin-cron = {
       description = "batch Munin master programs";
       wantedBy = [ "timers.target" ];
diff --git a/nixos/modules/services/networking/ndppd.nix b/nixos/modules/services/networking/ndppd.nix
index 1d6c48dd8d37..ba17f1ba825a 100644
--- a/nixos/modules/services/networking/ndppd.nix
+++ b/nixos/modules/services/networking/ndppd.nix
@@ -5,43 +5,163 @@ with lib;
 let
   cfg = config.services.ndppd;
 
-  configFile = pkgs.runCommand "ndppd.conf" {} ''
-    substitute ${pkgs.ndppd}/etc/ndppd.conf $out \
-      --replace eth0 ${cfg.interface} \
-      --replace 1111:: ${cfg.network}
-  '';
-in {
-  options = {
-    services.ndppd = {
-      enable = mkEnableOption "daemon that proxies NDP (Neighbor Discovery Protocol) messages between interfaces";
+  render = s: f: concatStringsSep "\n" (mapAttrsToList f s);
+  prefer = a: b: if a != null then a else b;
+
+  ndppdConf = prefer cfg.configFile (pkgs.writeText "ndppd.conf" ''
+    route-ttl ${toString cfg.routeTTL}
+    ${render cfg.proxies (proxyInterfaceName: proxy: ''
+    proxy ${prefer proxy.interface proxyInterfaceName} {
+      router ${boolToString proxy.router}
+      timeout ${toString proxy.timeout}
+      ttl ${toString proxy.ttl}
+      ${render proxy.rules (ruleNetworkName: rule: ''
+      rule ${prefer rule.network ruleNetworkName} {
+        ${rule.method}${if rule.method == "iface" then " ${rule.interface}" else ""}
+      }'')}
+    }'')}
+  '');
+
+  proxy = types.submodule {
+    options = {
       interface = mkOption {
-        type = types.string;
-        default = "eth0";
-        example = "ens3";
-        description = "Interface which is on link-level with router.";
+        type = types.nullOr types.str;
+        description = ''
+          Listen for any Neighbor Solicitation messages on this interface,
+          and respond to them according to a set of rules.
+          Defaults to the name of the attrset.
+        '';
+        default = null;
+      };
+      router = mkOption {
+        type = types.bool;
+        description = ''
+          Turns on or off the router flag for Neighbor Advertisement Messages.
+        '';
+        default = true;
+      };
+      timeout = mkOption {
+        type = types.int;
+        description = ''
+          Controls how long to wait for a Neighbor Advertisment Message before 
+          invalidating the entry, in milliseconds.
+        '';
+        default = 500;
+      };
+      ttl = mkOption {
+        type = types.int;
+        description = ''
+          Controls how long a valid or invalid entry remains in the cache, in 
+          milliseconds.
+        '';
+        default = 30000;
       };
+      rules = mkOption {
+        type = types.attrsOf rule;
+        description = ''
+          This is a rule that the target address is to match against. If no netmask
+          is provided, /128 is assumed. You may have several rule sections, and the
+          addresses may or may not overlap.
+        '';
+        default = {};
+      };
+    };
+  };
+
+  rule = types.submodule {
+    options = {
       network = mkOption {
-        type = types.string;
-        default = "1111::";
-        example = "2001:DB8::/32";
-        description = "Network that we proxy.";
+        type = types.nullOr types.str;
+        description = ''
+          This is the target address is to match against. If no netmask
+          is provided, /128 is assumed. The addresses of serveral rules
+          may or may not overlap.
+          Defaults to the name of the attrset.
+        '';
+        default = null;
+      };
+      method = mkOption {
+        type = types.enum [ "static" "iface" "auto" ];
+        description = ''
+          static: Immediately answer any Neighbor Solicitation Messages
+            (if they match the IP rule).
+          iface: Forward the Neighbor Solicitation Message through the specified
+            interface and only respond if a matching Neighbor Advertisement
+            Message is received.
+          auto: Same as iface, but instead of manually specifying the outgoing
+            interface, check for a matching route in /proc/net/ipv6_route.
+        '';
+        default = "auto";
       };
-      configFile = mkOption {
-        type = types.nullOr types.path;
+      interface = mkOption {
+        type = types.nullOr types.str;
+        description = "Interface to use when method is iface.";
         default = null;
-        description = "Path to configuration file.";
       };
     };
   };
 
+in {
+  options.services.ndppd = {
+    enable = mkEnableOption "daemon that proxies NDP (Neighbor Discovery Protocol) messages between interfaces";
+    interface = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        Interface which is on link-level with router.
+        (Legacy option, use services.ndppd.proxies.&lt;interface&gt;.rules.&lt;network&gt; instead)
+      '';
+      default = null;
+      example = "eth0";
+    };
+    network = mkOption {
+      type = types.nullOr types.str;
+      description = ''
+        Network that we proxy.
+        (Legacy option, use services.ndppd.proxies.&lt;interface&gt;.rules.&lt;network&gt; instead)
+      '';
+      default = null;
+      example = "1111::/64";
+    };
+    configFile = mkOption {
+      type = types.nullOr types.path;
+      description = "Path to configuration file.";
+      default = null;
+    };
+    routeTTL = mkOption {
+      type = types.int;
+      description = ''
+        This tells 'ndppd' how often to reload the route file /proc/net/ipv6_route,
+        in milliseconds.
+      '';
+      default = 30000;
+    };
+    proxies = mkOption {
+      type = types.attrsOf proxy;
+      description = ''
+        This sets up a listener, that will listen for any Neighbor Solicitation
+        messages, and respond to them according to a set of rules.
+      '';
+      default = {};
+      example = { "eth0".rules."1111::/64" = {}; };
+    };
+  };
+
   config = mkIf cfg.enable {
-    systemd.packages = [ pkgs.ndppd ];
-    environment.etc."ndppd.conf".source = if (cfg.configFile != null) then cfg.configFile else configFile;
+    warnings = mkIf (cfg.interface != null && cfg.network != null) [ ''
+      The options services.ndppd.interface and services.ndppd.network will probably be removed soon,
+      please use services.ndppd.proxies.<interface>.rules.<network> instead.
+    '' ];
+
+    services.ndppd.proxies = mkIf (cfg.interface != null && cfg.network != null) {
+      "${cfg.interface}".rules."${cfg.network}" = {};
+    };
+
     systemd.services.ndppd = {
-      serviceConfig.RuntimeDirectory = [ "ndppd" ];
+      description = "NDP Proxy Daemon";
+      documentation = [ "man:ndppd(1)" "man:ndppd.conf(5)" ];
+      after = [ "network-pre.target" ];
       wantedBy = [ "multi-user.target" ];
+      serviceConfig.ExecStart = "${pkgs.ndppd}/bin/ndppd -c ${ndppdConf}";
     };
   };
-
-  meta.maintainers = with maintainers; [ gnidorah ];
 }
diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
index b2ef1885a955..702481ec5177 100644
--- a/nixos/modules/services/networking/syncthing.nix
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -122,7 +122,7 @@ in {
 
     systemd.packages = [ pkgs.syncthing ];
 
-    users = mkIf (cfg.user == defaultUser) {
+    users = mkIf (cfg.systemService && cfg.user == defaultUser) {
       users."${defaultUser}" =
         { group = cfg.group;
           home  = cfg.dataDir;
diff --git a/nixos/modules/services/networking/unifi.nix b/nixos/modules/services/networking/unifi.nix
index 89b9ac4eadf5..c82e0af2803d 100644
--- a/nixos/modules/services/networking/unifi.nix
+++ b/nixos/modules/services/networking/unifi.nix
@@ -121,11 +121,12 @@ in
     };
 
     networking.firewall = mkIf cfg.openPorts {
-      # https://help.ubnt.com/hc/en-us/articles/204910084-UniFi-Change-Default-Ports-for-Controller-and-UAPs
+      # https://help.ubnt.com/hc/en-us/articles/218506997
       allowedTCPPorts = [
         8080  # Port for UAP to inform controller.
         8880  # Port for HTTP portal redirect, if guest portal is enabled.
         8843  # Port for HTTPS portal redirect, ditto.
+        6789  # Port for UniFi mobile speed test.
       ];
       allowedUDPPorts = [
         3478  # UDP port used for STUN.
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index ecb1c5615d58..ee1354d6a997 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -426,7 +426,7 @@ in {
               "~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+)\\.php(?:$|/)" = {
                 priority = 500;
                 extraConfig = ''
-                  include ${pkgs.nginxMainline}/conf/fastcgi.conf;
+                  include ${config.services.nginx.package}/conf/fastcgi.conf;
                   fastcgi_split_path_info ^(.+\.php)(/.*)$;
                   fastcgi_param PATH_INFO $fastcgi_path_info;
                   fastcgi_param HTTPS ${if cfg.https then "on" else "off"};
diff --git a/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix b/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
index d1ee076e9185..efec943c0075 100644
--- a/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm-greeters/gtk.nix
@@ -6,12 +6,14 @@ let
 
   dmcfg = config.services.xserver.displayManager;
   ldmcfg = dmcfg.lightdm;
+  xcfg = config.services.xserver;
   cfg = ldmcfg.greeters.gtk;
 
   inherit (pkgs) writeText;
 
   theme = cfg.theme.package;
   icons = cfg.iconTheme.package;
+  cursors = cfg.cursorTheme.package;
 
   # The default greeter provided with this expression is the GTK greeter.
   # Again, we need a few things in the environment for the greeter to run with
@@ -28,7 +30,8 @@ let
         --set GTK_EXE_PREFIX "${theme}" \
         --set GTK_DATA_PREFIX "${theme}" \
         --set XDG_DATA_DIRS "${theme}/share:${icons}/share" \
-        --set XDG_CONFIG_HOME "${theme}/share"
+        --set XDG_CONFIG_HOME "${theme}/share" \
+        --set XCURSOR_PATH "${cursors}/share/icons"
 
       cat - > $out/lightdm-gtk-greeter.desktop << EOF
       [Desktop Entry]
@@ -44,9 +47,12 @@ let
     [greeter]
     theme-name = ${cfg.theme.name}
     icon-theme-name = ${cfg.iconTheme.name}
+    cursor-theme-name = ${cfg.cursorTheme.name}
+    cursor-theme-size = ${toString cfg.cursorTheme.size}
     background = ${ldmcfg.background}
     ${optionalString (cfg.clock-format != null) "clock-format = ${cfg.clock-format}"}
     ${optionalString (cfg.indicators != null) "indicators = ${concatStringsSep ";" cfg.indicators}"}
+    ${optionalString (xcfg.dpi != null) "xft-dpi=${toString xcfg.dpi}"}
     ${cfg.extraConfig}
     '';
 
@@ -106,6 +112,33 @@ in
 
       };
 
+      cursorTheme = {
+
+        package = mkOption {
+          default = pkgs.gnome3.defaultIconTheme;
+          defaultText = "pkgs.gnome3.defaultIconTheme";
+          description = ''
+            The package path that contains the cursor theme given in the name option.
+          '';
+        };
+
+        name = mkOption {
+          type = types.str;
+          default = "Adwaita";
+          description = ''
+            Name of the cursor theme to use for the lightdm-gtk-greeter.
+          '';
+        };
+
+        size = mkOption {
+          type = types.int;
+          default = 16;
+          description = ''
+            Size of the cursor theme to use for the lightdm-gtk-greeter.
+          '';
+        };
+      };
+
       clock-format = mkOption {
         type = types.nullOr types.str;
         default = null;
diff --git a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
index 047651dc6426..7db60daa60b8 100644
--- a/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
+++ b/nixos/modules/system/boot/loader/raspberrypi/raspberrypi.nix
@@ -33,7 +33,7 @@ let
       avoid_warnings=1
     '' + optional isAarch64 ''
       # Boot in 64-bit mode.
-      arm_control=0x200
+      arm_64bit=1
     '' + (if cfg.uboot.enable then ''
       kernel=u-boot-rpi.bin
     '' else ''
diff --git a/nixos/modules/tasks/auto-upgrade.nix b/nixos/modules/tasks/auto-upgrade.nix
index 7b756b70e2fc..d225778a3878 100644
--- a/nixos/modules/tasks/auto-upgrade.nix
+++ b/nixos/modules/tasks/auto-upgrade.nix
@@ -78,7 +78,7 @@ let cfg = config.system.autoUpgrade; in
           HOME = "/root";
         } // config.networking.proxy.envVars;
 
-      path = [ pkgs.gnutar pkgs.xz.bin config.nix.package.out ];
+      path = [ pkgs.gnutar pkgs.xz.bin pkgs.gitMinimal config.nix.package.out ];
 
       script = ''
         ${config.system.build.nixos-rebuild}/bin/nixos-rebuild switch ${toString cfg.flags}
diff --git a/nixos/modules/tasks/encrypted-devices.nix b/nixos/modules/tasks/encrypted-devices.nix
index 2ffbb8777068..2c9231f55236 100644
--- a/nixos/modules/tasks/encrypted-devices.nix
+++ b/nixos/modules/tasks/encrypted-devices.nix
@@ -19,21 +19,21 @@ let
         description = "The block device is backed by an encrypted one, adds this device as a initrd luks entry.";
       };
 
-      options.blkDev = mkOption {
+      blkDev = mkOption {
         default = null;
         example = "/dev/sda1";
         type = types.nullOr types.str;
         description = "Location of the backing encrypted device.";
       };
 
-      options.label = mkOption {
+      label = mkOption {
         default = null;
         example = "rootfs";
         type = types.nullOr types.str;
         description = "Label of the unlocked encrypted device. Set <literal>fileSystems.&lt;name?&gt;.device</literal> to <literal>/dev/mapper/&lt;label&gt;</literal> to mount the unlocked device.";
       };
 
-      options.keyFile = mkOption {
+      keyFile = mkOption {
         default = null;
         example = "/mnt-root/root/.swapkey";
         type = types.nullOr types.str;
diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix
index 9e4057b50897..07f8214cea2c 100644
--- a/nixos/modules/tasks/filesystems.nix
+++ b/nixos/modules/tasks/filesystems.nix
@@ -231,7 +231,7 @@ in
         fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "vboxsf" "glusterfs" ];
         skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck;
         # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces
-        escape = string: builtins.replaceStrings [ " " ] [ "\\040" ] string;
+        escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string;
       in ''
         # This is a generated file.  Do not edit!
         #