summary refs log tree commit diff
path: root/nixos/modules/system
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/system')
-rw-r--r--nixos/modules/system/activation/switch-to-configuration.pl184
-rw-r--r--nixos/modules/system/activation/top-level.nix2
-rw-r--r--nixos/modules/system/boot/stage-1-init.sh3
-rw-r--r--nixos/modules/system/boot/stage-1.nix3
-rw-r--r--nixos/modules/system/boot/systemd.nix5
-rw-r--r--nixos/modules/system/etc/etc-activation.nix12
-rw-r--r--nixos/modules/system/etc/etc.nix6
-rw-r--r--nixos/modules/system/etc/test.nix70
8 files changed, 253 insertions, 32 deletions
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index 1fe346114e4..68333d43a5d 100644
--- a/nixos/modules/system/activation/switch-to-configuration.pl
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -5,7 +5,7 @@ use warnings;
 use Config::IniFiles;
 use File::Path qw(make_path);
 use File::Basename;
-use File::Slurp;
+use File::Slurp qw(read_file write_file edit_file);
 use Net::DBus;
 use Sys::Syslog qw(:standard :macros);
 use Cwd 'abs_path';
@@ -20,12 +20,19 @@ my $restartListFile = "/run/nixos/restart-list";
 my $reloadListFile = "/run/nixos/reload-list";
 
 # Parse restart/reload requests by the activation script.
-# Activation scripts may write newline-separated units to this
+# Activation scripts may write newline-separated units to the restart
 # file and switch-to-configuration will handle them. While
 # `stopIfChanged = true` is ignored, switch-to-configuration will
 # handle `restartIfChanged = false` and `reloadIfChanged = true`.
+# This is the same as specifying a restart trigger in the NixOS module.
+#
+# The reload file asks the script to reload a unit. This is the same as
+# specifying a reload trigger in the NixOS module and can be ignored if
+# the unit is restarted in this activation.
 my $restartByActivationFile = "/run/nixos/activation-restart-list";
+my $reloadByActivationFile = "/run/nixos/activation-reload-list";
 my $dryRestartByActivationFile = "/run/nixos/dry-activation-restart-list";
+my $dryReloadByActivationFile = "/run/nixos/dry-activation-reload-list";
 
 make_path("/run/nixos", { mode => oct(755) });
 
