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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 = ''
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 fixedRandomDelay = mkOption {
113 default = false;
114 type = types.bool;
115 example = true;
116 description = ''
117 Make the randomized delay consistent between runs.
118 This reduces the jitter between automatic upgrades.
119 See {option}`randomizedDelaySec` for configuring the randomized delay.
120 '';
121 };
122
123 rebootWindow = mkOption {
124 description = ''
125 Define a lower and upper time value (in HH:MM format) which
126 constitute a time window during which reboots are allowed after an upgrade.
127 This option only has an effect when {option}`allowReboot` is enabled.
128 The default value of `null` means that reboots are allowed at any time.
129 '';
130 default = null;
131 example = { lower = "01:00"; upper = "05:00"; };
132 type = with types; nullOr (submodule {
133 options = {
134 lower = mkOption {
135 description = "Lower limit of the reboot window";
136 type = types.strMatching "[[:digit:]]{2}:[[:digit:]]{2}";
137 example = "01:00";
138 };
139
140 upper = mkOption {
141 description = "Upper limit of the reboot window";
142 type = types.strMatching "[[:digit:]]{2}:[[:digit:]]{2}";
143 example = "05:00";
144 };
145 };
146 });
147 };
148
149 persistent = mkOption {
150 default = true;
151 type = types.bool;
152 example = false;
153 description = ''
154 Takes a boolean argument. If true, the time when the service
155 unit was last triggered is stored on disk. When the timer is
156 activated, the service unit is triggered immediately if it
157 would have been triggered at least once during the time when
158 the timer was inactive. Such triggering is nonetheless
159 subject to the delay imposed by RandomizedDelaySec=. This is
160 useful to catch up on missed runs of the service when the
161 system was powered down.
162 '';
163 };
164
165 };
166
167 };
168
169 config = lib.mkIf cfg.enable {
170
171 assertions = [{
172 assertion = !((cfg.channel != null) && (cfg.flake != null));
173 message = ''
174 The options 'system.autoUpgrade.channels' and 'system.autoUpgrade.flake' cannot both be set.
175 '';
176 }];
177
178 system.autoUpgrade.flags = (if cfg.flake == null then
179 [ "--no-build-output" ] ++ optionals (cfg.channel != null) [
180 "-I"
181 "nixpkgs=${cfg.channel}/nixexprs.tar.xz"
182 ]
183 else
184 [ "--flake ${cfg.flake}" ]);
185
186 systemd.services.nixos-upgrade = {
187 description = "NixOS Upgrade";
188
189 restartIfChanged = false;
190 unitConfig.X-StopOnRemoval = false;
191
192 serviceConfig.Type = "oneshot";
193
194 environment = config.nix.envVars // {
195 inherit (config.environment.sessionVariables) NIX_PATH;
196 HOME = "/root";
197 } // config.networking.proxy.envVars;
198
199 path = with pkgs; [
200 coreutils
201 gnutar
202 xz.bin
203 gzip
204 gitMinimal
205 config.nix.package.out
206 config.programs.ssh.package
207 ];
208
209 script = let
210 nixos-rebuild = "${config.system.build.nixos-rebuild}/bin/nixos-rebuild";
211 date = "${pkgs.coreutils}/bin/date";
212 readlink = "${pkgs.coreutils}/bin/readlink";
213 shutdown = "${config.systemd.package}/bin/shutdown";
214 upgradeFlag = optional (cfg.channel == null) "--upgrade";
215 in if cfg.allowReboot then ''
216 ${nixos-rebuild} boot ${toString (cfg.flags ++ upgradeFlag)}
217 booted="$(${readlink} /run/booted-system/{initrd,kernel,kernel-modules})"
218 built="$(${readlink} /nix/var/nix/profiles/system/{initrd,kernel,kernel-modules})"
219
220 ${optionalString (cfg.rebootWindow != null) ''
221 current_time="$(${date} +%H:%M)"
222
223 lower="${cfg.rebootWindow.lower}"
224 upper="${cfg.rebootWindow.upper}"
225
226 if [[ "''${lower}" < "''${upper}" ]]; then
227 if [[ "''${current_time}" > "''${lower}" ]] && \
228 [[ "''${current_time}" < "''${upper}" ]]; then
229 do_reboot="true"
230 else
231 do_reboot="false"
232 fi
233 else
234 # lower > upper, so we are crossing midnight (e.g. lower=23h, upper=6h)
235 # we want to reboot if cur > 23h or cur < 6h
236 if [[ "''${current_time}" < "''${upper}" ]] || \
237 [[ "''${current_time}" > "''${lower}" ]]; then
238 do_reboot="true"
239 else
240 do_reboot="false"
241 fi
242 fi
243 ''}
244
245 if [ "''${booted}" = "''${built}" ]; then
246 ${nixos-rebuild} ${cfg.operation} ${toString cfg.flags}
247 ${optionalString (cfg.rebootWindow != null) ''
248 elif [ "''${do_reboot}" != true ]; then
249 echo "Outside of configured reboot window, skipping."
250 ''}
251 else
252 ${shutdown} -r +1
253 fi
254 '' else ''
255 ${nixos-rebuild} ${cfg.operation} ${toString (cfg.flags ++ upgradeFlag)}
256 '';
257
258 startAt = cfg.dates;
259
260 after = [ "network-online.target" ];
261 wants = [ "network-online.target" ];
262 };
263
264 systemd.timers.nixos-upgrade = {
265 timerConfig = {
266 RandomizedDelaySec = cfg.randomizedDelaySec;
267 FixedRandomDelay = cfg.fixedRandomDelay;
268 Persistent = cfg.persistent;
269 };
270 };
271 };
272
273}
274