1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let cfg = config.system.autoUpgrade;
6
7in {
8
9 options = {
10
11 system.autoUpgrade = {
12
13 enable = mkOption {
14 type = types.bool;
15 default = false;
16 description = lib.mdDoc ''
17 Whether to periodically upgrade NixOS to the latest
18 version. If enabled, a systemd timer will run
19 `nixos-rebuild switch --upgrade` once a
20 day.
21 '';
22 };
23
24 operation = mkOption {
25 type = types.enum ["switch" "boot"];
26 default = "switch";
27 example = "boot";
28 description = lib.mdDoc ''
29 Whether to run
30 `nixos-rebuild switch --upgrade` or run
31 `nixos-rebuild boot --upgrade`
32 '';
33 };
34
35 flake = mkOption {
36 type = types.nullOr types.str;
37 default = null;
38 example = "github:kloenk/nix";
39 description = lib.mdDoc ''
40 The Flake URI of the NixOS configuration to build.
41 Disables the option {option}`system.autoUpgrade.channel`.
42 '';
43 };
44
45 channel = mkOption {
46 type = types.nullOr types.str;
47 default = null;
48 example = "https://nixos.org/channels/nixos-14.12-small";
49 description = lib.mdDoc ''
50 The URI of the NixOS channel to use for automatic
51 upgrades. By default, this is the channel set using
52 {command}`nix-channel` (run `nix-channel --list`
53 to see the current value).
54 '';
55 };
56
57 flags = mkOption {
58 type = types.listOf types.str;
59 default = [ ];
60 example = [
61 "-I"
62 "stuff=/home/alice/nixos-stuff"
63 "--option"
64 "extra-binary-caches"
65 "http://my-cache.example.org/"
66 ];
67 description = lib.mdDoc ''
68 Any additional flags passed to {command}`nixos-rebuild`.
69
70 If you are using flakes and use a local repo you can add
71 {command}`[ "--update-input" "nixpkgs" "--commit-lock-file" ]`
72 to update nixpkgs.
73 '';
74 };
75
76 dates = mkOption {
77 type = types.str;
78 default = "04:40";
79 example = "daily";
80 description = lib.mdDoc ''
81 How often or when upgrade occurs. For most desktop and server systems
82 a sufficient upgrade frequency is once a day.
83
84 The format is described in
85 {manpage}`systemd.time(7)`.
86 '';
87 };
88
89 allowReboot = mkOption {
90 default = false;
91 type = types.bool;
92 description = lib.mdDoc ''
93 Reboot the system into the new generation instead of a switch
94 if the new generation uses a different kernel, kernel modules
95 or initrd than the booted system.
96 See {option}`rebootWindow` for configuring the times at which a reboot is allowed.
97 '';
98 };
99
100 randomizedDelaySec = mkOption {
101 default = "0";
102 type = types.str;
103 example = "45min";
104 description = lib.mdDoc ''
105 Add a randomized delay before each automatic upgrade.
106 The delay will be chosen between zero and this value.
107 This value must be a time span in the format specified by
108 {manpage}`systemd.time(7)`
109 '';
110 };
111
112 rebootWindow = mkOption {
113 description = lib.mdDoc ''
114 Define a lower and upper time value (in HH:MM format) which
115 constitute a time window during which reboots are allowed after an upgrade.
116 This option only has an effect when {option}`allowReboot` is enabled.
117 The default value of `null` means that reboots are allowed at any time.
118 '';
119 default = null;
120 example = { lower = "01:00"; upper = "05:00"; };
121 type = with types; nullOr (submodule {
122 options = {
123 lower = mkOption {
124 description = lib.mdDoc "Lower limit of the reboot window";
125 type = types.strMatching "[[:digit:]]{2}:[[:digit:]]{2}";
126 example = "01:00";
127 };
128
129 upper = mkOption {
130 description = lib.mdDoc "Upper limit of the reboot window";
131 type = types.strMatching "[[:digit:]]{2}:[[:digit:]]{2}";
132 example = "05:00";
133 };
134 };
135 });
136 };
137
138 persistent = mkOption {
139 default = true;
140 type = types.bool;
141 example = false;
142 description = lib.mdDoc ''
143 Takes a boolean argument. If true, the time when the service
144 unit was last triggered is stored on disk. When the timer is
145 activated, the service unit is triggered immediately if it
146 would have been triggered at least once during the time when
147 the timer was inactive. Such triggering is nonetheless
148 subject to the delay imposed by RandomizedDelaySec=. This is
149 useful to catch up on missed runs of the service when the
150 system was powered down.
151 '';
152 };
153
154 };
155
156 };
157
158 config = lib.mkIf cfg.enable {
159
160 assertions = [{
161 assertion = !((cfg.channel != null) && (cfg.flake != null));
162 message = ''
163 The options 'system.autoUpgrade.channels' and 'system.autoUpgrade.flake' cannot both be set.
164 '';
165 }];
166
167 system.autoUpgrade.flags = (if cfg.flake == null then
168 [ "--no-build-output" ] ++ optionals (cfg.channel != null) [
169 "-I"
170 "nixpkgs=${cfg.channel}/nixexprs.tar.xz"
171 ]
172 else
173 [ "--flake ${cfg.flake}" ]);
174
175 systemd.services.nixos-upgrade = {
176 description = "NixOS Upgrade";
177
178 restartIfChanged = false;
179 unitConfig.X-StopOnRemoval = false;
180
181 serviceConfig.Type = "oneshot";
182
183 environment = config.nix.envVars // {
184 inherit (config.environment.sessionVariables) NIX_PATH;
185 HOME = "/root";
186 } // config.networking.proxy.envVars;
187
188 path = with pkgs; [
189 coreutils
190 gnutar
191 xz.bin
192 gzip
193 gitMinimal
194 config.nix.package.out
195 config.programs.ssh.package
196 ];
197
198 script = let
199 nixos-rebuild = "${config.system.build.nixos-rebuild}/bin/nixos-rebuild";
200 date = "${pkgs.coreutils}/bin/date";
201 readlink = "${pkgs.coreutils}/bin/readlink";
202 shutdown = "${config.systemd.package}/bin/shutdown";
203 upgradeFlag = optional (cfg.channel == null) "--upgrade";
204 in if cfg.allowReboot then ''
205 ${nixos-rebuild} boot ${toString (cfg.flags ++ upgradeFlag)}
206 booted="$(${readlink} /run/booted-system/{initrd,kernel,kernel-modules})"
207 built="$(${readlink} /nix/var/nix/profiles/system/{initrd,kernel,kernel-modules})"
208
209 ${optionalString (cfg.rebootWindow != null) ''
210 current_time="$(${date} +%H:%M)"
211
212 lower="${cfg.rebootWindow.lower}"
213 upper="${cfg.rebootWindow.upper}"
214
215 if [[ "''${lower}" < "''${upper}" ]]; then
216 if [[ "''${current_time}" > "''${lower}" ]] && \
217 [[ "''${current_time}" < "''${upper}" ]]; then
218 do_reboot="true"
219 else
220 do_reboot="false"
221 fi
222 else
223 # lower > upper, so we are crossing midnight (e.g. lower=23h, upper=6h)
224 # we want to reboot if cur > 23h or cur < 6h
225 if [[ "''${current_time}" < "''${upper}" ]] || \
226 [[ "''${current_time}" > "''${lower}" ]]; then
227 do_reboot="true"
228 else
229 do_reboot="false"
230 fi
231 fi
232 ''}
233
234 if [ "''${booted}" = "''${built}" ]; then
235 ${nixos-rebuild} ${cfg.operation} ${toString cfg.flags}
236 ${optionalString (cfg.rebootWindow != null) ''
237 elif [ "''${do_reboot}" != true ]; then
238 echo "Outside of configured reboot window, skipping."
239 ''}
240 else
241 ${shutdown} -r +1
242 fi
243 '' else ''
244 ${nixos-rebuild} ${cfg.operation} ${toString (cfg.flags ++ upgradeFlag)}
245 '';
246
247 startAt = cfg.dates;
248
249 after = [ "network-online.target" ];
250 wants = [ "network-online.target" ];
251 };
252
253 systemd.timers.nixos-upgrade = {
254 timerConfig = {
255 RandomizedDelaySec = cfg.randomizedDelaySec;
256 Persistent = cfg.persistent;
257 };
258 };
259 };
260
261}
262