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