1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7# TODO: This is not secure, have a look at the file docs/security.txt inside
8# the project sources.
9let
10 cfg = config.power.ups;
11 defaultPort = 3493;
12
13 envVars = {
14 NUT_CONFPATH = "/etc/nut";
15 NUT_STATEPATH = "/var/lib/nut";
16 };
17
18 nutFormat = {
19
20 type =
21 with lib.types;
22 let
23
24 singleAtom =
25 nullOr (oneOf [
26 bool
27 int
28 float
29 str
30 ])
31 // {
32 description = "atom (null, bool, int, float or string)";
33 };
34
35 in
36 attrsOf (oneOf [
37 singleAtom
38 (listOf (nonEmptyListOf singleAtom))
39 ]);
40
41 generate =
42 name: value:
43 let
44 normalizedValue = lib.mapAttrs (
45 key: val:
46 if lib.isList val then
47 lib.forEach val (elem: if lib.isList elem then elem else [ elem ])
48 else if val == null then
49 [ ]
50 else
51 [ [ val ] ]
52 ) value;
53
54 mkValueString = lib.concatMapStringsSep " " (
55 v:
56 let
57 str = lib.generators.mkValueStringDefault { } v;
58 in
59 # Quote the value if it has spaces and isn't already quoted.
60 if (lib.hasInfix " " str) && !(lib.hasPrefix "\"" str && lib.hasSuffix "\"" str) then
61 "\"${str}\""
62 else
63 str
64 );
65
66 in
67 pkgs.writeText name (
68 lib.generators.toKeyValue {
69 mkKeyValue = lib.generators.mkKeyValueDefault { inherit mkValueString; } " ";
70 listsAsDuplicateKeys = true;
71 } normalizedValue
72 );
73
74 };
75
76 installSecrets =
77 source: target: owner: secrets:
78 pkgs.writeShellScript "installSecrets.sh" ''
79 install -m0600 -o${owner} -D ${source} "${target}"
80 ${lib.concatLines (
81 lib.forEach secrets (name: ''
82 ${pkgs.replace-secret}/bin/replace-secret \
83 '@${name}@' \
84 "$CREDENTIALS_DIRECTORY/${name}" \
85 "${target}"
86 '')
87 )}
88 chmod u-w "${target}"
89 '';
90
91 upsmonConf = nutFormat.generate "upsmon.conf" cfg.upsmon.settings;
92
93 upsdUsers = pkgs.writeText "upsd.users" (
94 let
95 # This looks like INI, but it's not quite because the
96 # 'upsmon' option lacks a '='. See: man upsd.users
97 userConfig =
98 name: user:
99 lib.concatStringsSep "\n " (
100 lib.concatLists [
101 [
102 "[${name}]"
103 "password = \"@upsdusers_password_${name}@\""
104 ]
105 (lib.optional (user.upsmon != null) "upsmon ${user.upsmon}")
106 (lib.forEach user.actions (action: "actions = ${action}"))
107 (lib.forEach user.instcmds (instcmd: "instcmds = ${instcmd}"))
108 ]
109 );
110 in
111 lib.concatStringsSep "\n\n" (lib.mapAttrsToList userConfig cfg.users)
112 );
113
114 upsOptions =
115 { name, config, ... }:
116 {
117 options = {
118 # This can be inferred from the UPS model by looking at
119 # /nix/store/nut/share/driver.list
120 driver = lib.mkOption {
121 type = lib.types.str;
122 description = ''
123 Specify the program to run to talk to this UPS. apcsmart,
124 bestups, and sec are some examples.
125 '';
126 };
127
128 port = lib.mkOption {
129 type = lib.types.str;
130 description = ''
131 The serial port to which your UPS is connected. /dev/ttyS0 is
132 usually the first port on Linux boxes, for example.
133 '';
134 };
135
136 shutdownOrder = lib.mkOption {
137 default = 0;
138 type = lib.types.int;
139 description = ''
140 When you have multiple UPSes on your system, you usually need to
141 turn them off in a certain order. upsdrvctl shuts down all the
142 0s, then the 1s, 2s, and so on. To exclude a UPS from the
143 shutdown sequence, set this to -1.
144 '';
145 };
146
147 maxStartDelay = lib.mkOption {
148 default = null;
149 type = lib.types.uniq (lib.types.nullOr lib.types.int);
150 description = ''
151 This can be set as a global variable above your first UPS
152 definition and it can also be set in a UPS section. This value
153 controls how long upsdrvctl will wait for the driver to finish
154 starting. This keeps your system from getting stuck due to a
155 broken driver or UPS.
156 '';
157 };
158
159 description = lib.mkOption {
160 default = "";
161 type = lib.types.str;
162 description = ''
163 Description of the UPS.
164 '';
165 };
166
167 directives = lib.mkOption {
168 default = [ ];
169 type = lib.types.listOf lib.types.str;
170 description = ''
171 List of configuration directives for this UPS.
172 '';
173 };
174
175 summary = lib.mkOption {
176 default = "";
177 type = lib.types.lines;
178 description = ''
179 Lines which would be added inside ups.conf for handling this UPS.
180 '';
181 };
182
183 };
184
185 config = {
186 directives = lib.mkOrder 10 (
187 [
188 "driver = ${config.driver}"
189 "port = ${config.port}"
190 ''desc = "${config.description}"''
191 "sdorder = ${toString config.shutdownOrder}"
192 ]
193 ++ (lib.optional (config.maxStartDelay != null) "maxstartdelay = ${toString config.maxStartDelay}")
194 );
195
196 summary = lib.concatStringsSep "\n " ([ "[${name}]" ] ++ config.directives);
197 };
198 };
199
200 listenOptions = {
201 options = {
202 address = lib.mkOption {
203 type = lib.types.str;
204 description = ''
205 Address of the interface for `upsd` to listen on.
206 See `man upsd.conf` for details.
207 '';
208 };
209
210 port = lib.mkOption {
211 type = lib.types.port;
212 default = defaultPort;
213 description = ''
214 TCP port for `upsd` to listen on.
215 See `man upsd.conf` for details.
216 '';
217 };
218 };
219 };
220
221 upsdOptions = {
222 options = {
223 enable = lib.mkOption {
224 type = lib.types.bool;
225 defaultText = lib.literalMD "`true` if `mode` is one of `standalone`, `netserver`";
226 description = "Whether to enable `upsd`.";
227 };
228
229 listen = lib.mkOption {
230 type = with lib.types; listOf (submodule listenOptions);
231 default = [ ];
232 example = [
233 {
234 address = "192.168.50.1";
235 }
236 {
237 address = "::1";
238 port = 5923;
239 }
240 ];
241 description = ''
242 Address of the interface for `upsd` to listen on.
243 See `man upsd` for details`.
244 '';
245 };
246
247 extraConfig = lib.mkOption {
248 type = lib.types.lines;
249 default = "";
250 description = ''
251 Additional lines to add to `upsd.conf`.
252 '';
253 };
254 };
255
256 config = {
257 enable = lib.mkDefault (
258 lib.elem cfg.mode [
259 "standalone"
260 "netserver"
261 ]
262 );
263 };
264 };
265
266 monitorOptions =
267 { name, config, ... }:
268 {
269 options = {
270 system = lib.mkOption {
271 type = lib.types.str;
272 default = name;
273 description = ''
274 Identifier of the UPS to monitor, in this form: `<upsname>[@<hostname>[:<port>]]`
275 See `upsmon.conf` for details.
276 '';
277 };
278
279 powerValue = lib.mkOption {
280 type = lib.types.int;
281 default = 1;
282 description = ''
283 Number of power supplies that the UPS feeds on this system.
284 See `upsmon.conf` for details.
285 '';
286 };
287
288 user = lib.mkOption {
289 type = lib.types.str;
290 description = ''
291 Username from `upsd.users` for accessing this UPS.
292 See `upsmon.conf` for details.
293 '';
294 };
295
296 passwordFile = lib.mkOption {
297 type = lib.types.str;
298 defaultText = lib.literalMD "power.ups.users.\${user}.passwordFile";
299 description = ''
300 The full path to a file containing the password from
301 `upsd.users` for accessing this UPS. The password file
302 is read on service start.
303 See `upsmon.conf` for details.
304 '';
305 };
306
307 type = lib.mkOption {
308 type = lib.types.str;
309 default = "master";
310 description = ''
311 The relationship with `upsd`.
312 See `upsmon.conf` for details.
313 '';
314 };
315 };
316
317 config = {
318 passwordFile = lib.mkDefault cfg.users.${config.user}.passwordFile;
319 };
320 };
321
322 upsmonOptions = {
323 options = {
324 enable = lib.mkOption {
325 type = lib.types.bool;
326 defaultText = lib.literalMD "`true` if `mode` is one of `standalone`, `netserver`, `netclient`";
327 description = "Whether to enable `upsmon`.";
328 };
329
330 user = lib.mkOption {
331 type = lib.types.str;
332 default = "nutmon";
333 description = ''
334 User to run `upsmon` as. `upsmon.conf` will have its owner set to this
335 user. If not specified, a default user will be created.
336 '';
337 };
338 group = lib.mkOption {
339 type = lib.types.str;
340 default = "nutmon";
341 description = ''
342 Group for the default `nutmon` user. If the default user is created
343 and this is not specified, a default group will be created.
344 '';
345 };
346
347 monitor = lib.mkOption {
348 type = with lib.types; attrsOf (submodule monitorOptions);
349 default = { };
350 description = ''
351 Set of UPS to monitor. See `man upsmon.conf` for details.
352 '';
353 };
354
355 settings = lib.mkOption {
356 type = nutFormat.type;
357 default = { };
358 defaultText = lib.literalMD ''
359 {
360 MINSUPPLIES = 1;
361 MONITOR = <generated from config.power.ups.upsmon.monitor>
362 NOTIFYCMD = "''${cfg.package}/bin/upssched";
363 POWERDOWNFLAG = "/run/killpower";
364 SHUTDOWNCMD = "''${pkgs.systemd}/bin/shutdown now";
365 }
366 '';
367 description = "Additional settings to add to `upsmon.conf`.";
368 example = lib.literalMD ''
369 {
370 MINSUPPLIES = 2;
371 NOTIFYFLAG = [
372 [ "ONLINE" "SYSLOG+EXEC" ]
373 [ "ONBATT" "SYSLOG+EXEC" ]
374 ];
375 }
376 '';
377 };
378 };
379
380 config = {
381 enable = lib.mkDefault (
382 lib.elem cfg.mode [
383 "standalone"
384 "netserver"
385 "netclient"
386 ]
387 );
388 settings = {
389 MINSUPPLIES = lib.mkDefault 1;
390 MONITOR = lib.flip lib.mapAttrsToList cfg.upsmon.monitor (
391 name: monitor: with monitor; [
392 system
393 powerValue
394 user
395 "\"@upsmon_password_${name}@\""
396 type
397 ]
398 );
399 NOTIFYCMD = lib.mkDefault "${cfg.package}/bin/upssched";
400 POWERDOWNFLAG = lib.mkDefault "/run/killpower";
401 SHUTDOWNCMD = lib.mkDefault "${pkgs.systemd}/bin/shutdown now";
402 };
403 };
404 };
405
406 userOptions = {
407 options = {
408 passwordFile = lib.mkOption {
409 type = lib.types.str;
410 description = ''
411 The full path to a file that contains the user's (clear text)
412 password. The password file is read on service start.
413 '';
414 };
415
416 actions = lib.mkOption {
417 type = with lib.types; listOf str;
418 default = [ ];
419 description = ''
420 Allow the user to do certain things with upsd.
421 See `man upsd.users` for details.
422 '';
423 };
424
425 instcmds = lib.mkOption {
426 type = with lib.types; listOf str;
427 default = [ ];
428 description = ''
429 Let the user initiate specific instant commands. Use "ALL" to grant all commands automatically. For the full list of what your UPS supports, use "upscmd -l".
430 See `man upsd.users` for details.
431 '';
432 };
433
434 upsmon = lib.mkOption {
435 type =
436 with lib.types;
437 nullOr (enum [
438 "primary"
439 "secondary"
440 ]);
441 default = null;
442 description = ''
443 Add the necessary actions for a upsmon process to work.
444 See `man upsd.users` for details.
445 '';
446 };
447 };
448 };
449
450in
451
452{
453 options = {
454 # powerManagement.powerDownCommands
455
456 power.ups = {
457 enable = lib.mkEnableOption ''
458 support for Power Devices, such as Uninterruptible Power
459 Supplies, Power Distribution Units and Solar Controllers
460 '';
461
462 package = lib.mkPackageOption pkgs "nut" { };
463
464 mode = lib.mkOption {
465 default = "standalone";
466 type = lib.types.enum [
467 "none"
468 "standalone"
469 "netserver"
470 "netclient"
471 ];
472 description = ''
473 The MODE determines which part of the NUT is to be started, and
474 which configuration files must be modified.
475
476 The values of MODE can be:
477
478 - none: NUT is not configured, or use the Integrated Power
479 Management, or use some external system to startup NUT
480 components. So nothing is to be started.
481
482 - standalone: This mode address a local only configuration, with 1
483 UPS protecting the local system. This implies to start the 3 NUT
484 layers (driver, upsd and upsmon) and the matching configuration
485 files. This mode can also address UPS redundancy.
486
487 - netserver: same as for the standalone configuration, but also
488 need some more ACLs and possibly a specific LISTEN directive in
489 upsd.conf. Since this MODE is opened to the network, a special
490 care should be applied to security concerns.
491
492 - netclient: this mode only requires upsmon.
493 '';
494 };
495
496 schedulerRules = lib.mkOption {
497 example = "/etc/nixos/upssched.conf";
498 type = lib.types.str;
499 description = ''
500 File which contains the rules to handle UPS events.
501 '';
502 };
503
504 openFirewall = lib.mkOption {
505 type = lib.types.bool;
506 default = false;
507 description = ''
508 Open ports in the firewall for `upsd`.
509 '';
510 };
511
512 maxStartDelay = lib.mkOption {
513 default = 45;
514 type = lib.types.int;
515 description = ''
516 This can be set as a global variable above your first UPS
517 definition and it can also be set in a UPS section. This value
518 controls how long upsdrvctl will wait for the driver to finish
519 starting. This keeps your system from getting stuck due to a
520 broken driver or UPS.
521 '';
522 };
523
524 upsmon = lib.mkOption {
525 default = { };
526 description = ''
527 Options for the `upsmon.conf` configuration file.
528 '';
529 type = lib.types.submodule upsmonOptions;
530 };
531
532 upsd = lib.mkOption {
533 default = { };
534 description = ''
535 Options for the `upsd.conf` configuration file.
536 '';
537 type = lib.types.submodule upsdOptions;
538 };
539
540 ups = lib.mkOption {
541 default = { };
542 # see nut/etc/ups.conf.sample
543 description = ''
544 This is where you configure all the UPSes that this system will be
545 monitoring directly. These are usually attached to serial ports,
546 but USB devices are also supported.
547 '';
548 type = with lib.types; attrsOf (submodule upsOptions);
549 };
550
551 users = lib.mkOption {
552 default = { };
553 description = ''
554 Users that can access upsd. See `man upsd.users`.
555 '';
556 type = with lib.types; attrsOf (submodule userOptions);
557 };
558
559 };
560 };
561
562 config = lib.mkIf cfg.enable {
563
564 assertions = [
565 (
566 let
567 totalPowerValue = lib.foldl' lib.add 0 (
568 map (monitor: monitor.powerValue) (lib.attrValues cfg.upsmon.monitor)
569 );
570 minSupplies = cfg.upsmon.settings.MINSUPPLIES;
571 in
572 lib.mkIf cfg.upsmon.enable {
573 assertion = totalPowerValue >= minSupplies;
574 message = ''
575 `power.ups.upsmon`: Total configured power value (${toString totalPowerValue}) must be at least MINSUPPLIES (${toString minSupplies}).
576 '';
577 }
578 )
579 ];
580
581 # For interactive use.
582 environment.systemPackages = [ cfg.package ];
583 environment.variables = envVars;
584
585 networking.firewall = lib.mkIf cfg.openFirewall {
586 allowedTCPPorts =
587 if cfg.upsd.listen == [ ] then
588 [ defaultPort ]
589 else
590 lib.unique (lib.forEach cfg.upsd.listen (listen: listen.port));
591 };
592
593 systemd.slices.system-ups = {
594 description = "Network UPS Tools (NUT) Slice";
595 documentation = [ "https://networkupstools.org/" ];
596 };
597
598 systemd.services.upsmon =
599 let
600 secrets = lib.mapAttrsToList (name: monitor: "upsmon_password_${name}") cfg.upsmon.monitor;
601 createUpsmonConf = installSecrets upsmonConf "/run/nut/upsmon.conf" cfg.upsmon.user secrets;
602 in
603 {
604 enable = cfg.upsmon.enable;
605 description = "Uninterruptible Power Supplies (Monitor)";
606 after = [ "network.target" ];
607 wantedBy = [ "multi-user.target" ];
608 serviceConfig = {
609 Type = "forking";
610 ExecStartPre = "${createUpsmonConf}";
611 ExecStart = "${cfg.package}/sbin/upsmon -u ${cfg.upsmon.user}";
612 ExecReload = "${cfg.package}/sbin/upsmon -c reload";
613 LoadCredential = lib.mapAttrsToList (
614 name: monitor: "upsmon_password_${name}:${monitor.passwordFile}"
615 ) cfg.upsmon.monitor;
616 Slice = "system-ups.slice";
617 };
618 environment = envVars;
619 };
620
621 systemd.services.upsd =
622 let
623 secrets = lib.mapAttrsToList (name: user: "upsdusers_password_${name}") cfg.users;
624 createUpsdUsers = installSecrets upsdUsers "/run/nut/upsd.users" "root" secrets;
625 in
626 {
627 enable = cfg.upsd.enable;
628 description = "Uninterruptible Power Supplies (Daemon)";
629 after = [
630 "network.target"
631 "upsmon.service"
632 ];
633 wantedBy = [ "multi-user.target" ];
634 serviceConfig = {
635 Type = "forking";
636 ExecStartPre = "${createUpsdUsers}";
637 # TODO: replace 'root' by another username.
638 ExecStart = "${cfg.package}/sbin/upsd -u root";
639 ExecReload = "${cfg.package}/sbin/upsd -c reload";
640 LoadCredential = lib.mapAttrsToList (
641 name: user: "upsdusers_password_${name}:${user.passwordFile}"
642 ) cfg.users;
643 Slice = "system-ups.slice";
644 };
645 environment = envVars;
646 restartTriggers = [
647 config.environment.etc."nut/upsd.conf".source
648 ];
649 };
650
651 systemd.services.upsdrv = {
652 enable = cfg.upsd.enable;
653 description = "Uninterruptible Power Supplies (Register all UPS)";
654 after = [ "upsd.service" ];
655 wantedBy = [ "multi-user.target" ];
656 serviceConfig = {
657 Type = "oneshot";
658 RemainAfterExit = true;
659 # TODO: replace 'root' by another username.
660 ExecStart = "${cfg.package}/bin/upsdrvctl -u root start";
661 Slice = "system-ups.slice";
662 };
663 environment = envVars;
664 restartTriggers = [
665 config.environment.etc."nut/ups.conf".source
666 ];
667 };
668
669 systemd.services.ups-killpower = lib.mkIf (cfg.upsmon.settings.POWERDOWNFLAG != null) {
670 enable = cfg.upsd.enable;
671 description = "UPS Kill Power";
672 wantedBy = [ "shutdown.target" ];
673 after = [ "shutdown.target" ];
674 before = [ "final.target" ];
675 unitConfig = {
676 ConditionPathExists = cfg.upsmon.settings.POWERDOWNFLAG;
677 DefaultDependencies = "no";
678 };
679 environment = envVars;
680 serviceConfig = {
681 Type = "oneshot";
682 ExecStart = "${cfg.package}/bin/upsdrvctl shutdown";
683 Slice = "system-ups.slice";
684 };
685 };
686
687 environment.etc = {
688 "nut/nut.conf".source = pkgs.writeText "nut.conf" ''
689 MODE = ${cfg.mode}
690 '';
691 "nut/ups.conf".source = pkgs.writeText "ups.conf" ''
692 maxstartdelay = ${toString cfg.maxStartDelay}
693
694 ${lib.concatStringsSep "\n\n" (lib.forEach (lib.attrValues cfg.ups) (ups: ups.summary))}
695 '';
696 "nut/upsd.conf".source = pkgs.writeText "upsd.conf" ''
697 ${lib.concatStringsSep "\n" (
698 lib.forEach cfg.upsd.listen (listen: "LISTEN ${listen.address} ${toString listen.port}")
699 )}
700 ${cfg.upsd.extraConfig}
701 '';
702 "nut/upssched.conf".source = cfg.schedulerRules;
703 "nut/upsd.users".source = "/run/nut/upsd.users";
704 "nut/upsmon.conf".source = "/run/nut/upsmon.conf";
705 };
706
707 power.ups.schedulerRules = lib.mkDefault "${cfg.package}/etc/upssched.conf.sample";
708
709 systemd.tmpfiles.rules = [
710 "d /var/state/ups -"
711 "d /var/lib/nut 700"
712 ];
713
714 services.udev.packages = [ cfg.package ];
715
716 users.users.nutmon = lib.mkIf (cfg.upsmon.user == "nutmon") {
717 isSystemUser = true;
718 group = cfg.upsmon.group;
719 };
720 users.groups.nutmon = lib.mkIf (cfg.upsmon.user == "nutmon" && cfg.upsmon.group == "nutmon") { };
721
722 };
723}