@@ -131,6 +138,10 @@ sub parseSystemdIni {
 
     # Copy over all sections
     foreach my $sectionName (keys %fileContents) {
+        if ($sectionName eq "Install") {
+            # Skip the [Install] section because it has no relevant keys for us
+            next;
+        }
         # Copy over all keys
         foreach my $iniKey (keys %{$fileContents{$sectionName}}) {
             # Ensure the value is an array so it's easier to work with
@@ -192,16 +203,96 @@ sub recordUnit {
     write_file($fn, { append => 1 }, "$unit\n") if $action ne "dry-activate";
 }
 
-# As a fingerprint for determining whether a unit has changed, we use
-# its absolute path. If it has an override file, we append *its*
-# absolute path as well.
-sub fingerprintUnit {
-    my ($s) = @_;
-    return abs_path($s) . (-f "${s}.d/overrides.conf" ? " " . abs_path "${s}.d/overrides.conf" : "");
+# The opposite of recordUnit, removes a unit name from a file
+sub unrecord_unit {
+    my ($fn, $unit) = @_;
+    edit_file { s/^$unit\n//msx } $fn if $action ne "dry-activate";
+}
+
+# Compare the contents of two unit files and return whether the unit
+# needs to be restarted or reloaded. If the units differ, the service
+# is restarted unless the only difference is `X-Reload-Triggers` in the
+# `Unit` section. If this is the only modification, the unit is reloaded
+# instead of restarted.
+# Returns:
+# - 0 if the units are equal
+# - 1 if the units are different and a restart action is required
+# - 2 if the units are different and a reload action is required
+sub compare_units {
+    my ($old_unit, $new_unit) = @_;
+    my $ret = 0;
+
+    my $comp_array = sub {
+      my ($a, $b) = @_;
+      return join("\0", @{$a}) eq join("\0", @{$b});
+    };
+
+    # Comparison hash for the sections
+    my %section_cmp = map { $_ => 1 } keys %{$new_unit};
+    # Iterate over the sections
+    foreach my $section_name (keys %{$old_unit}) {
+        # Missing section in the new unit?
+        if (not exists $section_cmp{$section_name}) {
+            if ($section_name eq 'Unit' and %{$old_unit->{'Unit'}} == 1 and defined(%{$old_unit->{'Unit'}}{'X-Reload-Triggers'})) {
+                # If a new [Unit] section was removed that only contained X-Reload-Triggers,
+                # do nothing.
+                next;
+            } else {
+                return 1;
+            }
+        }
+        delete $section_cmp{$section_name};
+        # Comparison hash for the section contents
+        my %ini_cmp = map { $_ => 1 } keys %{$new_unit->{$section_name}};
+        # Iterate over the keys of the section
+        foreach my $ini_key (keys %{$old_unit->{$section_name}}) {
+            delete $ini_cmp{$ini_key};
+            my @old_value = @{$old_unit->{$section_name}{$ini_key}};
+            # If the key is missing in the new unit, they are different...
+            if (not $new_unit->{$section_name}{$ini_key}) {
+                # ... unless the key that is now missing was the reload trigger
+                if ($section_name eq 'Unit' and $ini_key eq 'X-Reload-Triggers') {
+                    next;
+                }
+                return 1;
+            }
+            my @new_value = @{$new_unit->{$section_name}{$ini_key}};
+            # If the contents are different, the units are different
+            if (not $comp_array->(\@old_value, \@new_value)) {
+                # Check if only the reload triggers changed
+                if ($section_name eq 'Unit' and $ini_key eq 'X-Reload-Triggers') {
+                    $ret = 2;
+                } else {
+                    return 1;
+                }
+            }
+        }
+        # A key was introduced that was missing in the old unit
+        if (%ini_cmp) {
+            if ($section_name eq 'Unit' and %ini_cmp == 1 and defined($ini_cmp{'X-Reload-Triggers'})) {
+                # If the newly introduced key was the reload triggers, reload the unit
+                $ret = 2;
+            } else {
+                return 1;
+            }
+        };
+    }
+    # A section was introduced that was missing in the old unit
+    if (%section_cmp) {
+        if (%section_cmp == 1 and defined($section_cmp{'Unit'}) and %{$new_unit->{'Unit'}} == 1 and defined(%{$new_unit->{'Unit'}}{'X-Reload-Triggers'})) {
+            # If a new [Unit] section was introduced that only contains X-Reload-Triggers,
+            # reload instead of restarting
+            $ret = 2;
+        } else {
+            return 1;
+        }
+    }
+
+    return $ret;
 }
 
 sub handleModifiedUnit {
-    my ($unit, $baseName, $newUnitFile, $activePrev, $unitsToStop, $unitsToStart, $unitsToReload, $unitsToRestart, $unitsToSkip) = @_;
+    my ($unit, $baseName, $newUnitFile, $newUnitInfo, $activePrev, $unitsToStop, $unitsToStart, $unitsToReload, $unitsToRestart, $unitsToSkip) = @_;
 
     if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target" || $unit =~ /\.path$/ || $unit =~ /\.slice$/) {
         # Do nothing.  These cannot be restarted directly.
@@ -219,8 +310,8 @@ sub handleModifiedUnit {
         # Revert of the attempt: https://github.com/NixOS/nixpkgs/pull/147609
         # More details: https://github.com/NixOS/nixpkgs/issues/74899#issuecomment-981142430
     } else {
-        my %unitInfo = parseUnit($newUnitFile);
-        if (parseSystemdBool(\%unitInfo, "Service", "X-ReloadIfChanged", 0)) {
+        my %unitInfo = $newUnitInfo ? %{$newUnitInfo} : parseUnit($newUnitFile);
+        if (parseSystemdBool(\%unitInfo, "Service", "X-ReloadIfChanged", 0) and not $unitsToRestart->{$unit} and not $unitsToStop->{$unit}) {
             $unitsToReload->{$unit} = 1;
             recordUnit($reloadListFile, $unit);
         }
@@ -234,6 +325,11 @@ sub handleModifiedUnit {
                 # stopped and started.
                 $unitsToRestart->{$unit} = 1;
                 recordUnit($restartListFile, $unit);
+                # Remove from units to reload so we don't restart and reload
+                if ($unitsToReload->{$unit}) {
+                    delete $unitsToReload->{$unit};
+                    unrecord_unit($reloadListFile, $unit);
+                }
             } else {
                 # If this unit is socket-activated, then stop the
                 # socket unit(s) as well, and restart the
@@ -254,6 +350,11 @@ sub handleModifiedUnit {
                                 recordUnit($startListFile, $socket);
                                 $socketActivated = 1;
                             }
+                            # Remove from units to reload so we don't restart and reload
+                            if ($unitsToReload->{$unit}) {
+                                delete $unitsToReload->{$unit};
+                                unrecord_unit($reloadListFile, $unit);
+                            }
                         }
                     }
                 }
@@ -268,6 +369,11 @@ sub handleModifiedUnit {
                 }
 
                 $unitsToStop->{$unit} = 1;
+                # Remove from units to reload so we don't restart and reload
+                if ($unitsToReload->{$unit}) {
+                    delete $unitsToReload->{$unit};
+                    unrecord_unit($reloadListFile, $unit);
+                }
             }
         }
     }
@@ -344,8 +450,16 @@ while (my ($unit, $state) = each %{$activePrev}) {
             }
         }
 
-        elsif (fingerprintUnit($prevUnitFile) ne fingerprintUnit($newUnitFile)) {
-            handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+        else {
+            my %old_unit_info = parseUnit($prevUnitFile);
+            my %new_unit_info = parseUnit($newUnitFile);
+            my $diff = compare_units(\%old_unit_info, \%new_unit_info);
+            if ($diff eq 1) {
+                handleModifiedUnit($unit, $baseName, $newUnitFile, \%new_unit_info, $activePrev, \%unitsToStop, \%unitsToStart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+            } elsif ($diff eq 2 and not $unitsToRestart{$unit}) {
+                $unitsToReload{$unit} = 1;
+                recordUnit($reloadListFile, $unit);
+            }
         }
     }
 }
@@ -361,17 +475,6 @@ sub pathToUnitName {
     return $escaped;
 }
 
-sub unique {
-    my %seen;
-    my @res;
-    foreach my $name (@_) {
-        next if $seen{$name};
-        $seen{$name} = 1;
-        push @res, $name;
-    }
-    return @res;
-}
-
 # Compare the previous and new fstab to figure out which filesystems
 # need a remount or need to be unmounted.  New filesystems are mounted
 # automatically by starting local-fs.target.  FIXME: might be nicer if
@@ -407,8 +510,12 @@ foreach my $device (keys %$prevSwaps) {
         # "systemctl stop" here because systemd has lots of alias
         # units that prevent a stop from actually calling
         # "swapoff".
-        print STDERR "stopping swap device: $device\n";
-        system("@utillinux@/sbin/swapoff", $device);
+        if ($action ne "dry-activate") {
+            print STDERR "would stop swap device: $device\n";
+        } else {
+            print STDERR "stopping swap device: $device\n";
+            system("@utillinux@/sbin/swapoff", $device);
+        }
     }
     # FIXME: update swap options (i.e. its priority).
 }
@@ -469,10 +576,20 @@ if ($action eq "dry-activate") {
             next;
         }
 
-        handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToRestart, \%unitsToRestart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+        handleModifiedUnit($unit, $baseName, $newUnitFile, undef, $activePrev, \%unitsToRestart, \%unitsToRestart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
     }
     unlink($dryRestartByActivationFile);
 
+    foreach (split('\n', read_file($dryReloadByActivationFile, err_mode => 'quiet') // "")) {
+        my $unit = $_;
+
+        if (defined($activePrev->{$unit}) and not $unitsToRestart{$unit} and not $unitsToStop{$unit}) {
+            $unitsToReload{$unit} = 1;
+            recordUnit($reloadListFile, $unit);
+        }
+    }
+    unlink($dryReloadByActivationFile);
+
     print STDERR "would restart systemd\n" if $restartSystemd;
     print STDERR "would reload the following units: ", join(", ", sort(keys %unitsToReload)), "\n"
         if scalar(keys %unitsToReload) > 0;
@@ -525,11 +642,22 @@ foreach (split('\n', read_file($restartByActivationFile, err_mode => 'quiet') //
         next;
     }
 
-    handleModifiedUnit($unit, $baseName, $newUnitFile, $activePrev, \%unitsToRestart, \%unitsToRestart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
+    handleModifiedUnit($unit, $baseName, $newUnitFile, undef, $activePrev, \%unitsToRestart, \%unitsToRestart, \%unitsToReload, \%unitsToRestart, \%unitsToSkip);
 }
 # We can remove the file now because it has been propagated to the other restart/reload files
 unlink($restartByActivationFile);
 
+foreach (split('\n', read_file($reloadByActivationFile, err_mode => 'quiet') // "")) {
+    my $unit = $_;
+
+    if (defined($activePrev->{$unit}) and not $unitsToRestart{$unit} and not $unitsToStop{$unit}) {
+        $unitsToReload{$unit} = 1;
+        recordUnit($reloadListFile, $unit);
+    }
+}
+# We can remove the file now because it has been propagated to the other reload file
+unlink($reloadByActivationFile);
+
 # Restart systemd if necessary. Note that this is done using the
 # current version of systemd, just in case the new one has trouble
 # communicating with the running pid 1.
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index 9e6ca75b9da..b8aeee8c11b 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -117,7 +117,7 @@ let
     configurationName = config.boot.loader.grub.configurationName;
 
     # Needed by switch-to-configuration.
-    perl = pkgs.perl.withPackages (p: with p; [ FileSlurp NetDBus XMLParser XMLTwig ConfigIniFiles ]);
+    perl = pkgs.perl.withPackages (p: with p; [ ConfigIniFiles FileSlurp NetDBus ]);
   };
 
   # Handle assertions and warnings
diff --git a/nixos/modules/system/boot/stage-1-init.sh b/nixos/modules/system/boot/stage-1-init.sh
index f86d4641228..8fcc1f02972 100644
--- a/nixos/modules/system/boot/stage-1-init.sh
+++ b/nixos/modules/system/boot/stage-1-init.sh
@@ -282,6 +282,9 @@ checkFS() {
     # Don't check resilient COWs as they validate the fs structures at mount time
     if [ "$fsType" = btrfs -o "$fsType" = zfs -o "$fsType" = bcachefs ]; then return 0; fi
 
+    # Skip fsck for apfs as the fsck utility does not support repairing the filesystem (no -a option)
+    if [ "$fsType" = apfs ]; then return 0; fi
+
     # Skip fsck for nilfs2 - not needed by design and no fsck tool for this filesystem.
     if [ "$fsType" = nilfs2 ]; then return 0; fi
 
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
index 9c684fbada2..1575c0257d1 100644
--- a/nixos/modules/system/boot/stage-1.nix
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -350,6 +350,9 @@ let
             '';
           symlink = "/etc/modprobe.d/ubuntu.conf";
         }
+        { object = config.environment.etc."modprobe.d/nixos.conf".source;
+          symlink = "/etc/modprobe.d/nixos.conf";
+        }
         { object = pkgs.kmod-debian-aliases;
           symlink = "/etc/modprobe.d/debian.conf";
         }
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index 9dcf9eb769f..1f2dd618698 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -243,6 +243,8 @@ let
           { Requisite = toString config.requisite; }
         // optionalAttrs (config.restartTriggers != [])
           { X-Restart-Triggers = toString config.restartTriggers; }
+        // optionalAttrs (config.reloadTriggers != [])
+          { X-Reload-Triggers = toString config.reloadTriggers; }
         // optionalAttrs (config.description != "") {
           Description = config.description; }
         // optionalAttrs (config.documentation != []) {
@@ -917,6 +919,9 @@ in
               (optional hasDeprecated
                 "Service '${name}.service' uses the attribute 'StartLimitInterval' in the Service section, which is deprecated. See https://github.com/NixOS/nixpkgs/issues/45786."
               )
+              (optional (service.reloadIfChanged && service.reloadTriggers != [])
+                "Service '${name}.service' has both 'reloadIfChanged' and 'reloadTriggers' set. This is probably not what you want, because 'reloadTriggers' behave the same whay as 'restartTriggers' if 'reloadIfChanged' is set."
+              )
             ]
         )
         cfg.services
