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