diff options
Diffstat (limited to 'nixos/modules/system')
-rw-r--r-- | nixos/modules/system/activation/switch-to-configuration.pl | 184 | ||||
-rw-r--r-- | nixos/modules/system/activation/top-level.nix | 2 | ||||
-rw-r--r-- | nixos/modules/system/boot/stage-1-init.sh | 3 | ||||
-rw-r--r-- | nixos/modules/system/boot/stage-1.nix | 3 | ||||
-rw-r--r-- | nixos/modules/system/boot/systemd.nix | 5 | ||||
-rw-r--r-- | nixos/modules/system/etc/etc-activation.nix | 12 | ||||
-rw-r--r-- | nixos/modules/system/etc/etc.nix | 6 | ||||
-rw-r--r-- | nixos/modules/system/etc/test.nix | 70 |
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"' + ''; + +} |