diff --git a/nixos/modules/system/etc/etc-activation.nix b/nixos/modules/system/etc/etc-activation.nix
new file mode 100644
index 00000000000..78010495018
--- /dev/null
+++ b/nixos/modules/system/etc/etc-activation.nix
@@ -0,0 +1,12 @@
+{ config, lib, ... }:
+let
+  inherit (lib) stringAfter;
+in {
+
+  imports = [ ./etc.nix ];
+
+  config = {
+    system.activationScripts.etc =
+      stringAfter [ "users" "groups" ] config.system.build.etcActivationCommands;
+  };
+}
diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix
index 6cc8c341e6d..ed552fecec5 100644
--- a/nixos/modules/system/etc/etc.nix
+++ b/nixos/modules/system/etc/etc.nix
@@ -66,6 +66,8 @@ in
 
 {
 
+  imports = [ ../build.nix ];
+
   ###### interface
 
   options = {
@@ -188,14 +190,12 @@ in
   config = {
 
     system.build.etc = etc;
-
-    system.activationScripts.etc = stringAfter [ "users" "groups" ]
+    system.build.etcActivationCommands =
       ''
         # Set up the statically computed bits of /etc.
         echo "setting up /etc..."
         ${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc
       '';
-
   };
 
 }
diff --git a/nixos/modules/system/etc/test.nix b/nixos/modules/system/etc/test.nix
new file mode 100644
index 00000000000..5e43b155038
--- /dev/null
+++ b/nixos/modules/system/etc/test.nix
@@ -0,0 +1,70 @@
+{ lib
+, coreutils
+, fakechroot
+, fakeroot
+, evalMinimalConfig
+, pkgsModule
+, runCommand
+, util-linux
+, vmTools
+, writeText
+}:
+let
+  node = evalMinimalConfig ({ config, ... }: {
+    imports = [ pkgsModule ../etc/etc.nix ];
+    environment.etc."passwd" = {
+      text = passwdText;
+    };
+    environment.etc."hosts" = {
+      text = hostsText;
+      mode = "0751";
+    };
+  });
+  passwdText = ''
+    root:x:0:0:System administrator:/root:/run/current-system/sw/bin/bash
+  '';
+  hostsText = ''
+    127.0.0.1 localhost
+    ::1 localhost
+    # testing...
+  '';
+in
+lib.recurseIntoAttrs {
+  test-etc-vm =
+    vmTools.runInLinuxVM (runCommand "test-etc-vm" { } ''
+      mkdir -p /etc
+      ${node.config.system.build.etcActivationCommands}
+      set -x
+      [[ -L /etc/passwd ]]
+      diff /etc/passwd ${writeText "expected-passwd" passwdText}
+      [[ 751 = $(stat --format %a /etc/hosts) ]]
+      diff /etc/hosts ${writeText "expected-hosts" hostsText}
+      set +x
+      touch $out
+    '');
+
+  # fakeroot is behaving weird
+  test-etc-fakeroot =
+    runCommand "test-etc"
+      {
+        nativeBuildInputs = [
+          fakeroot
+          fakechroot
+          # for chroot
+          coreutils
+          # fakechroot needs getopt, which is provided by util-linux
+          util-linux
+        ];
+        fakeRootCommands = ''
+          mkdir -p /etc
+          ${node.config.system.build.etcActivationCommands}
+          diff /etc/hosts ${writeText "expected-hosts" hostsText}
+          touch $out
+        '';
+      } ''
+      mkdir fake-root
+      export FAKECHROOT_EXCLUDE_PATH=/dev:/proc:/sys:${builtins.storeDir}:$out
+      fakechroot fakeroot chroot $PWD/fake-root bash -c 'source $stdenv/setup; eval "$fakeRootCommands"'
+    '';
+
+}