at 17.09-beta 18 kB view raw
1#! @perl@ 2 3use strict; 4use warnings; 5use File::Basename; 6use File::Slurp; 7use Sys::Syslog qw(:standard :macros); 8use Cwd 'abs_path'; 9 10my $out = "@out@"; 11 12# To be robust against interruption, record what units need to be started etc. 13my $startListFile = "/run/systemd/start-list"; 14my $restartListFile = "/run/systemd/restart-list"; 15my $reloadListFile = "/run/systemd/reload-list"; 16 17my $action = shift @ARGV; 18 19if (!defined $action || ($action ne "switch" && $action ne "boot" && $action ne "test" && $action ne "dry-activate")) { 20 print STDERR <<EOF; 21Usage: $0 [switch|boot|test] 22 23switch: make the configuration the boot default and activate now 24boot: make the configuration the boot default 25test: activate the configuration, but don\'t make it the boot default 26dry-activate: show what would be done if this configuration were activated 27EOF 28 exit 1; 29} 30 31# This is a NixOS installation if it has /etc/NIXOS or a proper 32# /etc/os-release. 33die "This is not a NixOS installation!\n" unless 34 -f "/etc/NIXOS" || (read_file("/etc/os-release", err_mode => 'quiet') // "") =~ /ID=nixos/s; 35 36openlog("nixos", "", LOG_USER); 37 38# Install or update the bootloader. 39if ($action eq "switch" || $action eq "boot") { 40 system("@installBootLoader@ $out") == 0 or exit 1; 41} 42 43# Just in case the new configuration hangs the system, do a sync now. 44system("@coreutils@/bin/sync", "-f", "/nix/store") unless ($ENV{"NIXOS_NO_SYNC"} // "") eq "1"; 45 46exit 0 if $action eq "boot"; 47 48# Check if we can activate the new configuration. 49my $oldVersion = read_file("/run/current-system/init-interface-version", err_mode => 'quiet') // ""; 50my $newVersion = read_file("$out/init-interface-version"); 51 52if ($newVersion ne $oldVersion) { 53 print STDERR <<EOF; 54Warning: the new NixOS configuration has an ‘init’ that is 55incompatible with the current configuration. The new configuration 56won\'t take effect until you reboot the system. 57EOF 58 exit 100; 59} 60 61# Ignore SIGHUP so that we're not killed if we're running on (say) 62# virtual console 1 and we restart the "tty1" unit. 63$SIG{PIPE} = "IGNORE"; 64 65sub getActiveUnits { 66 # FIXME: use D-Bus or whatever to query this, since parsing the 67 # output of list-units is likely to break. 68 my $lines = `LANG= systemctl list-units --full --no-legend`; 69 my $res = {}; 70 foreach my $line (split '\n', $lines) { 71 chomp $line; 72 last if $line eq ""; 73 $line =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s/ or next; 74 next if $1 eq "UNIT"; 75 $res->{$1} = { load => $2, state => $3, substate => $4 }; 76 } 77 return $res; 78} 79 80sub parseFstab { 81 my ($filename) = @_; 82 my ($fss, $swaps); 83 foreach my $line (read_file($filename, err_mode => 'quiet')) { 84 chomp $line; 85 $line =~ s/^\s*#.*//; 86 next if $line =~ /^\s*$/; 87 my @xs = split / /, $line; 88 if ($xs[2] eq "swap") { 89 $swaps->{$xs[0]} = { options => $xs[3] // "" }; 90 } else { 91 $fss->{$xs[1]} = { device => $xs[0], fsType => $xs[2], options => $xs[3] // "" }; 92 } 93 } 94 return ($fss, $swaps); 95} 96 97sub parseUnit { 98 my ($filename) = @_; 99 my $info = {}; 100 parseKeyValues($info, read_file($filename)) if -f $filename; 101 parseKeyValues($info, read_file("${filename}.d/overrides.conf")) if -f "${filename}.d/overrides.conf"; 102 return $info; 103} 104 105sub parseKeyValues { 106 my $info = shift; 107 foreach my $line (@_) { 108 # FIXME: not quite correct. 109 $line =~ /^([^=]+)=(.*)$/ or next; 110 $info->{$1} = $2; 111 } 112} 113 114sub boolIsTrue { 115 my ($s) = @_; 116 return $s eq "yes" || $s eq "true"; 117} 118 119sub recordUnit { 120 my ($fn, $unit) = @_; 121 write_file($fn, { append => 1 }, "$unit\n") if $action ne "dry-activate"; 122} 123 124# As a fingerprint for determining whether a unit has changed, we use 125# its absolute path. If it has an override file, we append *its* 126# absolute path as well. 127sub fingerprintUnit { 128 my ($s) = @_; 129 return abs_path($s) . (-f "${s}.d/overrides.conf" ? " " . abs_path "${s}.d/overrides.conf" : ""); 130} 131 132# Figure out what units need to be stopped, started, restarted or reloaded. 133my (%unitsToStop, %unitsToSkip, %unitsToStart, %unitsToRestart, %unitsToReload); 134 135my %unitsToFilter; # units not shown 136 137$unitsToStart{$_} = 1 foreach 138 split('\n', read_file($startListFile, err_mode => 'quiet') // ""); 139 140$unitsToRestart{$_} = 1 foreach 141 split('\n', read_file($restartListFile, err_mode => 'quiet') // ""); 142 143$unitsToReload{$_} = 1 foreach 144 split '\n', read_file($reloadListFile, err_mode => 'quiet') // ""; 145 146my $activePrev = getActiveUnits; 147while (my ($unit, $state) = each %{$activePrev}) { 148 my $baseUnit = $unit; 149 150 my $prevUnitFile = "/etc/systemd/system/$baseUnit"; 151 my $newUnitFile = "$out/etc/systemd/system/$baseUnit"; 152 153 # Detect template instances. 154 if (!-e $prevUnitFile && !-e $newUnitFile && $unit =~ /^(.*)@[^\.]*\.(.*)$/) { 155 $baseUnit = "$1\@.$2"; 156 $prevUnitFile = "/etc/systemd/system/$baseUnit"; 157 $newUnitFile = "$out/etc/systemd/system/$baseUnit"; 158 } 159 160 my $baseName = $baseUnit; 161 $baseName =~ s/\.[a-z]*$//; 162 163 if (-e $prevUnitFile && ($state->{state} eq "active" || $state->{state} eq "activating")) { 164 if (! -e $newUnitFile || abs_path($newUnitFile) eq "/dev/null") { 165 my $unitInfo = parseUnit($prevUnitFile); 166 $unitsToStop{$unit} = 1 if boolIsTrue($unitInfo->{'X-StopOnRemoval'} // "yes"); 167 } 168 169 elsif ($unit =~ /\.target$/) { 170 my $unitInfo = parseUnit($newUnitFile); 171 172 # Cause all active target units to be restarted below. 173 # This should start most changed units we stop here as 174 # well as any new dependencies (including new mounts and 175 # swap devices). FIXME: the suspend target is sometimes 176 # active after the system has resumed, which probably 177 # should not be the case. Just ignore it. 178 if ($unit ne "suspend.target" && $unit ne "hibernate.target" && $unit ne "hybrid-sleep.target") { 179 unless (boolIsTrue($unitInfo->{'RefuseManualStart'} // "no")) { 180 $unitsToStart{$unit} = 1; 181 recordUnit($startListFile, $unit); 182 # Don't spam the user with target units that always get started. 183 $unitsToFilter{$unit} = 1; 184 } 185 } 186 187 # Stop targets that have X-StopOnReconfiguration set. 188 # This is necessary to respect dependency orderings 189 # involving targets: if unit X starts after target Y and 190 # target Y starts after unit Z, then if X and Z have both 191 # changed, then X should be restarted after Z. However, 192 # if target Y is in the "active" state, X and Z will be 193 # restarted at the same time because X's dependency on Y 194 # is already satisfied. Thus, we need to stop Y first. 195 # Stopping a target generally has no effect on other units 196 # (unless there is a PartOf dependency), so this is just a 197 # bookkeeping thing to get systemd to do the right thing. 198 if (boolIsTrue($unitInfo->{'X-StopOnReconfiguration'} // "no")) { 199 $unitsToStop{$unit} = 1; 200 } 201 } 202 203 elsif (fingerprintUnit($prevUnitFile) ne fingerprintUnit($newUnitFile)) { 204 if ($unit eq "sysinit.target" || $unit eq "basic.target" || $unit eq "multi-user.target" || $unit eq "graphical.target") { 205 # Do nothing. These cannot be restarted directly. 206 } elsif ($unit =~ /\.mount$/) { 207 # Reload the changed mount unit to force a remount. 208 $unitsToReload{$unit} = 1; 209 recordUnit($reloadListFile, $unit); 210 } elsif ($unit =~ /\.socket$/ || $unit =~ /\.path$/ || $unit =~ /\.slice$/) { 211 # FIXME: do something? 212 } else { 213 my $unitInfo = parseUnit($newUnitFile); 214 if (boolIsTrue($unitInfo->{'X-ReloadIfChanged'} // "no")) { 215 $unitsToReload{$unit} = 1; 216 recordUnit($reloadListFile, $unit); 217 } 218 elsif (!boolIsTrue($unitInfo->{'X-RestartIfChanged'} // "yes") || boolIsTrue($unitInfo->{'RefuseManualStop'} // "no") ) { 219 $unitsToSkip{$unit} = 1; 220 } else { 221 if (!boolIsTrue($unitInfo->{'X-StopIfChanged'} // "yes")) { 222 # This unit should be restarted instead of 223 # stopped and started. 224 $unitsToRestart{$unit} = 1; 225 recordUnit($restartListFile, $unit); 226 } else { 227 # If this unit is socket-activated, then stop the 228 # socket unit(s) as well, and restart the 229 # socket(s) instead of the service. 230 my $socketActivated = 0; 231 if ($unit =~ /\.service$/) { 232 my @sockets = split / /, ($unitInfo->{Sockets} // ""); 233 if (scalar @sockets == 0) { 234 @sockets = ("$baseName.socket"); 235 } 236 foreach my $socket (@sockets) { 237 if (defined $activePrev->{$socket}) { 238 $unitsToStop{$socket} = 1; 239 $unitsToStart{$socket} = 1; 240 recordUnit($startListFile, $socket); 241 $socketActivated = 1; 242 } 243 } 244 } 245 246 # If the unit is not socket-activated, record 247 # that this unit needs to be started below. 248 # We write this to a file to ensure that the 249 # service gets restarted if we're interrupted. 250 if (!$socketActivated) { 251 $unitsToStart{$unit} = 1; 252 recordUnit($startListFile, $unit); 253 } 254 255 $unitsToStop{$unit} = 1; 256 } 257 } 258 } 259 } 260 } 261} 262 263sub pathToUnitName { 264 my ($path) = @_; 265 open my $cmd, "-|", "@systemd@/bin/systemd-escape", "--suffix=mount", "-p", $path 266 or die "Unable to escape $path!\n"; 267 my $escaped = join "", <$cmd>; 268 chomp $escaped; 269 close $cmd or die; 270 return $escaped; 271} 272 273sub unique { 274 my %seen; 275 my @res; 276 foreach my $name (@_) { 277 next if $seen{$name}; 278 $seen{$name} = 1; 279 push @res, $name; 280 } 281 return @res; 282} 283 284# Compare the previous and new fstab to figure out which filesystems 285# need a remount or need to be unmounted. New filesystems are mounted 286# automatically by starting local-fs.target. FIXME: might be nicer if 287# we generated units for all mounts; then we could unify this with the 288# unit checking code above. 289my ($prevFss, $prevSwaps) = parseFstab "/etc/fstab"; 290my ($newFss, $newSwaps) = parseFstab "$out/etc/fstab"; 291foreach my $mountPoint (keys %$prevFss) { 292 my $prev = $prevFss->{$mountPoint}; 293 my $new = $newFss->{$mountPoint}; 294 my $unit = pathToUnitName($mountPoint); 295 if (!defined $new) { 296 # Filesystem entry disappeared, so unmount it. 297 $unitsToStop{$unit} = 1; 298 } elsif ($prev->{fsType} ne $new->{fsType} || $prev->{device} ne $new->{device}) { 299 # Filesystem type or device changed, so unmount and mount it. 300 $unitsToStop{$unit} = 1; 301 $unitsToStart{$unit} = 1; 302 recordUnit($startListFile, $unit); 303 } elsif ($prev->{options} ne $new->{options}) { 304 # Mount options changes, so remount it. 305 $unitsToReload{$unit} = 1; 306 recordUnit($reloadListFile, $unit); 307 } 308} 309 310# Also handles swap devices. 311foreach my $device (keys %$prevSwaps) { 312 my $prev = $prevSwaps->{$device}; 313 my $new = $newSwaps->{$device}; 314 if (!defined $new) { 315 # Swap entry disappeared, so turn it off. Can't use 316 # "systemctl stop" here because systemd has lots of alias 317 # units that prevent a stop from actually calling 318 # "swapoff". 319 print STDERR "stopping swap device: $device\n"; 320 system("@utillinux@/sbin/swapoff", $device); 321 } 322 # FIXME: update swap options (i.e. its priority). 323} 324 325 326# Should we have systemd re-exec itself? 327my $prevSystemd = abs_path("/proc/1/exe") // "/unknown"; 328my $newSystemd = abs_path("@systemd@/lib/systemd/systemd") or die; 329my $restartSystemd = $prevSystemd ne $newSystemd; 330 331 332sub filterUnits { 333 my ($units) = @_; 334 my @res; 335 foreach my $unit (sort(keys %{$units})) { 336 push @res, $unit if !defined $unitsToFilter{$unit}; 337 } 338 return @res; 339} 340 341my @unitsToStopFiltered = filterUnits(\%unitsToStop); 342my @unitsToStartFiltered = filterUnits(\%unitsToStart); 343 344 345# Show dry-run actions. 346if ($action eq "dry-activate") { 347 print STDERR "would stop the following units: ", join(", ", @unitsToStopFiltered), "\n" 348 if scalar @unitsToStopFiltered > 0; 349 print STDERR "would NOT stop the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n" 350 if scalar(keys %unitsToSkip) > 0; 351 print STDERR "would restart systemd\n" if $restartSystemd; 352 print STDERR "would restart the following units: ", join(", ", sort(keys %unitsToRestart)), "\n" 353 if scalar(keys %unitsToRestart) > 0; 354 print STDERR "would start the following units: ", join(", ", @unitsToStartFiltered), "\n" 355 if scalar @unitsToStartFiltered; 356 print STDERR "would reload the following units: ", join(", ", sort(keys %unitsToReload)), "\n" 357 if scalar(keys %unitsToReload) > 0; 358 exit 0; 359} 360 361 362syslog(LOG_NOTICE, "switching to system configuration $out"); 363 364if (scalar (keys %unitsToStop) > 0) { 365 print STDERR "stopping the following units: ", join(", ", @unitsToStopFiltered), "\n" 366 if scalar @unitsToStopFiltered; 367 system("systemctl", "stop", "--", sort(keys %unitsToStop)); # FIXME: ignore errors? 368} 369 370print STDERR "NOT restarting the following changed units: ", join(", ", sort(keys %unitsToSkip)), "\n" 371 if scalar(keys %unitsToSkip) > 0; 372 373# Activate the new configuration (i.e., update /etc, make accounts, 374# and so on). 375my $res = 0; 376print STDERR "activating the configuration...\n"; 377system("$out/activate", "$out") == 0 or $res = 2; 378 379# Restart systemd if necessary. 380if ($restartSystemd) { 381 print STDERR "restarting systemd...\n"; 382 system("@systemd@/bin/systemctl", "daemon-reexec") == 0 or $res = 2; 383} 384 385# Forget about previously failed services. 386system("@systemd@/bin/systemctl", "reset-failed"); 387 388# Make systemd reload its units. 389system("@systemd@/bin/systemctl", "daemon-reload") == 0 or $res = 3; 390 391# Set the new tmpfiles 392print STDERR "setting up tmpfiles\n"; 393system("@systemd@/bin/systemd-tmpfiles", "--create", "--remove", "--exclude-prefix=/dev") == 0 or $res = 3; 394 395# Reload units that need it. This includes remounting changed mount 396# units. 397if (scalar(keys %unitsToReload) > 0) { 398 print STDERR "reloading the following units: ", join(", ", sort(keys %unitsToReload)), "\n"; 399 system("@systemd@/bin/systemctl", "reload", "--", sort(keys %unitsToReload)) == 0 or $res = 4; 400 unlink($reloadListFile); 401} 402 403# Restart changed services (those that have to be restarted rather 404# than stopped and started). 405if (scalar(keys %unitsToRestart) > 0) { 406 print STDERR "restarting the following units: ", join(", ", sort(keys %unitsToRestart)), "\n"; 407 system("@systemd@/bin/systemctl", "restart", "--", sort(keys %unitsToRestart)) == 0 or $res = 4; 408 unlink($restartListFile); 409} 410 411# Start all active targets, as well as changed units we stopped above. 412# The latter is necessary because some may not be dependencies of the 413# targets (i.e., they were manually started). FIXME: detect units 414# that are symlinks to other units. We shouldn't start both at the 415# same time because we'll get a "Failed to add path to set" error from 416# systemd. 417print STDERR "starting the following units: ", join(", ", @unitsToStartFiltered), "\n" 418 if scalar @unitsToStartFiltered; 419system("@systemd@/bin/systemctl", "start", "--", sort(keys %unitsToStart)) == 0 or $res = 4; 420unlink($startListFile); 421 422 423# Print failed and new units. 424my (@failed, @new, @restarting); 425my $activeNew = getActiveUnits; 426while (my ($unit, $state) = each %{$activeNew}) { 427 if ($state->{state} eq "failed") { 428 push @failed, $unit; 429 } 430 elsif ($state->{state} eq "auto-restart") { 431 # A unit in auto-restart state is a failure *if* it previously failed to start 432 my $lines = `@systemd@/bin/systemctl show '$unit'`; 433 my $info = {}; 434 parseKeyValues($info, split("\n", $lines)); 435 436 if ($info->{ExecMainStatus} ne '0') { 437 push @failed, $unit; 438 } 439 } 440 elsif ($state->{state} ne "failed" && !defined $activePrev->{$unit}) { 441 push @new, $unit; 442 } 443} 444 445print STDERR "the following new units were started: ", join(", ", sort(@new)), "\n" 446 if scalar @new > 0; 447 448if (scalar @failed > 0) { 449 print STDERR "warning: the following units failed: ", join(", ", sort(@failed)), "\n"; 450 foreach my $unit (@failed) { 451 print STDERR "\n"; 452 system("COLUMNS=1000 @systemd@/bin/systemctl status --no-pager '$unit' >&2"); 453 } 454 $res = 4; 455} 456 457if ($res == 0) { 458 syslog(LOG_NOTICE, "finished switching to system configuration $out"); 459} else { 460 syslog(LOG_ERR, "switching to system configuration $out failed (status $res)"); 461} 462 463exit $res;