vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / monitoring / ups.nix
blob9361393d91af75767e35d5050bf759018a930ced
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             RUN_AS_USER = "root";
313             NOTIFYCMD = "''${pkgs.nut}/bin/upssched";
314             SHUTDOWNCMD = "''${pkgs.systemd}/bin/shutdown now";
315           }
316         '';
317         description = "Additional settings to add to `upsmon.conf`.";
318         example = lib.literalMD ''
319           {
320             MINSUPPLIES = 2;
321             NOTIFYFLAG = [
322               [ "ONLINE" "SYSLOG+EXEC" ]
323               [ "ONBATT" "SYSLOG+EXEC" ]
324             ];
325           }
326         '';
327       };
328     };
330     config = {
331       enable = lib.mkDefault (lib.elem cfg.mode [ "standalone" "netserver" "netclient" ]);
332       settings = {
333         RUN_AS_USER = "root"; # TODO: replace 'root' by another username.
334         MINSUPPLIES = lib.mkDefault 1;
335         NOTIFYCMD = lib.mkDefault "${pkgs.nut}/bin/upssched";
336         SHUTDOWNCMD = lib.mkDefault "${pkgs.systemd}/bin/shutdown now";
337         MONITOR = lib.flip lib.mapAttrsToList cfg.upsmon.monitor (name: monitor: with monitor; [ system powerValue user "\"@upsmon_password_${name}@\"" type ]);
338       };
339     };
340   };
342   userOptions = {
343     options = {
344       passwordFile = lib.mkOption {
345         type = lib.types.str;
346         description = ''
347           The full path to a file that contains the user's (clear text)
348           password. The password file is read on service start.
349         '';
350       };
352       actions = lib.mkOption {
353         type = with lib.types; listOf str;
354         default = [];
355         description = ''
356           Allow the user to do certain things with upsd.
357           See `man upsd.users` for details.
358         '';
359       };
361       instcmds = lib.mkOption {
362         type = with lib.types; listOf str;
363         default = [];
364         description = ''
365           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".
366           See `man upsd.users` for details.
367         '';
368       };
370       upsmon = lib.mkOption {
371         type = with lib.types; nullOr (enum [ "primary" "secondary" ]);
372         default = null;
373         description = ''
374           Add the necessary actions for a upsmon process to work.
375           See `man upsd.users` for details.
376         '';
377       };
378     };
379   };
385   options = {
386     # powerManagement.powerDownCommands
388     power.ups = {
389       enable = lib.mkEnableOption ''
390         support for Power Devices, such as Uninterruptible Power
391         Supplies, Power Distribution Units and Solar Controllers
392       '';
394       mode = lib.mkOption {
395         default = "standalone";
396         type = lib.types.enum [ "none" "standalone" "netserver" "netclient" ];
397         description = ''
398           The MODE determines which part of the NUT is to be started, and
399           which configuration files must be modified.
401           The values of MODE can be:
403           - none: NUT is not configured, or use the Integrated Power
404             Management, or use some external system to startup NUT
405             components. So nothing is to be started.
407           - standalone: This mode address a local only configuration, with 1
408             UPS protecting the local system. This implies to start the 3 NUT
409             layers (driver, upsd and upsmon) and the matching configuration
410             files. This mode can also address UPS redundancy.
412           - netserver: same as for the standalone configuration, but also
413             need some more ACLs and possibly a specific LISTEN directive in
414             upsd.conf.  Since this MODE is opened to the network, a special
415             care should be applied to security concerns.
417           - netclient: this mode only requires upsmon.
418         '';
419       };
421       schedulerRules = lib.mkOption {
422         example = "/etc/nixos/upssched.conf";
423         type = lib.types.str;
424         description = ''
425           File which contains the rules to handle UPS events.
426         '';
427       };
429       openFirewall = lib.mkOption {
430         type = lib.types.bool;
431         default = false;
432         description = ''
433           Open ports in the firewall for `upsd`.
434         '';
435       };
437       maxStartDelay = lib.mkOption {
438         default = 45;
439         type = lib.types.int;
440         description = ''
441           This can be set as a global variable above your first UPS
442           definition and it can also be set in a UPS section.  This value
443           controls how long upsdrvctl will wait for the driver to finish
444           starting.  This keeps your system from getting stuck due to a
445           broken driver or UPS.
446         '';
447       };
449       upsmon = lib.mkOption {
450         default = {};
451         description = ''
452           Options for the `upsmon.conf` configuration file.
453         '';
454         type = lib.types.submodule upsmonOptions;
455       };
457       upsd = lib.mkOption {
458         default = {};
459         description = ''
460           Options for the `upsd.conf` configuration file.
461         '';
462         type = lib.types.submodule upsdOptions;
463       };
465       ups = lib.mkOption {
466         default = {};
467         # see nut/etc/ups.conf.sample
468         description = ''
469           This is where you configure all the UPSes that this system will be
470           monitoring directly.  These are usually attached to serial ports,
471           but USB devices are also supported.
472         '';
473         type = with lib.types; attrsOf (submodule upsOptions);
474       };
476       users = lib.mkOption {
477         default = {};
478         description = ''
479           Users that can access upsd. See `man upsd.users`.
480         '';
481         type = with lib.types; attrsOf (submodule userOptions);
482       };
484     };
485   };
487   config = lib.mkIf cfg.enable {
489     assertions = [
490       (let
491         totalPowerValue = lib.foldl' lib.add 0 (map (monitor: monitor.powerValue) (lib.attrValues cfg.upsmon.monitor));
492         minSupplies = cfg.upsmon.settings.MINSUPPLIES;
493       in lib.mkIf cfg.upsmon.enable {
494         assertion = totalPowerValue >= minSupplies;
495         message = ''
496           `power.ups.upsmon`: Total configured power value (${toString totalPowerValue}) must be at least MINSUPPLIES (${toString minSupplies}).
497         '';
498       })
499     ];
501     # For interactive use.
502     environment.systemPackages = [ pkgs.nut ];
503     environment.variables = envVars;
505     networking.firewall = lib.mkIf cfg.openFirewall {
506       allowedTCPPorts =
507         if cfg.upsd.listen == []
508         then [ defaultPort ]
509         else lib.unique (lib.forEach cfg.upsd.listen (listen: listen.port));
510     };
512     systemd.slices.system-ups = {
513       description = "Network UPS Tools (NUT) Slice";
514       documentation = [ "https://networkupstools.org/" ];
515     };
517     systemd.services.upsmon = let
518       secrets = lib.mapAttrsToList (name: monitor: "upsmon_password_${name}") cfg.upsmon.monitor;
519       createUpsmonConf = installSecrets upsmonConf "/run/nut/upsmon.conf" secrets;
520     in {
521       enable = cfg.upsmon.enable;
522       description = "Uninterruptible Power Supplies (Monitor)";
523       after = [ "network.target" ];
524       wantedBy = [ "multi-user.target" ];
525       serviceConfig = {
526         Type = "forking";
527         ExecStartPre = "${createUpsmonConf}";
528         ExecStart = "${pkgs.nut}/sbin/upsmon";
529         ExecReload = "${pkgs.nut}/sbin/upsmon -c reload";
530         LoadCredential = lib.mapAttrsToList (name: monitor: "upsmon_password_${name}:${monitor.passwordFile}") cfg.upsmon.monitor;
531         Slice = "system-ups.slice";
532       };
533       environment = envVars;
534     };
536     systemd.services.upsd = let
537       secrets = lib.mapAttrsToList (name: user: "upsdusers_password_${name}") cfg.users;
538       createUpsdUsers = installSecrets upsdUsers "/run/nut/upsd.users" secrets;
539     in {
540       enable = cfg.upsd.enable;
541       description = "Uninterruptible Power Supplies (Daemon)";
542       after = [ "network.target" "upsmon.service" ];
543       wantedBy = [ "multi-user.target" ];
544       serviceConfig = {
545         Type = "forking";
546         ExecStartPre = "${createUpsdUsers}";
547         # TODO: replace 'root' by another username.
548         ExecStart = "${pkgs.nut}/sbin/upsd -u root";
549         ExecReload = "${pkgs.nut}/sbin/upsd -c reload";
550         LoadCredential = lib.mapAttrsToList (name: user: "upsdusers_password_${name}:${user.passwordFile}") cfg.users;
551         Slice = "system-ups.slice";
552       };
553       environment = envVars;
554       restartTriggers = [
555         config.environment.etc."nut/upsd.conf".source
556       ];
557     };
559     systemd.services.upsdrv = {
560       enable = cfg.upsd.enable;
561       description = "Uninterruptible Power Supplies (Register all UPS)";
562       after = [ "upsd.service" ];
563       wantedBy = [ "multi-user.target" ];
564       serviceConfig = {
565         Type = "oneshot";
566         RemainAfterExit = true;
567         # TODO: replace 'root' by another username.
568         ExecStart = "${pkgs.nut}/bin/upsdrvctl -u root start";
569         Slice = "system-ups.slice";
570       };
571       environment = envVars;
572       restartTriggers = [
573         config.environment.etc."nut/ups.conf".source
574       ];
575     };
577     environment.etc = {
578       "nut/nut.conf".source = pkgs.writeText "nut.conf"
579         ''
580           MODE = ${cfg.mode}
581         '';
582       "nut/ups.conf".source = pkgs.writeText "ups.conf"
583         ''
584           maxstartdelay = ${toString cfg.maxStartDelay}
586           ${lib.concatStringsSep "\n\n" (lib.forEach (lib.attrValues cfg.ups) (ups: ups.summary))}
587         '';
588       "nut/upsd.conf".source = pkgs.writeText "upsd.conf"
589         ''
590           ${lib.concatStringsSep "\n" (lib.forEach cfg.upsd.listen (listen: "LISTEN ${listen.address} ${toString listen.port}"))}
591           ${cfg.upsd.extraConfig}
592         '';
593       "nut/upssched.conf".source = cfg.schedulerRules;
594       "nut/upsd.users".source = "/run/nut/upsd.users";
595       "nut/upsmon.conf".source = "/run/nut/upsmon.conf";
596     };
598     power.ups.schedulerRules = lib.mkDefault "${pkgs.nut}/etc/upssched.conf.sample";
600     systemd.tmpfiles.rules = [
601       "d /var/state/ups -"
602       "d /var/lib/nut 700"
603     ];
605     services.udev.packages = [ pkgs.nut ];
608     users.users.nut =
609       { uid = 84;
610         home = "/var/lib/nut";
611         createHome = true;
612         group = "nut";
613         description = "UPnP A/V Media Server user";
614       };
616     users.groups."nut" =
617       { gid = 84; };
620   };