about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2111.section.xml22
-rw-r--r--nixos/doc/manual/man-nixos-rebuild.xml16
-rw-r--r--nixos/doc/manual/release-notes/rl-2111.section.md6
-rw-r--r--nixos/modules/config/update-users-groups.pl43
-rw-r--r--nixos/modules/config/users-groups.nix6
-rw-r--r--nixos/modules/services/audio/spotifyd.nix27
-rw-r--r--nixos/modules/services/backup/sanoid.nix9
-rw-r--r--nixos/modules/services/backup/znapzend.nix2
-rw-r--r--nixos/modules/services/monitoring/nagios.nix2
-rw-r--r--nixos/modules/services/network-filesystems/ipfs.nix2
-rw-r--r--nixos/modules/services/networking/firefox/sync-server.nix2
-rw-r--r--nixos/modules/system/activation/activation-script.nix100
-rw-r--r--nixos/modules/system/activation/switch-to-configuration.pl6
-rw-r--r--nixos/modules/system/activation/top-level.nix9
-rw-r--r--nixos/modules/tasks/lvm.nix26
-rw-r--r--nixos/tests/miniflux.nix2
-rw-r--r--nixos/tests/mutable-users.nix28
17 files changed, 233 insertions, 75 deletions
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
index 1b0371a0179a..5554927b8b2a 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
@@ -37,6 +37,17 @@
           PostgreSQL now defaults to major version 13.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          Activation scripts can now opt int to be run when running
+          <literal>nixos-rebuild dry-activate</literal> and detect the
+          dry activation by reading <literal>$NIXOS_ACTION</literal>.
+          This allows activation scripts to output what they would
+          change if the activation was really run. The users/modules
+          activation script supports this and outputs some of is
+          actions.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-21.11-new-services">
@@ -1121,6 +1132,17 @@ Superuser created successfully.
           rofi’s changelog</link>.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          ipfs now defaults to not listening on you local network. This
+          setting was change as server providers won’t accept port
+          scanning on their private network. If you have several ipfs
+          instances running on a network you own, feel free to change
+          the setting <literal>ipfs.localDiscovery = true;</literal>.
+          localDiscovery enables different instances to discover each
+          other and share data.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
 </section>
diff --git a/nixos/doc/manual/man-nixos-rebuild.xml b/nixos/doc/manual/man-nixos-rebuild.xml
index 8c34ea7458e6..0e0ea5d74b0b 100644
--- a/nixos/doc/manual/man-nixos-rebuild.xml
+++ b/nixos/doc/manual/man-nixos-rebuild.xml
@@ -553,6 +553,22 @@
 
    <varlistentry>
     <term>
+     <option>--use-substitutes</option>
+    </term>
+    <listitem>
+     <para>
+       When set, nixos-rebuild will add <option>--use-substitutes</option>
+       to each invocation of nix-copy-closure. This will only affect the
+       behavior of nixos-rebuild if <option>--target-host</option> or
+       <option>--build-host</option> is also set. This is useful when
+       the target-host connection to cache.nixos.org is faster than the
+       connection between hosts.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
      <option>--use-remote-sudo</option>
     </term>
     <listitem>
diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md
index 3df77d21d827..00844d529b77 100644
--- a/nixos/doc/manual/release-notes/rl-2111.section.md
+++ b/nixos/doc/manual/release-notes/rl-2111.section.md
@@ -14,6 +14,10 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - PostgreSQL now defaults to major version 13.
 
