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 = "''${pkgs.nut}/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 "${pkgs.nut}/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 mode = lib.mkOption { 463 default = "standalone"; 464 type = lib.types.enum [ 465 "none" 466 "standalone" 467 "netserver" 468 "netclient" 469 ]; 470 description = '' 471 The MODE determines which part of the NUT is to be started, and 472 which configuration files must be modified. 473 474 The values of MODE can be: 475 476 - none: NUT is not configured, or use the Integrated Power 477 Management, or use some external system to startup NUT 478 components. So nothing is to be started. 479 480 - standalone: This mode address a local only configuration, with 1 481 UPS protecting the local system. This implies to start the 3 NUT 482 layers (driver, upsd and upsmon) and the matching configuration 483 files. This mode can also address UPS redundancy. 484 485 - netserver: same as for the standalone configuration, but also 486 need some more ACLs and possibly a specific LISTEN directive in 487 upsd.conf. Since this MODE is opened to the network, a special 488 care should be applied to security concerns. 489 490 - netclient: this mode only requires upsmon. 491 ''; 492 }; 493 494 schedulerRules = lib.mkOption { 495 example = "/etc/nixos/upssched.conf"; 496 type = lib.types.str; 497 description = '' 498 File which contains the rules to handle UPS events. 499 ''; 500 }; 501 502 openFirewall = lib.mkOption { 503 type = lib.types.bool; 504 default = false; 505 description = '' 506 Open ports in the firewall for `upsd`. 507 ''; 508 }; 509 510 maxStartDelay = lib.mkOption { 511 default = 45; 512 type = lib.types.int; 513 description = '' 514 This can be set as a global variable above your first UPS 515 definition and it can also be set in a UPS section. This value 516 controls how long upsdrvctl will wait for the driver to finish 517 starting. This keeps your system from getting stuck due to a 518 broken driver or UPS. 519 ''; 520 }; 521 522 upsmon = lib.mkOption { 523 default = { }; 524 description = '' 525 Options for the `upsmon.conf` configuration file. 526 ''; 527 type = lib.types.submodule upsmonOptions; 528 }; 529 530 upsd = lib.mkOption { 531 default = { }; 532 description = '' 533 Options for the `upsd.conf` configuration file. 534 ''; 535 type = lib.types.submodule upsdOptions; 536 }; 537 538 ups = lib.mkOption { 539 default = { }; 540 # see nut/etc/ups.conf.sample 541 description = '' 542 This is where you configure all the UPSes that this system will be 543 monitoring directly. These are usually attached to serial ports, 544 but USB devices are also supported. 545 ''; 546 type = with lib.types; attrsOf (submodule upsOptions); 547 }; 548 549 users = lib.mkOption { 550 default = { }; 551 description = '' 552 Users that can access upsd. See `man upsd.users`. 553 ''; 554 type = with lib.types; attrsOf (submodule userOptions); 555 }; 556 557 }; 558 }; 559 560 config = lib.mkIf cfg.enable { 561 562 assertions = [ 563 ( 564 let 565 totalPowerValue = lib.foldl' lib.add 0 ( 566 map (monitor: monitor.powerValue) (lib.attrValues cfg.upsmon.monitor) 567 ); 568 minSupplies = cfg.upsmon.settings.MINSUPPLIES; 569 in 570 lib.mkIf cfg.upsmon.enable { 571 assertion = totalPowerValue >= minSupplies; 572 message = '' 573 `power.ups.upsmon`: Total configured power value (${toString totalPowerValue}) must be at least MINSUPPLIES (${toString minSupplies}). 574 ''; 575 } 576 ) 577 ]; 578 579 # For interactive use. 580 environment.systemPackages = [ pkgs.nut ]; 581 environment.variables = envVars; 582 583 networking.firewall = lib.mkIf cfg.openFirewall { 584 allowedTCPPorts = 585 if cfg.upsd.listen == [ ] then 586 [ defaultPort ] 587 else 588 lib.unique (lib.forEach cfg.upsd.listen (listen: listen.port)); 589 }; 590 591 systemd.slices.system-ups = { 592 description = "Network UPS Tools (NUT) Slice"; 593 documentation = [ "https://networkupstools.org/" ]; 594 }; 595 596 systemd.services.upsmon = 597 let 598 secrets = lib.mapAttrsToList (name: monitor: "upsmon_password_${name}") cfg.upsmon.monitor; 599 createUpsmonConf = installSecrets upsmonConf "/run/nut/upsmon.conf" cfg.upsmon.user secrets; 600 in 601 { 602 enable = cfg.upsmon.enable; 603 description = "Uninterruptible Power Supplies (Monitor)"; 604 after = [ "network.target" ]; 605 wantedBy = [ "multi-user.target" ]; 606 serviceConfig = { 607 Type = "forking"; 608 ExecStartPre = "${createUpsmonConf}"; 609 ExecStart = "${pkgs.nut}/sbin/upsmon -u ${cfg.upsmon.user}"; 610 ExecReload = "${pkgs.nut}/sbin/upsmon -c reload"; 611 LoadCredential = lib.mapAttrsToList ( 612 name: monitor: "upsmon_password_${name}:${monitor.passwordFile}" 613 ) cfg.upsmon.monitor; 614 Slice = "system-ups.slice"; 615 }; 616 environment = envVars; 617 }; 618 619 systemd.services.upsd = 620 let 621 secrets = lib.mapAttrsToList (name: user: "upsdusers_password_${name}") cfg.users; 622 createUpsdUsers = installSecrets upsdUsers "/run/nut/upsd.users" "root" secrets; 623 in 624 { 625 enable = cfg.upsd.enable; 626 description = "Uninterruptible Power Supplies (Daemon)"; 627 after = [ 628 "network.target" 629 "upsmon.service" 630 ]; 631 wantedBy = [ "multi-user.target" ]; 632 serviceConfig = { 633 Type = "forking"; 634 ExecStartPre = "${createUpsdUsers}"; 635 # TODO: replace 'root' by another username. 636 ExecStart = "${pkgs.nut}/sbin/upsd -u root"; 637 ExecReload = "${pkgs.nut}/sbin/upsd -c reload"; 638 LoadCredential = lib.mapAttrsToList ( 639 name: user: "upsdusers_password_${name}:${user.passwordFile}" 640 ) cfg.users; 641 Slice = "system-ups.slice"; 642 }; 643 environment = envVars; 644 restartTriggers = [ 645 config.environment.etc."nut/upsd.conf".source 646 ]; 647 }; 648 649 systemd.services.upsdrv = { 650 enable = cfg.upsd.enable; 651 description = "Uninterruptible Power Supplies (Register all UPS)"; 652 after = [ "upsd.service" ]; 653 wantedBy = [ "multi-user.target" ]; 654 serviceConfig = { 655 Type = "oneshot"; 656 RemainAfterExit = true; 657 # TODO: replace 'root' by another username. 658 ExecStart = "${pkgs.nut}/bin/upsdrvctl -u root start"; 659 Slice = "system-ups.slice"; 660 }; 661 environment = envVars; 662 restartTriggers = [ 663 config.environment.etc."nut/ups.conf".source 664 ]; 665 }; 666 667 systemd.services.ups-killpower = lib.mkIf (cfg.upsmon.settings.POWERDOWNFLAG != null) { 668 enable = cfg.upsd.enable; 669 description = "UPS Kill Power"; 670 wantedBy = [ "shutdown.target" ]; 671 after = [ "shutdown.target" ]; 672 before = [ "final.target" ]; 673 unitConfig = { 674 ConditionPathExists = cfg.upsmon.settings.POWERDOWNFLAG; 675 DefaultDependencies = "no"; 676 }; 677 environment = envVars; 678 serviceConfig = { 679 Type = "oneshot"; 680 ExecStart = "${pkgs.nut}/bin/upsdrvctl shutdown"; 681 Slice = "system-ups.slice"; 682 }; 683 }; 684 685 environment.etc = { 686 "nut/nut.conf".source = pkgs.writeText "nut.conf" '' 687 MODE = ${cfg.mode} 688 ''; 689 "nut/ups.conf".source = pkgs.writeText "ups.conf" '' 690 maxstartdelay = ${toString cfg.maxStartDelay} 691 692 ${lib.concatStringsSep "\n\n" (lib.forEach (lib.attrValues cfg.ups) (ups: ups.summary))} 693 ''; 694 "nut/upsd.conf".source = pkgs.writeText "upsd.conf" '' 695 ${lib.concatStringsSep "\n" ( 696 lib.forEach cfg.upsd.listen (listen: "LISTEN ${listen.address} ${toString listen.port}") 697 )} 698 ${cfg.upsd.extraConfig} 699 ''; 700 "nut/upssched.conf".source = cfg.schedulerRules; 701 "nut/upsd.users".source = "/run/nut/upsd.users"; 702 "nut/upsmon.conf".source = "/run/nut/upsmon.conf"; 703 }; 704 705 power.ups.schedulerRules = lib.mkDefault "${pkgs.nut}/etc/upssched.conf.sample"; 706 707 systemd.tmpfiles.rules = [ 708 "d /var/state/ups -" 709 "d /var/lib/nut 700" 710 ]; 711 712 services.udev.packages = [ pkgs.nut ]; 713 714 users.users.nutmon = lib.mkIf (cfg.upsmon.user == "nutmon") { 715 isSystemUser = true; 716 group = cfg.upsmon.group; 717 }; 718 users.groups.nutmon = lib.mkIf (cfg.upsmon.user == "nutmon" && cfg.upsmon.group == "nutmon") { }; 719 720 }; 721}