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