+- Activation scripts can now opt int to be run when running `nixos-rebuild dry-activate` and detect the dry activation by reading `$NIXOS_ACTION`.
+  This allows activation scripts to output what they would change if the activation was really run.
+  The users/modules activation script supports this and outputs some of is actions.
+
 ## New Services {#sec-release-21.11-new-services}
 
 - [btrbk](https://digint.ch/btrbk/index.html), a backup tool for btrfs subvolumes, taking advantage of btrfs specific capabilities to create atomic snapshots and transfer them incrementally to your backup locations. Available as [services.btrbk](options.html#opt-services.brtbk.instances).
@@ -320,3 +324,5 @@ To be able to access the web UI this port needs to be opened in the firewall.
 - GNOME desktop environment now enables `QGnomePlatform` as the Qt platform theme, which should avoid crashes when opening file chooser dialogs in Qt apps by using XDG desktop portal. Additionally, it will make the apps fit better visually.
 
 - `rofi` has been updated from '1.6.1' to '1.7.0', one important thing is the removal of the old xresources based configuration setup. Read more [in rofi's changelog](https://github.com/davatorium/rofi/blob/cb12e6fc058f4a0f4f/Changelog#L1).
+
+- ipfs now defaults to not listening on you local network. This setting was change as server providers won't accept port scanning on their private network. If you have several ipfs instances running on a network you own, feel free to change the setting `ipfs.localDiscovery = true;`. localDiscovery enables different instances to discover each other and share data.
diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl
index bef08dc40207..232f886789d3 100644
--- a/nixos/modules/config/update-users-groups.pl
+++ b/nixos/modules/config/update-users-groups.pl
@@ -1,11 +1,10 @@
 use strict;
+use warnings;
 use File::Path qw(make_path);
 use File::Slurp;
+use Getopt::Long;
 use JSON;
 
-make_path("/var/lib/nixos", { mode => 0755 });
-
-
 # Keep track of deleted uids and gids.
 my $uidMapFile = "/var/lib/nixos/uid-map";
 my $uidMap = -e $uidMapFile ? decode_json(read_file($uidMapFile)) : {};
@@ -13,12 +12,19 @@ my $uidMap = -e $uidMapFile ? decode_json(read_file($uidMapFile)) : {};
 my $gidMapFile = "/var/lib/nixos/gid-map";
 my $gidMap = -e $gidMapFile ? decode_json(read_file($gidMapFile)) : {};
 
+my $is_dry = ($ENV{'NIXOS_ACTION'} // "") eq "dry-activate";
+GetOptions("dry-activate" => \$is_dry);
+make_path("/var/lib/nixos", { mode => 0755 }) unless $is_dry;
 
 sub updateFile {
     my ($path, $contents, $perms) = @_;
+    return if $is_dry;
     write_file($path, { atomic => 1, binmode => ':utf8', perms => $perms // 0644 }, $contents) or die;
 }
 
+sub nscdInvalidate {
+    system("nscd", "--invalidate", $_[0]) unless $is_dry;
+}
 
 sub hashPassword {
     my ($password) = @_;
@@ -28,6 +34,14 @@ sub hashPassword {
     return crypt($password, '$6$' . $salt . '$');
 }
 
+sub dry_print {
+    if ($is_dry) {
+        print STDERR ("$_[1] $_[2]\n")
+    } else {
+        print STDERR ("$_[0] $_[2]\n")
+    }
+}
+
 
 # Functions for allocating free GIDs/UIDs. FIXME: respect ID ranges in
 # /etc/login.defs.
@@ -51,7 +65,7 @@ sub allocGid {
     my ($name) = @_;
     my $prevGid = $gidMap->{$name};
     if (defined $prevGid && !defined $gidsUsed{$prevGid}) {
-        print STDERR "reviving group '$name' with GID $prevGid\n";
+        dry_print("reviving", "would revive", "group '$name' with GID $prevGid");
         $gidsUsed{$prevGid} = 1;
         return $prevGid;
     }
@@ -63,15 +77,14 @@ sub allocUid {
     my ($min, $max, $up) = $isSystemUser ? (400, 999, 0) : (1000, 29999, 1);
     my $prevUid = $uidMap->{$name};
     if (defined $prevUid && $prevUid >= $min && $prevUid <= $max && !defined $uidsUsed{$prevUid}) {
-        print STDERR "reviving user '$name' with UID $prevUid\n";
+        dry_print("reviving", "would revive", "user '$name' with UID $prevUid");
         $uidsUsed{$prevUid} = 1;
         return $prevUid;
     }
     return allocId(\%uidsUsed, \%uidsPrevUsed, $min, $max, $up, sub { my ($uid) = @_; getpwuid($uid) });
 }
 
-
-# Read the declared users/groups.
+# Read the declared users/groups
 my $spec = decode_json(read_file($ARGV[0]));
 
 # Don't allocate UIDs/GIDs that are manually assigned.
@@ -134,7 +147,7 @@ foreach my $g (@{$spec->{groups}}) {
     if (defined $existing) {
         $g->{gid} = $existing->{gid} if !defined $g->{gid};
         if ($g->{gid} != $existing->{gid}) {
-            warn "warning: not applying GID change of group ‘$name’ ($existing->{gid} -> $g->{gid})\n";
+            dry_print("warning: not applying", "warning: would not apply", "GID change of group ‘$name’ ($existing->{gid} -> $g->{gid})");
             $g->{gid} = $existing->{gid};
         }
         $g->{password} = $existing->{password}; # do we want this?
@@ -163,7 +176,7 @@ foreach my $name (keys %groupsCur) {
     my $g = $groupsCur{$name};
     next if defined $groupsOut{$name};
     if (!$spec->{mutableUsers} || defined $declGroups{$name}) {
-        print STDERR "removing group ‘$name’\n";
+        dry_print("removing group", "would remove group", "‘$name’");
     } else {
         $groupsOut{$name} = $g;
     }
@@ -175,7 +188,7 @@ my @lines = map { join(":", $_->{name}, $_->{password}, $_->{gid}, $_->{members}
     (sort { $a->{gid} <=> $b->{gid} } values(%groupsOut));
 updateFile($gidMapFile, to_json($gidMap));
 updateFile("/etc/group", \@lines);
-system("nscd --invalidate group");
+nscdInvalidate("group");
 
 # Generate a new /etc/passwd containing the declared users.
 my %usersOut;
@@ -196,7 +209,7 @@ foreach my $u (@{$spec->{users}}) {
     if (defined $existing) {
         $u->{uid} = $existing->{uid} if !defined $u->{uid};
         if ($u->{uid} != $existing->{uid}) {
-            warn "warning: not applying UID change of user ‘$name’ ($existing->{uid} -> $u->{uid})\n";
+            dry_print("warning: not applying", "warning: would not apply", "UID change of user ‘$name’ ($existing->{uid} -> $u->{uid})");
             $u->{uid} = $existing->{uid};
         }
     } else {
@@ -211,7 +224,7 @@ foreach my $u (@{$spec->{users}}) {
 
     # Ensure home directory incl. ownership and permissions.
     if ($u->{createHome}) {
-        make_path($u->{home}, { mode => 0700 }) if ! -e $u->{home};
+        make_path($u->{home}, { mode => 0700 }) if ! -e $u->{home} and ! $is_dry;
         chown $u->{uid}, $u->{gid}, $u->{home};
         chmod 0700, $u->{home};
     }
@@ -250,7 +263,7 @@ foreach my $name (keys %usersCur) {
     my $u = $usersCur{$name};
     next if defined $usersOut{$name};
     if (!$spec->{mutableUsers} || defined $declUsers{$name}) {
-        print STDERR "removing user ‘$name’\n";
+        dry_print("removing user", "would remove user", "‘$name’");
     } else {
         $usersOut{$name} = $u;
     }
@@ -261,7 +274,7 @@ foreach my $name (keys %usersCur) {
     (sort { $a->{uid} <=> $b->{uid} } (values %usersOut));
 updateFile($uidMapFile, to_json($uidMap));
 updateFile("/etc/passwd", \@lines);
-system("nscd --invalidate passwd");
+nscdInvalidate("passwd");
 
 
 # Rewrite /etc/shadow to add new accounts or remove dead ones.
@@ -293,7 +306,7 @@ updateFile("/etc/shadow", \@shadowNew, 0640);
     my $uid = getpwnam "root";
     my $gid = getgrnam "shadow";
     my $path = "/etc/shadow";
-    chown($uid, $gid, $path) || die "Failed to change ownership of $path: $!";
+    (chown($uid, $gid, $path) || die "Failed to change ownership of $path: $!") unless $is_dry;
 }
 
 # Rewrite /etc/subuid & /etc/subgid to include default container mappings
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
index f86be3be2c65..d88162558e66 100644
--- a/nixos/modules/config/users-groups.nix
+++ b/nixos/modules/config/users-groups.nix
@@ -561,14 +561,16 @@ in {
       shadow.gid = ids.gids.shadow;
     };
 
-    system.activationScripts.users = stringAfter [ "stdio" ]
-      ''
+    system.activationScripts.users = {
+      supportsDryActivation = true;
+      text = ''
         install -m 0700 -d /root
         install -m 0755 -d /home
 
         ${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \
         -w ${./update-users-groups.pl} ${spec}
       '';
+    };
 
     # for backwards compatibility
     system.activationScripts.groups = stringAfter [ "users" ] "";
diff --git a/nixos/modules/services/audio/spotifyd.nix b/nixos/modules/services/audio/spotifyd.nix
index 9279a03aed4e..22848ed98000 100644
--- a/nixos/modules/services/audio/spotifyd.nix
+++ b/nixos/modules/services/audio/spotifyd.nix
@@ -4,7 +4,15 @@ with lib;
 
 let
   cfg = config.services.spotifyd;
-  spotifydConf = pkgs.writeText "spotifyd.conf" cfg.config;
+  toml = pkgs.formats.toml {};
+  warnConfig =
+    if cfg.config != ""
+    then lib.trace "Using the stringly typed .config attribute is discouraged. Use the TOML typed .settings attribute instead."
+    else id;
+  spotifydConf =
+    if cfg.settings != {}
+    then toml.generate "spotify.conf" cfg.settings
+    else warnConfig (pkgs.writeText "spotifyd.conf" cfg.config);
 in
 {
   options = {
@@ -15,6 +23,16 @@ in
         default = "";
         type = types.lines;
         description = ''
+          (Deprecated) Configuration for Spotifyd. For syntax and directives, see
+          <link xlink:href="https://github.com/Spotifyd/spotifyd#Configuration"/>.
+        '';
+      };
+
+      settings = mkOption {
+        default = {};
+        type = toml.type;
+        example = { global.bitrate = 320; };
+        description = ''
           Configuration for Spotifyd. For syntax and directives, see
           <link xlink:href="https://github.com/Spotifyd/spotifyd#Configuration"/>.
         '';
@@ -23,6 +41,13 @@ in
   };
 
   config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.config == "" || cfg.settings == {};
+        message = "At most one of the .config attribute and the .settings attribute may be set";
+      }
+    ];
+
     systemd.services.spotifyd = {
       wantedBy = [ "multi-user.target" ];
       after = [ "network-online.target" "sound.target" ];
diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix
index 41d0e2e1df68..e70063415ec0 100644
--- a/nixos/modules/services/backup/sanoid.nix
+++ b/nixos/modules/services/backup/sanoid.nix
@@ -57,8 +57,13 @@ let
     useTemplate = use_template;
 
     recursive = mkOption {
-      description = "Whether to recursively snapshot dataset children.";
-      type = types.bool;
+      description = ''
+        Whether to recursively snapshot dataset children.
+        You can also set this to <literal>"zfs"</literal> to handle datasets
+        recursively in an atomic way without the possibility to
+        override settings for child datasets.
+      '';
+      type = with types; oneOf [ bool (enum [ "zfs" ]) ];
       default = false;
     };
 
diff --git a/nixos/modules/services/backup/znapzend.nix b/nixos/modules/services/backup/znapzend.nix
index debb2a397050..1fccc7cd6076 100644
--- a/nixos/modules/services/backup/znapzend.nix
+++ b/nixos/modules/services/backup/znapzend.nix
@@ -324,7 +324,7 @@ in
       autoCreation = mkOption {
         type = bool;
         default = false;
-        description = "Automatically create the destination dataset if it does not exists.";
+        description = "Automatically create the destination dataset if it does not exist.";
       };
 
       zetup = mkOption {
diff --git a/nixos/modules/services/monitoring/nagios.nix b/nixos/modules/services/monitoring/nagios.nix
index 0afaefe04e18..280a9a001b5b 100644
--- a/nixos/modules/services/monitoring/nagios.nix
+++ b/nixos/modules/services/monitoring/nagios.nix
@@ -41,7 +41,7 @@ let
     validated =  pkgs.runCommand "nagios-checked.cfg" {preferLocalBuild=true;} ''
       cp ${file} nagios.cfg
       # nagios checks the existence of /var/lib/nagios, but
-      # it does not exists in the build sandbox, so we fake it
+      # it does not exist in the build sandbox, so we fake it
       mkdir lib
       lib=$(readlink -f lib)
       sed -i s@=${nagiosState}@=$lib@ nagios.cfg
diff --git a/nixos/modules/services/network-filesystems/ipfs.nix b/nixos/modules/services/network-filesystems/ipfs.nix
index 3a01c06edc3b..faa515835b67 100644
--- a/nixos/modules/services/network-filesystems/ipfs.nix
+++ b/nixos/modules/services/network-filesystems/ipfs.nix
@@ -178,7 +178,7 @@ in
         description = ''Whether to enable local discovery for the ipfs daemon.
           This will allow ipfs to scan ports on your local network. Some hosting services will ban you if you do this.
         '';
-        default = true;
+        default = false;
       };
 
       serviceFdlimit = mkOption {
diff --git a/nixos/modules/services/networking/firefox/sync-server.nix b/nixos/modules/services/networking/firefox/sync-server.nix
index 24f768649530..1ad573abfca3 100644
--- a/nixos/modules/services/networking/firefox/sync-server.nix
+++ b/nixos/modules/services/networking/firefox/sync-server.nix
@@ -119,7 +119,7 @@ in
           password, and the <option>syncserver.secret</option> setting is used by the server to
           generate cryptographically-signed authentication tokens.
 
-          If this file does not exists, then it is created with a generated
+          If this file does not exist, then it is created with a generated
           <option>syncserver.secret</option> settings.
        '';
       };
diff --git a/nixos/modules/system/activation/activation-script.nix b/nixos/modules/system/activation/activation-script.nix
index 3a6930314b1a..548b4de852b7 100644
--- a/nixos/modules/system/activation/activation-script.nix
+++ b/nixos/modules/system/activation/activation-script.nix
@@ -17,6 +17,41 @@ let
     '';
   });
 
+  systemActivationScript = set: onlyDry: let
+    set' = filterAttrs (_: v: onlyDry -> v.supportsDryActivation) (mapAttrs (_: v: if isString v then (noDepEntry v) // { supportsDryActivation = false; } else v) set);
+    withHeadlines = addAttributeName set';
+  in
+    ''
+      #!${pkgs.runtimeShell}
+
+      systemConfig='@out@'
+
+      export PATH=/empty
+      for i in ${toString path}; do
+          PATH=$PATH:$i/bin:$i/sbin
+      done
+
+      _status=0
+      trap "_status=1 _localstatus=\$?" ERR
+
+      # Ensure a consistent umask.
+      umask 0022
+
+      ${textClosureMap id (withHeadlines) (attrNames withHeadlines)}
+
+    '' + optionalString (!onlyDry) ''
+      # Make this configuration the current configuration.
+      # The readlink is there to ensure that when $systemConfig = /system
+      # (which is a symlink to the store), /run/current-system is still
+      # used as a garbage collection root.
+      ln -sfn "$(readlink -f "$systemConfig")" /run/current-system
+
+      # Prevent the current configuration from being garbage-collected.
+      ln -sfn /run/current-system /nix/var/nix/gcroots/current-system
+
+      exit $_status
+    '';
+
   path = with pkgs; map getBin
     [ coreutils
       gnugrep
@@ -28,7 +63,7 @@ let
       util-linux # needed for mount and mountpoint
     ];
 
-  scriptType = with types;
+  scriptType = withDry: with types;
     let scriptOptions =
       { deps = mkOption
           { type = types.listOf types.str;
@@ -39,6 +74,19 @@ let
           { type = types.lines;
             description = "The content of the script.";
           };
+      } // optionalAttrs withDry {
+        supportsDryActivation = mkOption
+          { type = types.bool;
+            default = false;
+            description = ''
+              Whether this activation script supports being dry-activated.
+              These activation scripts will also be executed on dry-activate
+              activations with the environment variable
+              <literal>NIXOS_ACTION</literal> being set to <literal>dry-activate
+              </literal>.  it's important that these activation scripts  don't
+              modify anything about the system when the variable is set.
+            '';
+          };
       };
     in either str (submodule { options = scriptOptions; });
 
@@ -74,47 +122,19 @@ in
         idempotent and fast.
       '';
 
-      type = types.attrsOf scriptType;
-
-      apply = set: {
-        script =
-          ''
-            #! ${pkgs.runtimeShell}
-
-            systemConfig=@out@
-
-            export PATH=/empty
-            for i in ${toString path}; do
-                PATH=$PATH:$i/bin:$i/sbin
-            done
-
-            _status=0
-            trap "_status=1 _localstatus=\$?" ERR
-
-            # Ensure a consistent umask.
-            umask 0022
-
-            ${
-              let
-                set' = mapAttrs (n: v: if isString v then noDepEntry v else v) set;
-                withHeadlines = addAttributeName set';
-              in textClosureMap id (withHeadlines) (attrNames withHeadlines)
-            }
-
-            # Make this configuration the current configuration.
-            # The readlink is there to ensure that when $systemConfig = /system
-            # (which is a symlink to the store), /run/current-system is still
-            # used as a garbage collection root.
-            ln -sfn "$(readlink -f "$systemConfig")" /run/current-system
-
-            # Prevent the current configuration from being garbage-collected.
-            ln -sfn /run/current-system /nix/var/nix/gcroots/current-system
-
-            exit $_status
-          '';
+      type = types.attrsOf (scriptType true);
+      apply = set: set // {
+        script = systemActivationScript set false;
       };
     };
 
+    system.dryActivationScript = mkOption {
+      description = "The shell script that is to be run when dry-activating a system.";
+      readOnly = true;
+      internal = true;
+      default = systemActivationScript (removeAttrs config.system.activationScripts [ "script" ]) true;
+    };
+
     system.userActivationScripts = mkOption {
       default = {};
 
@@ -137,7 +157,7 @@ in
         idempotent and fast.
       '';
 
-      type = with types; attrsOf scriptType;
+      type = with types; attrsOf (scriptType false);
 
       apply = set: {
         script = ''
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index dd391c8b5d78..b7a062755296 100644
--- a/nixos/modules/system/activation/switch-to-configuration.pl
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -36,6 +36,8 @@ EOF
     exit 1;
 }
 
+$ENV{NIXOS_ACTION} = $action;
+
 # This is a NixOS installation if it has /etc/NIXOS or a proper
 # /etc/os-release.
 die "This is not a NixOS installation!\n" unless
@@ -360,6 +362,10 @@ if ($action eq "dry-activate") {
         if scalar @unitsToStopFiltered > 0;
     print STDERR "would NOT stop the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n"
         if scalar(keys %unitsToSkip) > 0;
+
+    print STDERR "would activate the configuration...\n";
+    system("$out/dry-activate", "$out");
+
     print STDERR "would restart systemd\n" if $restartSystemd;
     print STDERR "would restart the following units: ", join(", ", sort(keys %unitsToRestart)), "\n"
         if scalar(keys %unitsToRestart) > 0;
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index d3e4923a993f..616e1422aa8c 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -56,9 +56,13 @@ let
       ''}
 
       echo "$activationScript" > $out/activate
+      echo "$dryActivationScript" > $out/dry-activate
       substituteInPlace $out/activate --subst-var out
-      chmod u+x $out/activate
-      unset activationScript
+      substituteInPlace $out/dry-activate --subst-var out
+      chmod u+x $out/activate $out/dry-activate
+      unset activationScript dryActivationScript
+      ${pkgs.runtimeShell} -n $out/activate
+      ${pkgs.runtimeShell} -n $out/dry-activate
 
       cp ${config.system.build.bootStage2} $out/init
       substituteInPlace $out/init --subst-var-by systemConfig $out
@@ -108,6 +112,7 @@ let
       config.system.build.installBootLoader
       or "echo 'Warning: do not know how to make this configuration bootable; please enable a boot loader.' 1>&2; true";
     activationScript = config.system.activationScripts.script;
+    dryActivationScript = config.system.dryActivationScript;
     nixosLabel = config.system.nixos.label;
 
     configurationName = config.boot.loader.grub.configurationName;
diff --git a/nixos/modules/tasks/lvm.nix b/nixos/modules/tasks/lvm.nix
index 98a0e2ddef90..aaa76b49fa30 100644
--- a/nixos/modules/tasks/lvm.nix
+++ b/nixos/modules/tasks/lvm.nix
@@ -46,22 +46,32 @@ in {
         kernelModules = [ "dm-snapshot" "dm-thin-pool" ];
 
         extraUtilsCommands = ''
-          copy_bin_and_libs ${pkgs.thin-provisioning-tools}/bin/pdata_tools
-          copy_bin_and_libs ${pkgs.thin-provisioning-tools}/bin/thin_check
+          for BIN in ${pkgs.thin-provisioning-tools}/bin/*; do
+            copy_bin_and_libs $BIN
+          done
+        '';
+
+        extraUtilsCommandsTest = ''
+          ls ${pkgs.thin-provisioning-tools}/bin/ | grep -v pdata_tools | while read BIN; do
+            $out/bin/$(basename $BIN) --help > /dev/null
+          done
         '';
       };
 
-      environment.etc."lvm/lvm.conf".text = ''
-        global/thin_check_executable = "${pkgs.thin-provisioning-tools}/bin/thin_check"
-      '';
+      environment.etc."lvm/lvm.conf".text = concatMapStringsSep "\n"
+        (bin: "global/${bin}_executable = ${pkgs.thin-provisioning-tools}/bin/${bin}")
+        [ "thin_check" "thin_dump" "thin_repair" "cache_check" "cache_dump" "cache_repair" ];
     })
     (mkIf (cfg.dmeventd.enable || cfg.boot.thin.enable) {
       boot.initrd.preLVMCommands = ''
           mkdir -p /etc/lvm
           cat << EOF >> /etc/lvm/lvm.conf
-          ${optionalString cfg.boot.thin.enable ''
-            global/thin_check_executable = "$(command -v thin_check)"
-          ''}
+          ${optionalString cfg.boot.thin.enable (
+            concatMapStringsSep "\n"
+              (bin: "global/${bin}_executable = $(command -v ${bin})")
+              [ "thin_check" "thin_dump" "thin_repair" "cache_check" "cache_dump" "cache_repair" ]
+            )
+          }
           ${optionalString cfg.dmeventd.enable ''
             dmeventd/executable = "$(command -v false)"
             activation/monitoring = 0
diff --git a/nixos/tests/miniflux.nix b/nixos/tests/miniflux.nix
index 9a25a9e77cc9..1015550fa8c7 100644
--- a/nixos/tests/miniflux.nix
+++ b/nixos/tests/miniflux.nix
@@ -11,7 +11,7 @@ in
 with lib;
 {
   name = "miniflux";
-  meta.maintainers = with pkgs.lib.maintainers; [ bricewge ];
+  meta.maintainers = with pkgs.lib.maintainers; [ ];
 
   nodes = {
     default =
diff --git a/nixos/tests/mutable-users.nix b/nixos/tests/mutable-users.nix
index e3f002d9b198..ebe32e6487ef 100644
--- a/nixos/tests/mutable-users.nix
+++ b/nixos/tests/mutable-users.nix
@@ -12,6 +12,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     };
     mutable = { ... }: {
       users.mutableUsers = true;
+      users.users.dry-test.isNormalUser = true;
     };
   };
 
@@ -41,5 +42,32 @@ import ./make-test-python.nix ({ pkgs, ...} : {
             "${mutableSystem}/bin/switch-to-configuration test"
         )
         assert "/run/wrappers/" in machine.succeed("which passwd")
+
+    with subtest("dry-activation does not change files"):
+        machine.succeed('test -e /home/dry-test')  # home was created
+        machine.succeed('rm -rf /home/dry-test')
+
+        files_to_check = ['/etc/group',
+                          '/etc/passwd',
+                          '/etc/shadow',
+                          '/etc/subuid',
+                          '/etc/subgid',
+                          '/var/lib/nixos/uid-map',
+                          '/var/lib/nixos/gid-map',
+                          '/var/lib/nixos/declarative-groups',
+                          '/var/lib/nixos/declarative-users'
+                         ]
+        expected_hashes = {}
+        expected_stats = {}
+        for file in files_to_check:
+            expected_hashes[file] = machine.succeed(f"sha256sum {file}")
+            expected_stats[file] = machine.succeed(f"stat {file}")
+
+        machine.succeed("/run/current-system/bin/switch-to-configuration dry-activate")
+
+        machine.fail('test -e /home/dry-test')  # home was not recreated
+        for file in files_to_check:
+            assert machine.succeed(f"sha256sum {file}") == expected_hashes[file]
+            assert machine.succeed(f"stat {file}") == expected_stats[file]
   '';
 })