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