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