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 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";
319 description = "Additional settings to add to `upsmon.conf`.";
320 example = lib.literalMD ''
324 [ "ONLINE" "SYSLOG+EXEC" ]
325 [ "ONBATT" "SYSLOG+EXEC" ]
333 enable = lib.mkDefault (lib.elem cfg.mode [ "standalone" "netserver" "netclient" ]);
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";
347 passwordFile = lib.mkOption {
348 type = lib.types.str;
350 The full path to a file that contains the user's (clear text)
351 password. The password file is read on service start.
355 actions = lib.mkOption {
356 type = with lib.types; listOf str;
359 Allow the user to do certain things with upsd.
360 See `man upsd.users` for details.
364 instcmds = lib.mkOption {
365 type = with lib.types; listOf str;
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.
373 upsmon = lib.mkOption {
374 type = with lib.types; nullOr (enum [ "primary" "secondary" ]);
377 Add the necessary actions for a upsmon process to work.
378 See `man upsd.users` for details.
389 # powerManagement.powerDownCommands
392 enable = lib.mkEnableOption ''
393 support for Power Devices, such as Uninterruptible Power
394 Supplies, Power Distribution Units and Solar Controllers
397 mode = lib.mkOption {
398 default = "standalone";
399 type = lib.types.enum [ "none" "standalone" "netserver" "netclient" ];
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.
424 schedulerRules = lib.mkOption {
425 example = "/etc/nixos/upssched.conf";
426 type = lib.types.str;
428 File which contains the rules to handle UPS events.
432 openFirewall = lib.mkOption {
433 type = lib.types.bool;
436 Open ports in the firewall for `upsd`.
440 maxStartDelay = lib.mkOption {
442 type = lib.types.int;
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.
452 upsmon = lib.mkOption {
455 Options for the `upsmon.conf` configuration file.
457 type = lib.types.submodule upsmonOptions;
460 upsd = lib.mkOption {
463 Options for the `upsd.conf` configuration file.
465 type = lib.types.submodule upsdOptions;
470 # see nut/etc/ups.conf.sample
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.
476 type = with lib.types; attrsOf (submodule upsOptions);
479 users = lib.mkOption {
482 Users that can access upsd. See `man upsd.users`.
484 type = with lib.types; attrsOf (submodule userOptions);
490 config = lib.mkIf cfg.enable {
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;
499 `power.ups.upsmon`: Total configured power value (${toString totalPowerValue}) must be at least MINSUPPLIES (${toString minSupplies}).
504 # For interactive use.
505 environment.systemPackages = [ pkgs.nut ];
506 environment.variables = envVars;
508 networking.firewall = lib.mkIf cfg.openFirewall {
510 if cfg.upsd.listen == []
512 else lib.unique (lib.forEach cfg.upsd.listen (listen: listen.port));
515 systemd.slices.system-ups = {
516 description = "Network UPS Tools (NUT) Slice";
517 documentation = [ "https://networkupstools.org/" ];
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;
524 enable = cfg.upsmon.enable;
525 description = "Uninterruptible Power Supplies (Monitor)";
526 after = [ "network.target" ];
527 wantedBy = [ "multi-user.target" ];
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";
536 environment = envVars;
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;
543 enable = cfg.upsd.enable;
544 description = "Uninterruptible Power Supplies (Daemon)";
545 after = [ "network.target" "upsmon.service" ];
546 wantedBy = [ "multi-user.target" ];
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";
556 environment = envVars;
558 config.environment.etc."nut/upsd.conf".source
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" ];
569 RemainAfterExit = true;
570 # TODO: replace 'root' by another username.
571 ExecStart = "${pkgs.nut}/bin/upsdrvctl -u root start";
572 Slice = "system-ups.slice";
574 environment = envVars;
576 config.environment.etc."nut/ups.conf".source
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" ];
587 ConditionPathExists = cfg.upsmon.settings.POWERDOWNFLAG;
588 DefaultDependencies = "no";
590 environment = envVars;
593 ExecStart = "${pkgs.nut}/bin/upsdrvctl shutdown";
594 Slice = "system-ups.slice";
599 "nut/nut.conf".source = pkgs.writeText "nut.conf"
603 "nut/ups.conf".source = pkgs.writeText "ups.conf"
605 maxstartdelay = ${toString cfg.maxStartDelay}
607 ${lib.concatStringsSep "\n\n" (lib.forEach (lib.attrValues cfg.ups) (ups: ups.summary))}
609 "nut/upsd.conf".source = pkgs.writeText "upsd.conf"
611 ${lib.concatStringsSep "\n" (lib.forEach cfg.upsd.listen (listen: "LISTEN ${listen.address} ${toString listen.port}"))}
612 ${cfg.upsd.extraConfig}
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";
619 power.ups.schedulerRules = lib.mkDefault "${pkgs.nut}/etc/upssched.conf.sample";
621 systemd.tmpfiles.rules = [
626 services.udev.packages = [ pkgs.nut ];
631 home = "/var/lib/nut";
634 description = "UPnP A/V Media Server user";