about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorJan Tojnar <jtojnar@gmail.com>2023-08-16 19:37:11 +0200
committerJan Tojnar <jtojnar@gmail.com>2023-08-16 19:37:11 +0200
commit86797b2008511751bfb4e9abf085d8bdea1d547f (patch)
treefc3b05e823892f3a59d9c88df9d3612e3e4e07d9 /nixos
parent1dcd19866c8608e7fc1f54cc3d84542f19c65503 (diff)
parentec1e6d834ae803f02a353914843e096c2091d233 (diff)
downloadnixlib-86797b2008511751bfb4e9abf085d8bdea1d547f.tar
nixlib-86797b2008511751bfb4e9abf085d8bdea1d547f.tar.gz
nixlib-86797b2008511751bfb4e9abf085d8bdea1d547f.tar.bz2
nixlib-86797b2008511751bfb4e9abf085d8bdea1d547f.tar.lz
nixlib-86797b2008511751bfb4e9abf085d8bdea1d547f.tar.xz
nixlib-86797b2008511751bfb4e9abf085d8bdea1d547f.tar.zst
nixlib-86797b2008511751bfb4e9abf085d8bdea1d547f.zip
Merge branch 'staging-next' into staging
Conflicts:
- pkgs/development/libraries/qt-6/default.nix
  Merge a5b92645f1e6762e4b53f48652cb766184d84e77 and 0597d865ef4f763f3fed54702b29ce328d28e2b4
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/administration/service-mgmt.chapter.md30
-rw-r--r--nixos/modules/services/networking/haproxy.nix17
-rw-r--r--nixos/modules/services/security/kanidm.nix14
-rw-r--r--nixos/modules/services/video/mediamtx.nix53
-rw-r--r--nixos/modules/services/web-apps/invidious.nix64
-rw-r--r--nixos/modules/services/x11/picom.nix6
-rwxr-xr-xnixos/modules/system/activation/switch-to-configuration.pl58
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/mediamtx.nix57
-rw-r--r--nixos/tests/prometheus-exporters.nix2
-rw-r--r--nixos/tests/switch-test.nix162
-rw-r--r--nixos/tests/web-servers/agate.nix2
12 files changed, 368 insertions, 98 deletions
diff --git a/nixos/doc/manual/administration/service-mgmt.chapter.md b/nixos/doc/manual/administration/service-mgmt.chapter.md
index 674c73741680..bc9bdbe3708b 100644
--- a/nixos/doc/manual/administration/service-mgmt.chapter.md
+++ b/nixos/doc/manual/administration/service-mgmt.chapter.md
@@ -118,3 +118,33 @@ the symlink, and this path is in `/nix/store/.../lib/systemd/user/`.
 Hence [garbage collection](#sec-nix-gc) will remove that file and you
 will wind up with a broken symlink in your systemd configuration, which
 in turn will not make the service / timer start on login.
+
+## Template units {#sect-nixos-systemd-template-units}
+
+systemd supports templated units where a base unit can be started multiple
+times with a different parameter. The syntax to accomplish this is
+`service-name@instance-name.service`. Units get the instance name passed to
+them (see `systemd.unit(5)`). NixOS has support for these kinds of units and
+for template-specific overrides. A service needs to be defined twice, once
+for the base unit and once for the instance. All instances must include
+`overrideStrategy = "asDropin"` for the change detection to work. This
+example illustrates this:
+```nix
+{
+  systemd.services = {
+    "base-unit@".serviceConfig = {
+      ExecStart = "...";
+      User = "...";
+    };
+    "base-unit@instance-a" = {
+      overrideStrategy = "asDropin"; # needed for templates to work
+      wantedBy = [ "multi-user.target" ]; # causes NixOS to manage the instance
+    };
+    "base-unit@instance-b" = {
+      overrideStrategy = "asDropin"; # needed for templates to work
+      wantedBy = [ "multi-user.target" ]; # causes NixOS to manage the instance
+      serviceConfig.User = "root"; # also override something for this specific instance
+    };
+  };
+}
+```
diff --git a/nixos/modules/services/networking/haproxy.nix b/nixos/modules/services/networking/haproxy.nix
index e0b686434b6e..208eb356d629 100644
--- a/nixos/modules/services/networking/haproxy.nix
+++ b/nixos/modules/services/networking/haproxy.nix
@@ -17,14 +17,9 @@ with lib;
   options = {
     services.haproxy = {
 
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to enable HAProxy, the reliable, high performance TCP/HTTP
-          load balancer.
-        '';
-      };
+      enable = mkEnableOption (lib.mdDoc "HAProxy, the reliable, high performance TCP/HTTP load balancer.");
+
+      package = mkPackageOptionMD pkgs "haproxy" { };
 
       user = mkOption {
         type = types.str;
@@ -70,15 +65,15 @@ with lib;
         ExecStartPre = [
           # when the master process receives USR2, it reloads itself using exec(argv[0]),
           # so we create a symlink there and update it before reloading
-          "${pkgs.coreutils}/bin/ln -sf ${pkgs.haproxy}/sbin/haproxy /run/haproxy/haproxy"
+          "${pkgs.coreutils}/bin/ln -sf ${lib.getExe cfg.package} /run/haproxy/haproxy"
           # when running the config test, don't be quiet so we can see what goes wrong
           "/run/haproxy/haproxy -c -f ${haproxyCfg}"
         ];
         ExecStart = "/run/haproxy/haproxy -Ws -f /etc/haproxy.cfg -p /run/haproxy/haproxy.pid";
         # support reloading
         ExecReload = [
-          "${pkgs.haproxy}/sbin/haproxy -c -f ${haproxyCfg}"
-          "${pkgs.coreutils}/bin/ln -sf ${pkgs.haproxy}/sbin/haproxy /run/haproxy/haproxy"
+          "${lib.getExe cfg.package} -c -f ${haproxyCfg}"
+          "${pkgs.coreutils}/bin/ln -sf ${lib.getExe cfg.package} /run/haproxy/haproxy"
           "${pkgs.coreutils}/bin/kill -USR2 $MAINPID"
         ];
         KillMode = "mixed";
diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix
index 6fb9f71a489e..d8a99dee59f4 100644
--- a/nixos/modules/services/security/kanidm.nix
+++ b/nixos/modules/services/security/kanidm.nix
@@ -69,6 +69,8 @@ in
     enableServer = lib.mkEnableOption (lib.mdDoc "the Kanidm server");
     enablePam = lib.mkEnableOption (lib.mdDoc "the Kanidm PAM and NSS integration");
 
+    package = lib.mkPackageOptionMD pkgs "kanidm" {};
+
     serverSettings = lib.mkOption {
       type = lib.types.submodule {
         freeformType = settingsFormat.type;
@@ -222,7 +224,7 @@ in
         }
       ];
 
-    environment.systemPackages = lib.mkIf cfg.enableClient [ pkgs.kanidm ];
+    environment.systemPackages = lib.mkIf cfg.enableClient [ cfg.package ];
 
     systemd.services.kanidm = lib.mkIf cfg.enableServer {
       description = "kanidm identity management daemon";
@@ -237,7 +239,7 @@ in
           StateDirectory = "kanidm";
           StateDirectoryMode = "0700";
           RuntimeDirectory = "kanidmd";
-          ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}";
+          ExecStart = "${cfg.package}/bin/kanidmd server -c ${serverConfigFile}";
           User = "kanidm";
           Group = "kanidm";
 
@@ -270,7 +272,7 @@ in
           CacheDirectory = "kanidm-unixd";
           CacheDirectoryMode = "0700";
           RuntimeDirectory = "kanidm-unixd";
-          ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd";
+          ExecStart = "${cfg.package}/bin/kanidm_unixd";
           User = "kanidm-unixd";
           Group = "kanidm-unixd";
 
@@ -302,7 +304,7 @@ in
       partOf = [ "kanidm-unixd.service" ];
       restartTriggers = [ unixConfigFile clientConfigFile ];
       serviceConfig = {
-        ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd_tasks";
+        ExecStart = "${cfg.package}/bin/kanidm_unixd_tasks";
 
         BindReadOnlyPaths = [
           "/nix/store"
@@ -346,7 +348,7 @@ in
       })
     ];
 
-    system.nssModules = lib.mkIf cfg.enablePam [ pkgs.kanidm ];
+    system.nssModules = lib.mkIf cfg.enablePam [ cfg.package ];
 
     system.nssDatabases.group = lib.optional cfg.enablePam "kanidm";
     system.nssDatabases.passwd = lib.optional cfg.enablePam "kanidm";
@@ -365,7 +367,7 @@ in
           description = "Kanidm server";
           isSystemUser = true;
           group = "kanidm";
-          packages = with pkgs; [ kanidm ];
+          packages = [ cfg.package ];
         };
       })
       (lib.mkIf cfg.enablePam {
diff --git a/nixos/modules/services/video/mediamtx.nix b/nixos/modules/services/video/mediamtx.nix
index 18a9e3d5fe30..c3abd9cdcc5c 100644
--- a/nixos/modules/services/video/mediamtx.nix
+++ b/nixos/modules/services/video/mediamtx.nix
@@ -1,79 +1,66 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
   cfg = config.services.mediamtx;
-  package = pkgs.mediamtx;
   format = pkgs.formats.yaml {};
 in
 {
+  meta.maintainers = with lib.maintainers; [ fpletz ];
+
   options = {
     services.mediamtx = {
-      enable = mkEnableOption (lib.mdDoc "MediaMTX");
+      enable = lib.mkEnableOption (lib.mdDoc "MediaMTX");
 
-      settings = mkOption {
+      package = lib.mkPackageOptionMD pkgs "mediamtx" { };
+
+      settings = lib.mkOption {
         description = lib.mdDoc ''
-          Settings for MediaMTX.
-          Read more at <https://github.com/aler9/mediamtx/blob/main/mediamtx.yml>
+          Settings for MediaMTX. Refer to the defaults at
+          <https://github.com/bluenviron/mediamtx/blob/main/mediamtx.yml>.
         '';
         type = format.type;
-
-        default = {
-          logLevel = "info";
-          logDestinations = [
-            "stdout"
-          ];
-          # we set this so when the user uses it, it just works (see LogsDirectory below). but it's not used by default.
-          logFile = "/var/log/mediamtx/mediamtx.log";
-        };
-
+        default = {};
         example = {
           paths = {
             cam = {
-              runOnInit = "ffmpeg -f v4l2 -i /dev/video0 -f rtsp rtsp://localhost:$RTSP_PORT/$RTSP_PATH";
+              runOnInit = "\${lib.getExe pkgs.ffmpeg} -f v4l2 -i /dev/video0 -f rtsp rtsp://localhost:$RTSP_PORT/$RTSP_PATH";
               runOnInitRestart = true;
             };
           };
         };
       };
 
-      env = mkOption {
-        type = with types; attrsOf anything;
+      env = lib.mkOption {
+        type = with lib.types; attrsOf anything;
         description = lib.mdDoc "Extra environment variables for MediaMTX";
         default = {};
         example = {
           MTX_CONFKEY = "mykey";
         };
       };
+
+      allowVideoAccess = lib.mkEnableOption (lib.mdDoc ''
+        Enable access to video devices like cameras on the system.
+      '');
     };
   };
 
-  config = mkIf (cfg.enable) {
+  config = lib.mkIf cfg.enable {
     # NOTE: mediamtx watches this file and automatically reloads if it changes
     environment.etc."mediamtx.yaml".source = format.generate "mediamtx.yaml" cfg.settings;
 
     systemd.services.mediamtx = {
-      environment = cfg.env;
-
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
 
-      path = with pkgs; [
-        ffmpeg
-      ];
+      environment = cfg.env;
 
       serviceConfig = {
         DynamicUser = true;
         User = "mediamtx";
         Group = "mediamtx";
-
-        LogsDirectory = "mediamtx";
-
-        # user likely may want to stream cameras, can't hurt to add video group
-        SupplementaryGroups = "video";
-
-        ExecStart = "${package}/bin/mediamtx /etc/mediamtx.yaml";
+        SupplementaryGroups = lib.mkIf cfg.allowVideoAccess "video";
+        ExecStart = "${cfg.package}/bin/mediamtx /etc/mediamtx.yaml";
       };
     };
   };
diff --git a/nixos/modules/services/web-apps/invidious.nix b/nixos/modules/services/web-apps/invidious.nix
index 8823da010014..5603ef7392e8 100644
--- a/nixos/modules/services/web-apps/invidious.nix
+++ b/nixos/modules/services/web-apps/invidious.nix
@@ -7,6 +7,9 @@ let
 
   settingsFile = settingsFormat.generate "invidious-settings" cfg.settings;
 
+  generatedHmacKeyFile = "/var/lib/invidious/hmac_key";
+  generateHmac = cfg.hmacKeyFile == null;
+
   serviceConfig = {
     systemd.services.invidious = {
       description = "Invidious (An alternative YouTube front-end)";
@@ -14,22 +17,47 @@ let
       after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
 
-      script =
-        let
-          jqFilter = "."
-            + lib.optionalString (cfg.database.host != null) "[0].db.password = \"'\"'\"$(cat ${lib.escapeShellArg cfg.database.passwordFile})\"'\"'\""
-            + " | .[0]"
-            + lib.optionalString (cfg.extraSettingsFile != null) " * .[1]";
-          jqFiles = [ settingsFile ] ++ lib.optional (cfg.extraSettingsFile != null) cfg.extraSettingsFile;
-        in
-        ''
-          export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s "${jqFilter}" ${lib.escapeShellArgs jqFiles})"
-          exec ${cfg.package}/bin/invidious
-        '';
+      preStart = lib.optionalString generateHmac ''
+        if [[ ! -e "${generatedHmacKeyFile}" ]]; then
+          ${pkgs.pwgen}/bin/pwgen 20 1 > "${generatedHmacKeyFile}"
+          chmod 0600 "${generatedHmacKeyFile}"
+        fi
+      '';
+
+      script = ''
+        configParts=()
+      ''
+      # autogenerated hmac_key
+      + lib.optionalString generateHmac ''
+        configParts+=("$(${pkgs.jq}/bin/jq -R '{"hmac_key":.}' <"${generatedHmacKeyFile}")")
+      ''
+      # generated settings file
+      + ''
+        configParts+=("$(< ${lib.escapeShellArg settingsFile})")
+      ''
+      # optional database password file
+      + lib.optionalString (cfg.database.host != null) ''
+        configParts+=("$(${pkgs.jq}/bin/jq -R '{"db":{"password":.}}' ${lib.escapeShellArg cfg.database.passwordFile})")
+      ''
+      # optional extra settings file
+      + lib.optionalString (cfg.extraSettingsFile != null) ''
+        configParts+=("$(< ${lib.escapeShellArg cfg.extraSettingsFile})")
+      ''
+      # explicitly specified hmac key file
+      + lib.optionalString (cfg.hmacKeyFile != null) ''
+        configParts+=("$(< ${lib.escapeShellArg cfg.hmacKeyFile})")
+      ''
+      # merge all parts into a single configuration with later elements overriding previous elements
+      + ''
+        export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s 'reduce .[] as $item ({}; . * $item)' <<<"''${configParts[*]}")"
+        exec ${cfg.package}/bin/invidious
+      '';
 
       serviceConfig = {
         RestartSec = "2s";
         DynamicUser = true;
+        StateDirectory = "invidious";
+        StateDirectoryMode = "0750";
 
         CapabilityBoundingSet = "";
         PrivateDevices = true;
@@ -171,6 +199,18 @@ in
       '';
     };
 
+    hmacKeyFile = lib.mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = lib.mdDoc ''
+        A path to a file containing the `hmac_key`. If `null`, a key will be generated automatically on first
+        start.
+
+        If non-`null`, this option overrides any `hmac_key` specified in {option}`services.invidious.settings` or
+        via {option}`services.invidious.extraSettingsFile`.
+      '';
+    };
+
     extraSettingsFile = lib.mkOption {
       type = types.nullOr types.str;
       default = null;
diff --git a/nixos/modules/services/x11/picom.nix b/nixos/modules/services/x11/picom.nix
index 1d6f3daa4022..3df0ea9e60bb 100644
--- a/nixos/modules/services/x11/picom.nix
+++ b/nixos/modules/services/x11/picom.nix
@@ -61,6 +61,8 @@ in {
       '';
     };
 
+    package = mkPackageOptionMD pkgs "picom" { };
+
     fade = mkOption {
       type = types.bool;
       default = false;
@@ -301,13 +303,13 @@ in {
       };
 
       serviceConfig = {
-        ExecStart = "${pkgs.picom}/bin/picom --config ${configFile}";
+        ExecStart = "${getExe cfg.package} --config ${configFile}";
         RestartSec = 3;
         Restart = "always";
       };
     };
 
-    environment.systemPackages = [ pkgs.picom ];
+    environment.systemPackages = [ cfg.package ];
   };
 
   meta.maintainers = with lib.maintainers; [ rnhmjoj ];
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index cfad64039868..04d90968c4c1 100755
--- a/nixos/modules/system/activation/switch-to-configuration.pl
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -253,16 +253,24 @@ sub parse_systemd_ini {
 # If a directory with the same basename ending in .d exists next to the unit file, it will be
 # assumed to contain override files which will be parsed as well and handled properly.
 sub parse_unit {
-    my ($unit_path) = @_;
+    my ($unit_path, $base_unit_path) = @_;
 
     # Parse the main unit and all overrides
     my %unit_data;
     # Replace \ with \\ so glob() still works with units that have a \ in them
     # Valid characters in unit names are ASCII letters, digits, ":", "-", "_", ".", and "\"
+    $base_unit_path =~ s/\\/\\\\/gmsx;
     $unit_path =~ s/\\/\\\\/gmsx;
-    foreach (glob("${unit_path}{,.d/*.conf}")) {
+
+    foreach (glob("${base_unit_path}{,.d/*.conf}")) {
         parse_systemd_ini(\%unit_data, "$_")
     }
+    # Handle drop-in template-unit instance overrides
+    if ($unit_path ne $base_unit_path) {
+        foreach (glob("${unit_path}.d/*.conf")) {
+            parse_systemd_ini(\%unit_data, "$_")
+        }
+    }
     return %unit_data;
 }
 
@@ -423,7 +431,7 @@ sub compare_units { ## no critic(Subroutines::ProhibitExcessComplexity)
 # Called when a unit exists in both the old systemd and the new system and the units
 # differ. This figures out of what units are to be stopped, restarted, reloaded, started, and skipped.
 sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutines::ProhibitExcessComplexity)
-    my ($unit, $base_name, $new_unit_file, $new_unit_info, $active_cur, $units_to_stop, $units_to_start, $units_to_reload, $units_to_restart, $units_to_skip) = @_;
+    my ($unit, $base_name, $new_unit_file, $new_base_unit_file, $new_unit_info, $active_cur, $units_to_stop, $units_to_start, $units_to_reload, $units_to_restart, $units_to_skip) = @_;
 
     if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target" || $unit =~ /\.path$/msx || $unit =~ /\.slice$/msx) {
         # Do nothing.  These cannot be restarted directly.
@@ -442,7 +450,7 @@ sub handle_modified_unit { ## no critic(Subroutines::ProhibitManyArgs, Subroutin
         # Revert of the attempt: https://github.com/NixOS/nixpkgs/pull/147609
         # More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430
     } else {
-        my %new_unit_info = $new_unit_info ? %{$new_unit_info} : parse_unit($new_unit_file);
+        my %new_unit_info = $new_unit_info ? %{$new_unit_info} : parse_unit($new_unit_file, $new_base_unit_file);
         if (parse_systemd_bool(\%new_unit_info, "Service", "X-ReloadIfChanged", 0) and not $units_to_restart->{$unit} and not $units_to_stop->{$unit}) {
             $units_to_reload->{$unit} = 1;
             record_unit($reload_list_file, $unit);
@@ -538,31 +546,33 @@ my %units_to_filter; # units not shown
 
 my $active_cur = get_active_units();
 while (my ($unit, $state) = each(%{$active_cur})) {
-    my $base_unit = $unit;
+    my $cur_unit_file = "/etc/systemd/system/$unit";
+    my $new_unit_file = "$toplevel/etc/systemd/system/$unit";
 
-    my $cur_unit_file = "/etc/systemd/system/$base_unit";
-    my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
+    my $base_unit = $unit;
+    my $cur_base_unit_file = $cur_unit_file;
+    my $new_base_unit_file = $new_unit_file;
 
     # Detect template instances.
     if (!-e $cur_unit_file && !-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
       $base_unit = "$1\@.$2";
-      $cur_unit_file = "/etc/systemd/system/$base_unit";
-      $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
+      $cur_base_unit_file = "/etc/systemd/system/$base_unit";
+      $new_base_unit_file = "$toplevel/etc/systemd/system/$base_unit";
     }
 
     my $base_name = $base_unit;
     $base_name =~ s/\.[[:lower:]]*$//msx;
 
-    if (-e $cur_unit_file && ($state->{state} eq "active" || $state->{state} eq "activating")) {
-        if (! -e $new_unit_file || abs_path($new_unit_file) eq "/dev/null") {
-            my %cur_unit_info = parse_unit($cur_unit_file);
+    if (-e $cur_base_unit_file && ($state->{state} eq "active" || $state->{state} eq "activating")) {
+        if (! -e $new_base_unit_file || abs_path($new_base_unit_file) eq "/dev/null") {
+            my %cur_unit_info = parse_unit($cur_unit_file, $cur_base_unit_file);
             if (parse_systemd_bool(\%cur_unit_info, "Unit", "X-StopOnRemoval", 1)) {
                 $units_to_stop{$unit} = 1;
             }
         }
 
         elsif ($unit =~ /\.target$/msx) {
-            my %new_unit_info = parse_unit($new_unit_file);
+            my %new_unit_info = parse_unit($new_unit_file, $new_base_unit_file);
 
             # Cause all active target units to be restarted below.
             # This should start most changed units we stop here as
@@ -596,11 +606,11 @@ while (my ($unit, $state) = each(%{$active_cur})) {
         }
 
         else {
-            my %cur_unit_info = parse_unit($cur_unit_file);
-            my %new_unit_info = parse_unit($new_unit_file);
+            my %cur_unit_info = parse_unit($cur_unit_file, $cur_base_unit_file);
+            my %new_unit_info = parse_unit($new_unit_file, $new_base_unit_file);
             my $diff = compare_units(\%cur_unit_info, \%new_unit_info);
             if ($diff == 1) {
-                handle_modified_unit($unit, $base_name, $new_unit_file, \%new_unit_info, $active_cur, \%units_to_stop, \%units_to_start, \%units_to_reload, \%units_to_restart, \%units_to_skip);
+                handle_modified_unit($unit, $base_name, $new_unit_file, $new_base_unit_file, \%new_unit_info, $active_cur, \%units_to_stop, \%units_to_start, \%units_to_reload, \%units_to_restart, \%units_to_skip);
             } elsif ($diff == 2 and not $units_to_restart{$unit}) {
                 $units_to_reload{$unit} = 1;
                 record_unit($reload_list_file, $unit);
@@ -710,13 +720,14 @@ if ($action eq "dry-activate") {
     # Handle the activation script requesting the restart or reload of a unit.
     foreach (split(/\n/msx, read_file($dry_restart_by_activation_file, err_mode => "quiet") // "")) {
         my $unit = $_;
+        my $new_unit_file = "$toplevel/etc/systemd/system/$unit";
         my $base_unit = $unit;
-        my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
+        my $new_base_unit_file = $new_unit_file;
 
         # Detect template instances.
         if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
           $base_unit = "$1\@.$2";
-          $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
+          $new_base_unit_file = "$toplevel/etc/systemd/system/$base_unit";
         }
 
         my $base_name = $base_unit;
@@ -728,7 +739,7 @@ if ($action eq "dry-activate") {
             next;
         }
 
-        handle_modified_unit($unit, $base_name, $new_unit_file, undef, $active_cur, \%units_to_restart, \%units_to_restart, \%units_to_reload, \%units_to_restart, \%units_to_skip);
+        handle_modified_unit($unit, $base_name, $new_unit_file, $new_base_unit_file, undef, $active_cur, \%units_to_restart, \%units_to_restart, \%units_to_reload, \%units_to_restart, \%units_to_skip);
     }
     unlink($dry_restart_by_activation_file);
 
@@ -782,13 +793,14 @@ system("$out/activate", "$out") == 0 or $res = 2;
 # Handle the activation script requesting the restart or reload of a unit.
 foreach (split(/\n/msx, read_file($restart_by_activation_file, err_mode => "quiet") // "")) {
     my $unit = $_;
+    my $new_unit_file = "$toplevel/etc/systemd/system/$unit";
     my $base_unit = $unit;
-    my $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
+    my $new_base_unit_file = $new_unit_file;
 
     # Detect template instances.
     if (!-e $new_unit_file && $unit =~ /^(.*)@[^\.]*\.(.*)$/msx) {
       $base_unit = "$1\@.$2";
-      $new_unit_file = "$toplevel/etc/systemd/system/$base_unit";
+      $new_base_unit_file = "$toplevel/etc/systemd/system/$base_unit";
     }
 
     my $base_name = $base_unit;
@@ -801,7 +813,7 @@ foreach (split(/\n/msx, read_file($restart_by_activation_file, err_mode => "quie
         next;
     }
 
-    handle_modified_unit($unit, $base_name, $new_unit_file, undef, $active_cur, \%units_to_restart, \%units_to_restart, \%units_to_reload, \%units_to_restart, \%units_to_skip);
+    handle_modified_unit($unit, $base_name, $new_unit_file, $new_base_unit_file, undef, $active_cur, \%units_to_restart, \%units_to_restart, \%units_to_reload, \%units_to_restart, \%units_to_skip);
 }
 # We can remove the file now because it has been propagated to the other restart/reload files
 unlink($restart_by_activation_file);
@@ -859,7 +871,7 @@ if (scalar(keys(%units_to_reload)) > 0) {
     for my $unit (keys(%units_to_reload)) {
         if (!unit_is_active($unit)) {
             # Figure out if we need to start the unit
-            my %unit_info = parse_unit("$toplevel/etc/systemd/system/$unit");
+            my %unit_info = parse_unit("$toplevel/etc/systemd/system/$unit", "$toplevel/etc/systemd/system/$unit");
             if (!(parse_systemd_bool(\%unit_info, "Unit", "RefuseManualStart", 0) || parse_systemd_bool(\%unit_info, "Unit", "X-OnlyManualStart", 0))) {
                 $units_to_start{$unit} = 1;
                 record_unit($start_list_file, $unit);
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 530447b99786..a54047433bcd 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -463,6 +463,7 @@ in {
   matrix-conduit = handleTest ./matrix/conduit.nix {};
   matrix-synapse = handleTest ./matrix/synapse.nix {};
   mattermost = handleTest ./mattermost.nix {};
+  mediamtx = handleTest ./mediamtx.nix {};
   mediatomb = handleTest ./mediatomb.nix {};
   mediawiki = handleTest ./mediawiki.nix {};
   meilisearch = handleTest ./meilisearch.nix {};
diff --git a/nixos/tests/mediamtx.nix b/nixos/tests/mediamtx.nix
new file mode 100644
index 000000000000..8cacd02631d9
--- /dev/null
+++ b/nixos/tests/mediamtx.nix
@@ -0,0 +1,57 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} :
+
+{
+  name = "mediamtx";
+  meta.maintainers = with lib.maintainers; [ fpletz ];
+
+  nodes = {
+    machine = { config, ... }: {
+      services.mediamtx = {
+        enable = true;
+        settings = {
+          metrics = true;
+          paths.all.source = "publisher";
+        };
+      };
+
+      systemd.services.rtmp-publish = {
+        description = "Publish an RTMP stream to mediamtx";
+        after = [ "mediamtx.service" ];
+        bindsTo = [ "mediamtx.service" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          DynamicUser = true;
+          Restart = "on-failure";
+          RestartSec = "1s";
+          TimeoutStartSec = "10s";
+          ExecStart = "${lib.getBin pkgs.ffmpeg-headless}/bin/ffmpeg -re -f lavfi -i smptebars=size=800x600:rate=10 -c libx264 -f flv rtmp://localhost:1935/test";
+        };
+      };
+
+      systemd.services.rtmp-receive = {
+        description = "Receive an RTMP stream from mediamtx";
+        after = [ "rtmp-publish.service" ];
+        bindsTo = [ "rtmp-publish.service" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          DynamicUser = true;
+          Restart = "on-failure";
+          RestartSec = "1s";
+          TimeoutStartSec = "10s";
+          ExecStart = "${lib.getBin pkgs.ffmpeg-headless}/bin/ffmpeg -y -re -i rtmp://localhost:1935/test -f flv /dev/null";
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("mediamtx.service")
+    machine.wait_for_unit("rtmp-publish.service")
+    machine.wait_for_unit("rtmp-receive.service")
+    machine.wait_for_open_port(9998)
+    machine.succeed("curl http://localhost:9998/metrics | grep '^rtmp_conns.*state=\"publish\".*1$'")
+    machine.succeed("curl http://localhost:9998/metrics | grep '^rtmp_conns.*state=\"read\".*1$'")
+  '';
+})
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
index 64e2811beb06..d86f8ac634e8 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -1200,7 +1200,7 @@ let
       };
       exporterTest = ''
         wait_until_succeeds(
-            'journalctl -eu prometheus-smartctl-exporter.service -o cat | grep "Device unavailable"'
+            'journalctl -eu prometheus-smartctl-exporter.service -o cat | grep "Unable to detect device type"'
         )
       '';
     };
diff --git a/nixos/tests/switch-test.nix b/nixos/tests/switch-test.nix
index f44dede7fef4..53595ae7d3e2 100644
--- a/nixos/tests/switch-test.nix
+++ b/nixos/tests/switch-test.nix
@@ -1,6 +1,6 @@
 # Test configuration switching.
 
-import ./make-test-python.nix ({ pkgs, ...} : let
+import ./make-test-python.nix ({ lib, pkgs, ...} : let
 
   # Simple service that can either be socket-activated or that will
   # listen on port 1234 if not socket-activated.
@@ -279,6 +279,28 @@ in {
           systemd.services.test-service.unitConfig.RefuseManualStart = true;
         };
 
+        unitWithTemplate.configuration = {
+          systemd.services."instantiated@".serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+            ExecStart = "${pkgs.coreutils}/bin/true";
+            ExecReload = "${pkgs.coreutils}/bin/true";
+          };
+          systemd.services."instantiated@one" = {
+            wantedBy = [ "multi-user.target" ];
+            overrideStrategy = "asDropin";
+          };
+          systemd.services."instantiated@two" = {
+            wantedBy = [ "multi-user.target" ];
+            overrideStrategy = "asDropin";
+          };
+        };
+
+        unitWithTemplateModified.configuration = {
+          imports = [ unitWithTemplate.configuration ];
+          systemd.services."instantiated@".serviceConfig.X-Test = "test";
+        };
+
         restart-and-reload-by-activation-script.configuration = {
           systemd.services = rec {
             simple-service = {
@@ -290,29 +312,50 @@ in {
                 ExecReload = "${pkgs.coreutils}/bin/true";
               };
             };
+            "templated-simple-service@" = simple-service;
+            "templated-simple-service@instance".overrideStrategy = "asDropin";
 
             simple-restart-service = simple-service // {
               stopIfChanged = false;
             };
+            "templated-simple-restart-service@" = simple-restart-service;
+            "templated-simple-restart-service@instance".overrideStrategy = "asDropin";
 
             simple-reload-service = simple-service // {
               reloadIfChanged = true;
             };
+            "templated-simple-reload-service@" = simple-reload-service;
+            "templated-simple-reload-service@instance".overrideStrategy = "asDropin";
 
             no-restart-service = simple-service // {
               restartIfChanged = false;
             };
+            "templated-no-restart-service@" = no-restart-service;
+            "templated-no-restart-service@instance".overrideStrategy = "asDropin";
 
             reload-triggers = simple-service // {
               wantedBy = [ "multi-user.target" ];
             };
+            "templated-reload-triggers@" = simple-service;
+            "templated-reload-triggers@instance" = {
+              overrideStrategy = "asDropin";
+              wantedBy = [ "multi-user.target" ];
+            };
 
             reload-triggers-and-restart-by-as = simple-service;
+            "templated-reload-triggers-and-restart-by-as@" = reload-triggers-and-restart-by-as;
+            "templated-reload-triggers-and-restart-by-as@instance".overrideStrategy = "asDropin";
 
             reload-triggers-and-restart = simple-service // {
               stopIfChanged = false; # easier to check for this
               wantedBy = [ "multi-user.target" ];
             };
+            "templated-reload-triggers-and-restart@" = simple-service;
+            "templated-reload-triggers-and-restart@instance" = {
+              overrideStrategy = "asDropin";
+              stopIfChanged = false; # easier to check for this
+              wantedBy = [ "multi-user.target" ];
+            };
           };
 
           system.activationScripts.restart-and-reload-test = {
@@ -332,12 +375,20 @@ in {
               simple-reload-service.service
               no-restart-service.service
               reload-triggers-and-restart-by-as.service
+              templated-simple-service@instance.service
+              templated-simple-restart-service@instance.service
+              templated-simple-reload-service@instance.service
+              templated-no-restart-service@instance.service
+              templated-reload-triggers-and-restart-by-as@instance.service
               EOF
 
               cat <<EOF >> "$g"
               reload-triggers.service
               reload-triggers-and-restart-by-as.service
               reload-triggers-and-restart.service
+              templated-reload-triggers@instance.service
+              templated-reload-triggers-and-restart-by-as@instance.service
+              templated-reload-triggers-and-restart@instance.service
               EOF
             '';
           };
@@ -346,6 +397,10 @@ in {
         restart-and-reload-by-activation-script-modified.configuration = {
           imports = [ restart-and-reload-by-activation-script.configuration ];
           systemd.services.reload-triggers-and-restart.serviceConfig.X-Modified = "test";
+          systemd.services."templated-reload-triggers-and-restart@instance" = {
+            overrideStrategy = "asDropin";
+            serviceConfig.X-Modified = "test";
+          };
         };
 
         simple-socket.configuration = {
@@ -507,6 +562,10 @@ in {
       set -o pipefail
       exec env -i "$@" | tee /dev/stderr
     '';
+
+    # Returns a comma separated representation of the given list in sorted
+    # order, that matches the output format of switch-to-configuration.pl
+    sortedUnits = xs: lib.concatStringsSep ", " (builtins.sort builtins.lessThan xs);
   in /* python */ ''
     def switch_to_specialisation(system, name, action="test", fail=False):
         if name == "":
@@ -733,6 +792,16 @@ in {
         assert_contains(out, "\nstarting the following units: required-service.service\n")
         assert_lacks(out, "the following new units were started:")
 
+        # Ensure templated units are restarted when the base unit changes
+        switch_to_specialisation("${machine}", "unitWithTemplate")
+        out = switch_to_specialisation("${machine}", "unitWithTemplateModified")
+        assert_contains(out, "stopping the following units: instantiated@one.service, instantiated@two.service\n")
+        assert_lacks(out, "NOT restarting the following changed units:")
+        assert_lacks(out, "reloading the following units:")
+        assert_lacks(out, "\nrestarting the following units:")
+        assert_contains(out, "\nstarting the following units: instantiated@one.service, instantiated@two.service\n")
+        assert_lacks(out, "the following new units were started:")
+
     with subtest("failing units"):
         # Let the simple service fail
         switch_to_specialisation("${machine}", "simpleServiceModified")
@@ -896,15 +965,62 @@ in {
         assert_lacks(out, "NOT restarting the following changed units:")
         assert_lacks(out, "reloading the following units:")
         assert_lacks(out, "restarting the following units:")
-        assert_contains(out, "\nstarting the following units: no-restart-service.service, reload-triggers-and-restart-by-as.service, simple-reload-service.service, simple-restart-service.service, simple-service.service\n")
-        assert_contains(out, "the following new units were started: no-restart-service.service, reload-triggers-and-restart-by-as.service, reload-triggers-and-restart.service, reload-triggers.service, simple-reload-service.service, simple-restart-service.service, simple-service.service\n")
+        assert_contains(out, "\nstarting the following units: ${sortedUnits [
+          "no-restart-service.service"
+          "reload-triggers-and-restart-by-as.service"
+          "simple-reload-service.service"
+          "simple-restart-service.service"
+          "simple-service.service"
+          "templated-no-restart-service@instance.service"
+          "templated-reload-triggers-and-restart-by-as@instance.service"
+          "templated-simple-reload-service@instance.service"
+          "templated-simple-restart-service@instance.service"
+          "templated-simple-service@instance.service"
+        ]}\n")
+        assert_contains(out, "the following new units were started: ${sortedUnits [
+          "no-restart-service.service"
+          "reload-triggers-and-restart-by-as.service"
+          "reload-triggers-and-restart.service"
+          "reload-triggers.service"
+          "simple-reload-service.service"
+          "simple-restart-service.service"
+          "simple-service.service"
+          "system-templated\\\\x2dno\\\\x2drestart\\\\x2dservice.slice"
+          "system-templated\\\\x2dreload\\\\x2dtriggers.slice"
+          "system-templated\\\\x2dreload\\\\x2dtriggers\\\\x2dand\\\\x2drestart.slice"
+          "system-templated\\\\x2dreload\\\\x2dtriggers\\\\x2dand\\\\x2drestart\\\\x2dby\\\\x2das.slice"
+          "system-templated\\\\x2dsimple\\\\x2dreload\\\\x2dservice.slice"
+          "system-templated\\\\x2dsimple\\\\x2drestart\\\\x2dservice.slice"
+          "system-templated\\\\x2dsimple\\\\x2dservice.slice"
+          "templated-no-restart-service@instance.service"
+          "templated-reload-triggers-and-restart-by-as@instance.service"
+          "templated-reload-triggers-and-restart@instance.service"
+          "templated-reload-triggers@instance.service"
+          "templated-simple-reload-service@instance.service"
+          "templated-simple-restart-service@instance.service"
+          "templated-simple-service@instance.service"
+        ]}\n")
         # Switch to the same system where the example services get restarted
         # and reloaded by the activation script
         out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script")
         assert_lacks(out, "stopping the following units:")
         assert_lacks(out, "NOT restarting the following changed units:")
-        assert_contains(out, "reloading the following units: reload-triggers-and-restart.service, reload-triggers.service, simple-reload-service.service\n")
-        assert_contains(out, "restarting the following units: reload-triggers-and-restart-by-as.service, simple-restart-service.service, simple-service.service\n")
+        assert_contains(out, "reloading the following units: ${sortedUnits [
+          "reload-triggers-and-restart.service"
+          "reload-triggers.service"
+          "simple-reload-service.service"
+          "templated-reload-triggers-and-restart@instance.service"
+          "templated-reload-triggers@instance.service"
+          "templated-simple-reload-service@instance.service"
+        ]}\n")
+        assert_contains(out, "restarting the following units: ${sortedUnits [
+          "reload-triggers-and-restart-by-as.service"
+          "simple-restart-service.service"
+          "simple-service.service"
+          "templated-reload-triggers-and-restart-by-as@instance.service"
+          "templated-simple-restart-service@instance.service"
+          "templated-simple-service@instance.service"
+        ]}\n")
         assert_lacks(out, "\nstarting the following units:")
         assert_lacks(out, "the following new units were started:")
         # Switch to the same system and see if the service gets restarted when it's modified
@@ -912,16 +1028,44 @@ in {
         out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script-modified")
         assert_lacks(out, "stopping the following units:")
         assert_lacks(out, "NOT restarting the following changed units:")
-        assert_contains(out, "reloading the following units: reload-triggers.service, simple-reload-service.service\n")
-        assert_contains(out, "restarting the following units: reload-triggers-and-restart-by-as.service, reload-triggers-and-restart.service, simple-restart-service.service, simple-service.service\n")
+        assert_contains(out, "reloading the following units: ${sortedUnits [
+          "reload-triggers.service"
+          "simple-reload-service.service"
+          "templated-reload-triggers@instance.service"
+          "templated-simple-reload-service@instance.service"
+        ]}\n")
+        assert_contains(out, "restarting the following units: ${sortedUnits [
+          "reload-triggers-and-restart-by-as.service"
+          "reload-triggers-and-restart.service"
+          "simple-restart-service.service"
+          "simple-service.service"
+          "templated-reload-triggers-and-restart-by-as@instance.service"
+          "templated-reload-triggers-and-restart@instance.service"
+          "templated-simple-restart-service@instance.service"
+          "templated-simple-service@instance.service"
+        ]}\n")
         assert_lacks(out, "\nstarting the following units:")
         assert_lacks(out, "the following new units were started:")
         # The same, but in dry mode
         out = switch_to_specialisation("${machine}", "restart-and-reload-by-activation-script", action="dry-activate")
         assert_lacks(out, "would stop the following units:")
         assert_lacks(out, "would NOT stop the following changed units:")
-        assert_contains(out, "would reload the following units: reload-triggers.service, simple-reload-service.service\n")
-        assert_contains(out, "would restart the following units: reload-triggers-and-restart-by-as.service, reload-triggers-and-restart.service, simple-restart-service.service, simple-service.service\n")
+        assert_contains(out, "would reload the following units: ${sortedUnits [
+          "reload-triggers.service"
+          "simple-reload-service.service"
+          "templated-reload-triggers@instance.service"
+          "templated-simple-reload-service@instance.service"
+        ]}\n")
+        assert_contains(out, "would restart the following units: ${sortedUnits [
+          "reload-triggers-and-restart-by-as.service"
+          "reload-triggers-and-restart.service"
+          "simple-restart-service.service"
+          "simple-service.service"
+          "templated-reload-triggers-and-restart-by-as@instance.service"
+          "templated-reload-triggers-and-restart@instance.service"
+          "templated-simple-restart-service@instance.service"
+          "templated-simple-service@instance.service"
+        ]}\n")
         assert_lacks(out, "\nwould start the following units:")
 
     with subtest("socket-activated services"):
diff --git a/nixos/tests/web-servers/agate.nix b/nixos/tests/web-servers/agate.nix
index e8d789a9ca44..0de27b6f7d8d 100644
--- a/nixos/tests/web-servers/agate.nix
+++ b/nixos/tests/web-servers/agate.nix
@@ -20,7 +20,7 @@
     geminiserver.wait_for_open_port(1965)
 
     with subtest("check is serving over gemini"):
-      response = geminiserver.succeed("${pkgs.gmni}/bin/gmni -j once -i -N gemini://localhost:1965")
+      response = geminiserver.succeed("${pkgs.gemget}/bin/gemget --header -o - gemini://localhost:1965")
       print(response)
       assert "Hello NixOS!" in response
   '';