diff --git a/nixos/modules/tasks/auto-upgrade.nix b/nixos/modules/tasks/auto-upgrade.nix index 7028c0be444d..8bb445db9352 100644 --- a/nixos/modules/tasks/auto-upgrade.nix +++ b/nixos/modules/tasks/auto-upgrade.nix @@ -114,6 +114,27 @@ in ''; }; + rebootTriggers = lib.mkOption { + type = lib.types.listOf lib.types.pathInStore; + default = [ ]; + description = '' + List of derivations that will cause an auto-reboot when changed when + {option}`system.autoUpgrade.allowReboot` is set to true. + ''; + defaultText = lib.literalExpression '' + [ + config.system.build.initialRamdisk + config.system.build.kernel + config.hardware.firmware + (pkgs.writeTextFile { + name = "kernel-params"; + text = lib.concatStringsSep " " config.boot.kernelParams; + }) + ] + ++ config.system.switch.inhibitors + ''; + }; + randomizedDelaySec = lib.mkOption { default = "0"; type = lib.types.str; @@ -197,133 +218,205 @@ in }; - config = lib.mkIf cfg.enable { + config = lib.mkMerge [ - assertions = [ - { - assertion = !((cfg.channel != null) && (cfg.flake != null)); - message = '' - The options 'system.autoUpgrade.channel' and 'system.autoUpgrade.flake' cannot both be set. - ''; - } - { - assertion = (cfg.runGarbageCollection -> config.nix.enable); - message = '' - The option 'system.autoUpgrade.runGarbageCollection = true' requires 'nix.enable = true'. - ''; - } - ]; - - system.autoUpgrade.flags = ( - if cfg.flake == null then - [ "--no-build-output" ] - ++ lib.optionals (cfg.channel != null) [ - "-I" - "nixpkgs=${cfg.channel}/nixexprs.tar.xz" + { + system = { + autoUpgrade.rebootTriggers = [ + config.system.build.initialRamdisk + config.system.build.kernel + config.hardware.firmware + (pkgs.writeTextFile { + name = "kernel-params"; + text = lib.concatStringsSep " " config.boot.kernelParams; + }) ] - else - [ - "--refresh" - "--flake ${cfg.flake}" - ] - ); + ++ config.system.switch.inhibitors; - systemd.services.nixos-upgrade = { - description = "NixOS Upgrade"; + systemBuilderCommands = '' + ln -s ${config.system.build.rebootTriggers} $out/reboot-triggers + ''; - restartIfChanged = false; - unitConfig.X-StopOnRemoval = false; - unitConfig.OnSuccess = lib.optional ( - cfg.runGarbageCollection && config.nix.enable - ) "nix-gc.service"; + build.rebootTriggers = pkgs.writeTextFile { + name = "reboot-triggers"; + text = lib.concatMapStringsSep "\n" (drv: drv.outPath) config.system.autoUpgrade.rebootTriggers; + }; + }; + } - serviceConfig.Type = "oneshot"; - - environment = - config.nix.envVars - // { - inherit (config.environment.sessionVariables) NIX_PATH; - HOME = "/root"; + (lib.mkIf cfg.enable { + assertions = [ + { + assertion = !((cfg.channel != null) && (cfg.flake != null)); + message = '' + The options 'system.autoUpgrade.channel' and 'system.autoUpgrade.flake' cannot both be set. + ''; + } + { + assertion = (cfg.runGarbageCollection -> config.nix.enable); + message = '' + The option 'system.autoUpgrade.runGarbageCollection = true' requires 'nix.enable = true'. + ''; } - // config.networking.proxy.envVars; - - path = with pkgs; [ - coreutils - gnutar - xz.bin - gzip - gitMinimal - config.nix.package.out - config.programs.ssh.package ]; - script = - let - nixos-rebuild = "${config.system.build.nixos-rebuild}/bin/nixos-rebuild"; - date = "${pkgs.coreutils}/bin/date"; - readlink = "${pkgs.coreutils}/bin/readlink"; - shutdown = "${config.systemd.package}/bin/shutdown"; - upgradeFlag = lib.optional (cfg.channel == null && cfg.upgrade) "--upgrade"; - in - if cfg.allowReboot then - '' - ${nixos-rebuild} boot ${toString (cfg.flags ++ upgradeFlag)} - booted="$(${readlink} /run/booted-system/{initrd,kernel,kernel-modules})" - built="$(${readlink} /nix/var/nix/profiles/system/{initrd,kernel,kernel-modules})" - - ${lib.optionalString (cfg.rebootWindow != null) '' - current_time="$(${date} +%H:%M)" - - lower="${cfg.rebootWindow.lower}" - upper="${cfg.rebootWindow.upper}" - - if [[ "''${lower}" < "''${upper}" ]]; then - if [[ "''${current_time}" > "''${lower}" ]] && \ - [[ "''${current_time}" < "''${upper}" ]]; then - do_reboot="true" - else - do_reboot="false" - fi - else - # lower > upper, so we are crossing midnight (e.g. lower=23h, upper=6h) - # we want to reboot if cur > 23h or cur < 6h - if [[ "''${current_time}" < "''${upper}" ]] || \ - [[ "''${current_time}" > "''${lower}" ]]; then - do_reboot="true" - else - do_reboot="false" - fi - fi - ''} - - if [ "''${booted}" = "''${built}" ]; then - ${nixos-rebuild} ${cfg.operation} ${toString cfg.flags} - ${lib.optionalString (cfg.rebootWindow != null) '' - elif [ "''${do_reboot}" != true ]; then - echo "Outside of configured reboot window, skipping." - ''} - else - ${shutdown} -r +1 - fi - '' + system.autoUpgrade.flags = ( + if cfg.flake == null then + [ "--no-build-output" ] + ++ lib.optionals (cfg.channel != null) [ + "-I" + "nixpkgs=${cfg.channel}/nixexprs.tar.xz" + ] else - '' - ${nixos-rebuild} ${cfg.operation} ${toString (cfg.flags ++ upgradeFlag)} - ''; + [ + "--refresh" + "--flake ${cfg.flake}" + ] + ); - startAt = cfg.dates; + systemd.services.nixos-upgrade = { + description = "NixOS Upgrade"; - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; - }; + restartIfChanged = false; + unitConfig.X-StopOnRemoval = false; + unitConfig.OnSuccess = lib.optional ( + cfg.runGarbageCollection && config.nix.enable + ) "nix-gc.service"; - systemd.timers.nixos-upgrade = { - timerConfig = { - RandomizedDelaySec = cfg.randomizedDelaySec; - FixedRandomDelay = cfg.fixedRandomDelay; - Persistent = cfg.persistent; + serviceConfig.Type = "oneshot"; + + environment = + config.nix.envVars + // { + inherit (config.environment.sessionVariables) NIX_PATH; + HOME = "/root"; + } + // config.networking.proxy.envVars; + + path = with pkgs; [ + coreutils + gnutar + xz.bin + gzip + gitMinimal + config.nix.package.out + config.programs.ssh.package + config.system.build.nixos-rebuild + config.systemd.package + ]; + + script = + let + upgradeFlag = lib.optional (cfg.channel == null && cfg.flake == null) "--upgrade"; + in + if cfg.allowReboot then + # bash + '' + echo "Running nixos-rebuild boot..." + new_configuration="$( + # For some reason we still get a newline here in the journal between the + # nixos-rebuild stderr output and us echoing the store path that was + # printed on stdout. + # This might have to do with the particular way in which systemd handles + # stdout/stderr, they are unix sockets and not normal streams. + store_path="$(nixos-rebuild boot ${toString (cfg.flags ++ upgradeFlag)})" + echo "$store_path" >&2 + echo "$store_path" + )" + if [ -z "$new_configuration" ]; then + echo "Looks like nixos-rebuild failed... Aborting" + exit 1 + fi + echo "New configuration is $new_configuration" + switch_to_new_configuration="$new_configuration"/bin/switch-to-configuration + + ${lib.optionalString (cfg.rebootWindow != null) # bash + '' + current_time="$(date +%H:%M)" + + lower="${cfg.rebootWindow.lower}" + upper="${cfg.rebootWindow.upper}" + + if [[ "''${lower}" < "''${upper}" ]]; then + if [[ "''${current_time}" > "''${lower}" ]] && [[ "''${current_time}" < "''${upper}" ]]; then + do_reboot="true" + else + do_reboot="false" + fi + else + # lower > upper, so we are crossing midnight (e.g. lower=23h, upper=6h) + # we want to reboot if cur > 23h or cur < 6h + if [[ "''${current_time}" < "''${upper}" ]] || [[ "''${current_time}" > "''${lower}" ]]; then + do_reboot="true" + else + do_reboot="false" + fi + fi + '' + } + + booted_triggers="$(realpath /run/booted-system)/reboot-triggers" + booted_triggers_sha="$( + if [ -f "$booted_triggers" ]; then + sha256sum - < "$booted_triggers" + else + echo 'none' + fi + )" + + new_triggers="$(realpath "$new_configuration")/reboot-triggers" + new_triggers_sha="$( + if [ -f "$new_triggers" ]; then + sha256sum - < "$new_triggers" + else + echo 'none' + fi + )" + + ${lib.optionalString (cfg.operation == "switch") # bash + '' + echo "Running switch-to-configuration check..." + if "$switch_to_new_configuration" check; then + echo "Checking reboot triggers..." + if [ "$new_triggers_sha" == "$booted_triggers_sha" ]; then + echo "Switching into the new generation..." + "$switch_to_new_configuration" ${cfg.operation} + exit 0 + fi + fi + '' + } + ${lib.optionalString (cfg.rebootWindow != null) # bash + '' + if [ "''${do_reboot}" != true ]; then + echo "Outside of configured reboot window, skipping." + exit 0 + fi + '' + } + echo "Scheduling a reboot to activate the new generation" + systemctl reboot --when="+2min" + '' + else + # bash + '' + nixos-rebuild ${cfg.operation} ${toString (cfg.flags ++ upgradeFlag)} + ''; + + startAt = cfg.dates; + + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; }; - }; - }; + systemd.timers.nixos-upgrade = { + timerConfig = { + RandomizedDelaySec = cfg.randomizedDelaySec; + FixedRandomDelay = cfg.fixedRandomDelay; + Persistent = cfg.persistent; + }; + }; + }) + + ]; }