1 { config, lib, pkgs, ... }:
2 # TODO: This is not secure, have a look at the file docs/security.txt inside
5 cfg = config.power.ups;
9 NUT_CONFPATH = "/etc/nut";
10 NUT_STATEPATH = "/var/lib/nut";
15 type = with lib.types; let
17 singleAtom = nullOr (oneOf [
23 description = "atom (null, bool, int, float or string)";
28 (listOf (nonEmptyListOf singleAtom))
31 generate = name: value:
34 lib.mapAttrs (key: val:
36 then lib.forEach val (elem: if lib.isList elem then elem else [elem])
43 mkValueString = lib.concatMapStringsSep " " (v:
44 let str = lib.generators.mkValueStringDefault {} v;
46 # Quote the value if it has spaces and isn't already quoted.
47 if (lib.hasInfix " " str) && !(lib.hasPrefix "\"" str && lib.hasSuffix "\"" str)
52 in pkgs.writeText name (lib.generators.toKeyValue {
53 mkKeyValue = lib.generators.mkKeyValueDefault { inherit mkValueString; } " ";
54 listsAsDuplicateKeys = true;
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 \
65 "$CREDENTIALS_DIRECTORY/${name}" \
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 [
79 "password = \"@upsdusers_password_${name}@\""
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}"))
85 in lib.concatStringsSep "\n\n" (lib.mapAttrsToList userConfig cfg.users));
88 upsOptions = {name, config, ...}:
91 # This can be inferred from the UPS model by looking at
92 # /nix/store/nut/share/driver.list
93 driver = lib.mkOption {
96 Specify the program to run to talk to this UPS. apcsmart,
97 bestups, and sec are some examples.
101 port = lib.mkOption {
102 type = lib.types.str;
104 The serial port to which your UPS is connected. /dev/ttyS0 is
105 usually the first port on Linux boxes, for example.
109 shutdownOrder = lib.mkOption {
111 type = lib.types.int;
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.
120 maxStartDelay = lib.mkOption {
122 type = lib.types.uniq (lib.types.nullOr lib.types.int);
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.
132 description = lib.mkOption {
134 type = lib.types.str;
136 Description of the UPS.
140 directives = lib.mkOption {
142 type = lib.types.listOf lib.types.str;
144 List of configuration directives for this UPS.
148 summary = lib.mkOption {
150 type = lib.types.lines;
152 Lines which would be added inside ups.conf for handling this UPS.
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}")
169 lib.concatStringsSep "\n "
170 (["[${name}]"] ++ config.directives);
176 address = lib.mkOption {
177 type = lib.types.str;
179 Address of the interface for `upsd` to listen on.
180 See `man upsd.conf` for details.
184 port = lib.mkOption {
185 type = lib.types.port;
186 default = defaultPort;
188 TCP port for `upsd` to listen on.
189 See `man upsd.conf` for details.
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`.";
203 listen = lib.mkOption {
204 type = with lib.types; listOf (submodule listenOptions);
208 address = "192.168.50.1";
216 Address of the interface for `upsd` to listen on.
217 See `man upsd` for details`.
221 extraConfig = lib.mkOption {
222 type = lib.types.lines;
225 Additional lines to add to `upsd.conf`.
231 enable = lib.mkDefault (lib.elem cfg.mode [ "standalone" "netserver" ]);
236 monitorOptions = { name, config, ... }: {
238 system = lib.mkOption {
239 type = lib.types.str;
242 Identifier of the UPS to monitor, in this form: `<upsname>[@<hostname>[:<port>]]`
243 See `upsmon.conf` for details.
247 powerValue = lib.mkOption {
248 type = lib.types.int;
251 Number of power supplies that the UPS feeds on this system.
252 See `upsmon.conf` for details.
256 user = lib.mkOption {
257 type = lib.types.str;
259 Username from `upsd.users` for accessing this UPS.
260 See `upsmon.conf` for details.
264 passwordFile = lib.mkOption {
265 type = lib.types.str;
266 defaultText = lib.literalMD "power.ups.users.\${user}.passwordFile";
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.
275 type = lib.mkOption {
276 type = lib.types.str;
279 The relationship with `upsd`.
280 See `upsmon.conf` for details.
286 passwordFile = lib.mkDefault cfg.users.${config.user}.passwordFile;
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`.";
298 monitor = lib.mkOption {
299 type = with lib.types; attrsOf (submodule monitorOptions);
302 Set of UPS to monitor. See `man upsmon.conf` for details.
306 settings = lib.mkOption {
307 type = nutFormat.type;
309 defaultText = lib.literalMD ''
312 RUN_AS_USER = "root";
313 NOTIFYCMD = "''${pkgs.nut}/bin/upssched";
314 SHUTDOWNCMD = "''${pkgs.systemd}/bin/shutdown now";
317 description = "Additional settings to add to `upsmon.conf`.";
318 example = lib.literalMD ''
322 [ "ONLINE" "SYSLOG+EXEC" ]
323 [ "ONBATT" "SYSLOG+EXEC" ]
331 enable = lib.mkDefault (lib.elem cfg.mode [ "standalone" "netserver" "netclient" ]);
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 ]);
344 passwordFile = lib.mkOption {
345 type = lib.types.str;
347 The full path to a file that contains the user's (clear text)
348 password. The password file is read on service start.
352 actions = lib.mkOption {
353 type = with lib.types; listOf str;
356 Allow the user to do certain things with upsd.
357 See `man upsd.users` for details.
361 instcmds = lib.mkOption {
362 type = with lib.types; listOf str;
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.
370 upsmon = lib.mkOption {
371 type = with lib.types; nullOr (enum [ "primary" "secondary" ]);
374 Add the necessary actions for a upsmon process to work.
375 See `man upsd.users` for details.
386 # powerManagement.powerDownCommands
389 enable = lib.mkEnableOption ''
390 support for Power Devices, such as Uninterruptible Power
391 Supplies, Power Distribution Units and Solar Controllers
394 mode = lib.mkOption {
395 default = "standalone";
396 type = lib.types.enum [ "none" "standalone" "netserver" "netclient" ];
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.
421 schedulerRules = lib.mkOption {
422 example = "/etc/nixos/upssched.conf";
423 type = lib.types.str;
425 File which contains the rules to handle UPS events.
429 openFirewall = lib.mkOption {
430 type = lib.types.bool;
433 Open ports in the firewall for `upsd`.
437 maxStartDelay = lib.mkOption {
439 type = lib.types.int;
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.
449 upsmon = lib.mkOption {
452 Options for the `upsmon.conf` configuration file.
454 type = lib.types.submodule upsmonOptions;
457 upsd = lib.mkOption {
460 Options for the `upsd.conf` configuration file.
462 type = lib.types.submodule upsdOptions;
467 # see nut/etc/ups.conf.sample
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.
473 type = with lib.types; attrsOf (submodule upsOptions);
476 users = lib.mkOption {
479 Users that can access upsd. See `man upsd.users`.
481 type = with lib.types; attrsOf (submodule userOptions);
487 config = lib.mkIf cfg.enable {
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;
496 `power.ups.upsmon`: Total configured power value (${toString totalPowerValue}) must be at least MINSUPPLIES (${toString minSupplies}).
501 # For interactive use.
502 environment.systemPackages = [ pkgs.nut ];
503 environment.variables = envVars;
505 networking.firewall = lib.mkIf cfg.openFirewall {
507 if cfg.upsd.listen == []
509 else lib.unique (lib.forEach cfg.upsd.listen (listen: listen.port));
512 systemd.slices.system-ups = {
513 description = "Network UPS Tools (NUT) Slice";
514 documentation = [ "https://networkupstools.org/" ];
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;
521 enable = cfg.upsmon.enable;
522 description = "Uninterruptible Power Supplies (Monitor)";
523 after = [ "network.target" ];
524 wantedBy = [ "multi-user.target" ];
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";
533 environment = envVars;
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;
540 enable = cfg.upsd.enable;
541 description = "Uninterruptible Power Supplies (Daemon)";
542 after = [ "network.target" "upsmon.service" ];
543 wantedBy = [ "multi-user.target" ];
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";
553 environment = envVars;
555 config.environment.etc."nut/upsd.conf".source
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" ];
566 RemainAfterExit = true;
567 # TODO: replace 'root' by another username.
568 ExecStart = "${pkgs.nut}/bin/upsdrvctl -u root start";
569 Slice = "system-ups.slice";
571 environment = envVars;
573 config.environment.etc."nut/ups.conf".source
578 "nut/nut.conf".source = pkgs.writeText "nut.conf"
582 "nut/ups.conf".source = pkgs.writeText "ups.conf"
584 maxstartdelay = ${toString cfg.maxStartDelay}
586 ${lib.concatStringsSep "\n\n" (lib.forEach (lib.attrValues cfg.ups) (ups: ups.summary))}
588 "nut/upsd.conf".source = pkgs.writeText "upsd.conf"
590 ${lib.concatStringsSep "\n" (lib.forEach cfg.upsd.listen (listen: "LISTEN ${listen.address} ${toString listen.port}"))}
591 ${cfg.upsd.extraConfig}
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";
598 power.ups.schedulerRules = lib.mkDefault "${pkgs.nut}/etc/upssched.conf.sample";
600 systemd.tmpfiles.rules = [
605 services.udev.packages = [ pkgs.nut ];
610 home = "/var/lib/nut";
613 description = "UPnP A/V Media Server user";