1{ config, lib, pkgs, ... }: 2 3# TODO: This is not secure, have a look at the file docs/security.txt inside 4# the project sources. 5with lib; 6 7let 8 cfg = config.power.ups; 9 defaultPort = 3493; 10 11 nutFormat = { 12 13 type = with lib.types; let 14 15 singleAtom = nullOr (oneOf [ 16 bool 17 int 18 float 19 str 20 ]) // { 21 description = "atom (null, bool, int, float or string)"; 22 }; 23 24 in attrsOf (oneOf [ 25 singleAtom 26 (listOf (nonEmptyListOf singleAtom)) 27 ]); 28 29 generate = name: value: 30 let 31 normalizedValue = 32 lib.mapAttrs (key: val: 33 if lib.isList val 34 then forEach val (elem: if lib.isList elem then elem else [elem]) 35 else 36 if val == null 37 then [] 38 else [[val]] 39 ) value; 40 41 mkValueString = concatMapStringsSep " " (v: 42 let str = generators.mkValueStringDefault {} v; 43 in 44 # Quote the value if it has spaces and isn't already quoted. 45 if (hasInfix " " str) && !(hasPrefix "\"" str && hasSuffix "\"" str) 46 then "\"${str}\"" 47 else str 48 ); 49 50 in pkgs.writeText name (lib.generators.toKeyValue { 51 mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " "; 52 listsAsDuplicateKeys = true; 53 } normalizedValue); 54 55 }; 56 57 installSecrets = source: target: secrets: 58 pkgs.writeShellScript "installSecrets.sh" '' 59 install -m0600 -D ${source} "${target}" 60 ${concatLines (forEach secrets (name: '' 61 ${pkgs.replace-secret}/bin/replace-secret \ 62 '@${name}@' \ 63 "$CREDENTIALS_DIRECTORY/${name}" \ 64 "${target}" 65 ''))} 66 chmod u-w "${target}" 67 ''; 68 69 upsmonConf = nutFormat.generate "upsmon.conf" cfg.upsmon.settings; 70 71 upsdUsers = pkgs.writeText "upsd.users" (let 72 # This looks like INI, but it's not quite because the 73 # 'upsmon' option lacks a '='. See: man upsd.users 74 userConfig = name: user: concatStringsSep "\n " (concatLists [ 75 [ 76 "[${name}]" 77 "password = \"@upsdusers_password_${name}@\"" 78 ] 79 (optional (user.upsmon != null) "upsmon ${user.upsmon}") 80 (forEach user.actions (action: "actions = ${action}")) 81 (forEach user.instcmds (instcmd: "instcmds = ${instcmd}")) 82 ]); 83 in concatStringsSep "\n\n" (mapAttrsToList userConfig cfg.users)); 84 85 86 upsOptions = {name, config, ...}: 87 { 88 options = { 89 # This can be inferred from the UPS model by looking at 90 # /nix/store/nut/share/driver.list 91 driver = mkOption { 92 type = types.str; 93 description = '' 94 Specify the program to run to talk to this UPS. apcsmart, 95 bestups, and sec are some examples. 96 ''; 97 }; 98 99 port = mkOption { 100 type = types.str; 101 description = '' 102 The serial port to which your UPS is connected. /dev/ttyS0 is 103 usually the first port on Linux boxes, for example. 104 ''; 105 }; 106 107 shutdownOrder = mkOption { 108 default = 0; 109 type = types.int; 110 description = '' 111 When you have multiple UPSes on your system, you usually need to 112 turn them off in a certain order. upsdrvctl shuts down all the 113 0s, then the 1s, 2s, and so on. To exclude a UPS from the 114 shutdown sequence, set this to -1. 115 ''; 116 }; 117 118 maxStartDelay = mkOption { 119 default = null; 120 type = types.uniq (types.nullOr types.int); 121 description = '' 122 This can be set as a global variable above your first UPS 123 definition and it can also be set in a UPS section. This value 124 controls how long upsdrvctl will wait for the driver to finish 125 starting. This keeps your system from getting stuck due to a 126 broken driver or UPS. 127 ''; 128 }; 129 130 description = mkOption { 131 default = ""; 132 type = types.str; 133 description = '' 134 Description of the UPS. 135 ''; 136 }; 137 138 directives = mkOption { 139 default = []; 140 type = types.listOf types.str; 141 description = '' 142 List of configuration directives for this UPS. 143 ''; 144 }; 145 146 summary = mkOption { 147 default = ""; 148 type = types.lines; 149 description = '' 150 Lines which would be added inside ups.conf for handling this UPS. 151 ''; 152 }; 153 154 }; 155 156 config = { 157 directives = mkOrder 10 ([ 158 "driver = ${config.driver}" 159 "port = ${config.port}" 160 ''desc = "${config.description}"'' 161 "sdorder = ${toString config.shutdownOrder}" 162 ] ++ (optional (config.maxStartDelay != null) 163 "maxstartdelay = ${toString config.maxStartDelay}") 164 ); 165 166 summary = 167 concatStringsSep "\n " 168 (["[${name}]"] ++ config.directives); 169 }; 170 }; 171 172 listenOptions = { 173 options = { 174 address = mkOption { 175 type = types.str; 176 description = '' 177 Address of the interface for `upsd` to listen on. 178 See `man upsd.conf` for details. 179 ''; 180 }; 181 182 port = mkOption { 183 type = types.port; 184 default = defaultPort; 185 description = '' 186 TCP port for `upsd` to listen on. 187 See `man upsd.conf` for details. 188 ''; 189 }; 190 }; 191 }; 192 193 upsdOptions = { 194 options = { 195 enable = mkOption { 196 type = types.bool; 197 defaultText = literalMD "`true` if `mode` is one of `standalone`, `netserver`"; 198 description = "Whether to enable `upsd`."; 199 }; 200 201 listen = mkOption { 202 type = with types; listOf (submodule listenOptions); 203 default = []; 204 example = [ 205 { 206 address = "192.168.50.1"; 207 } 208 { 209 address = "::1"; 210 port = 5923; 211 } 212 ]; 213 description = '' 214 Address of the interface for `upsd` to listen on. 215 See `man upsd` for details`. 216 ''; 217 }; 218 219 extraConfig = mkOption { 220 type = types.lines; 221 default = ""; 222 description = '' 223 Additional lines to add to `upsd.conf`. 224 ''; 225 }; 226 }; 227 228 config = { 229 enable = mkDefault (elem cfg.mode [ "standalone" "netserver" ]); 230 }; 231 }; 232 233 234 monitorOptions = { name, config, ... }: { 235 options = { 236 system = mkOption { 237 type = types.str; 238 default = name; 239 description = '' 240 Identifier of the UPS to monitor, in this form: `<upsname>[@<hostname>[:<port>]]` 241 See `upsmon.conf` for details. 242 ''; 243 }; 244 245 powerValue = mkOption { 246 type = types.int; 247 default = 1; 248 description = '' 249 Number of power supplies that the UPS feeds on this system. 250 See `upsmon.conf` for details. 251 ''; 252 }; 253 254 user = mkOption { 255 type = types.str; 256 description = '' 257 Username from `upsd.users` for accessing this UPS. 258 See `upsmon.conf` for details. 259 ''; 260 }; 261 262 passwordFile = mkOption { 263 type = types.str; 264 defaultText = literalMD "power.ups.users.\${user}.passwordFile"; 265 description = '' 266 The full path to a file containing the password from 267 `upsd.users` for accessing this UPS. The password file 268 is read on service start. 269 See `upsmon.conf` for details. 270 ''; 271 }; 272 273 type = mkOption { 274 type = types.str; 275 default = "master"; 276 description = '' 277 The relationship with `upsd`. 278 See `upsmon.conf` for details. 279 ''; 280 }; 281 }; 282 283 config = { 284 passwordFile = mkDefault cfg.users.${config.user}.passwordFile; 285 }; 286 }; 287 288 upsmonOptions = { 289 options = { 290 enable = mkOption { 291 type = types.bool; 292 defaultText = literalMD "`true` if `mode` is one of `standalone`, `netserver`, `netclient`"; 293 description = "Whether to enable `upsmon`."; 294 }; 295 296 monitor = mkOption { 297 type = with types; attrsOf (submodule monitorOptions); 298 default = {}; 299 description = '' 300 Set of UPS to monitor. See `man upsmon.conf` for details. 301 ''; 302 }; 303 304 settings = mkOption { 305 type = nutFormat.type; 306 default = {}; 307 defaultText = literalMD '' 308 { 309 MINSUPPLIES = 1; 310 RUN_AS_USER = "root"; 311 NOTIFYCMD = "''${pkgs.nut}/bin/upssched"; 312 SHUTDOWNCMD = "''${pkgs.systemd}/bin/shutdown now"; 313 } 314 ''; 315 description = "Additional settings to add to `upsmon.conf`."; 316 example = literalMD '' 317 { 318 MINSUPPLIES = 2; 319 NOTIFYFLAG = [ 320 [ "ONLINE" "SYSLOG+EXEC" ] 321 [ "ONBATT" "SYSLOG+EXEC" ] 322 ]; 323 } 324 ''; 325 }; 326 }; 327 328 config = { 329 enable = mkDefault (elem cfg.mode [ "standalone" "netserver" "netclient" ]); 330 settings = { 331 RUN_AS_USER = "root"; # TODO: replace 'root' by another username. 332 MINSUPPLIES = mkDefault 1; 333 NOTIFYCMD = mkDefault "${pkgs.nut}/bin/upssched"; 334 SHUTDOWNCMD = mkDefault "${pkgs.systemd}/bin/shutdown now"; 335 MONITOR = flip mapAttrsToList cfg.upsmon.monitor (name: monitor: with monitor; [ system powerValue user "\"@upsmon_password_${name}@\"" type ]); 336 }; 337 }; 338 }; 339 340 userOptions = { 341 options = { 342 passwordFile = mkOption { 343 type = types.str; 344 description = '' 345 The full path to a file that contains the user's (clear text) 346 password. The password file is read on service start. 347 ''; 348 }; 349 350 actions = mkOption { 351 type = with types; listOf str; 352 default = []; 353 description = '' 354 Allow the user to do certain things with upsd. 355 See `man upsd.users` for details. 356 ''; 357 }; 358 359 instcmds = mkOption { 360 type = with types; listOf str; 361 default = []; 362 description = '' 363 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". 364 See `man upsd.users` for details. 365 ''; 366 }; 367 368 upsmon = mkOption { 369 type = with types; nullOr str; 370 default = null; 371 description = '' 372 Add the necessary actions for a upsmon process to work. 373 See `man upsd.users` for details. 374 ''; 375 }; 376 }; 377 }; 378 379in 380 381 382{ 383 options = { 384 # powerManagement.powerDownCommands 385 386 power.ups = { 387 enable = mkEnableOption '' 388 Enables support for Power Devices, such as Uninterruptible Power 389 Supplies, Power Distribution Units and Solar Controllers. 390 ''; 391 392 mode = mkOption { 393 default = "standalone"; 394 type = types.enum [ "none" "standalone" "netserver" "netclient" ]; 395 description = '' 396 The MODE determines which part of the NUT is to be started, and 397 which configuration files must be modified. 398 399 The values of MODE can be: 400 401 - none: NUT is not configured, or use the Integrated Power 402 Management, or use some external system to startup NUT 403 components. So nothing is to be started. 404 405 - standalone: This mode address a local only configuration, with 1 406 UPS protecting the local system. This implies to start the 3 NUT 407 layers (driver, upsd and upsmon) and the matching configuration 408 files. This mode can also address UPS redundancy. 409 410 - netserver: same as for the standalone configuration, but also 411 need some more ACLs and possibly a specific LISTEN directive in 412 upsd.conf. Since this MODE is opened to the network, a special 413 care should be applied to security concerns. 414 415 - netclient: this mode only requires upsmon. 416 ''; 417 }; 418 419 schedulerRules = mkOption { 420 example = "/etc/nixos/upssched.conf"; 421 type = types.str; 422 description = '' 423 File which contains the rules to handle UPS events. 424 ''; 425 }; 426 427 openFirewall = mkOption { 428 type = types.bool; 429 default = false; 430 description = '' 431 Open ports in the firewall for `upsd`. 432 ''; 433 }; 434 435 maxStartDelay = mkOption { 436 default = 45; 437 type = types.int; 438 description = '' 439 This can be set as a global variable above your first UPS 440 definition and it can also be set in a UPS section. This value 441 controls how long upsdrvctl will wait for the driver to finish 442 starting. This keeps your system from getting stuck due to a 443 broken driver or UPS. 444 ''; 445 }; 446 447 upsmon = mkOption { 448 default = {}; 449 description = '' 450 Options for the `upsmon.conf` configuration file. 451 ''; 452 type = types.submodule upsmonOptions; 453 }; 454 455 upsd = mkOption { 456 default = {}; 457 description = '' 458 Options for the `upsd.conf` configuration file. 459 ''; 460 type = types.submodule upsdOptions; 461 }; 462 463 ups = mkOption { 464 default = {}; 465 # see nut/etc/ups.conf.sample 466 description = '' 467 This is where you configure all the UPSes that this system will be 468 monitoring directly. These are usually attached to serial ports, 469 but USB devices are also supported. 470 ''; 471 type = with types; attrsOf (submodule upsOptions); 472 }; 473 474 users = mkOption { 475 default = {}; 476 description = '' 477 Users that can access upsd. See `man upsd.users`. 478 ''; 479 type = with types; attrsOf (submodule userOptions); 480 }; 481 482 }; 483 }; 484 485 config = mkIf cfg.enable { 486 487 assertions = [ 488 (let 489 totalPowerValue = foldl' add 0 (map (monitor: monitor.powerValue) (attrValues cfg.upsmon.monitor)); 490 minSupplies = cfg.upsmon.settings.MINSUPPLIES; 491 in mkIf cfg.upsmon.enable { 492 assertion = totalPowerValue >= minSupplies; 493 message = '' 494 `power.ups.upsmon`: Total configured power value (${toString totalPowerValue}) must be at least MINSUPPLIES (${toString minSupplies}). 495 ''; 496 }) 497 ]; 498 499 environment.systemPackages = [ pkgs.nut ]; 500 501 networking.firewall = mkIf cfg.openFirewall { 502 allowedTCPPorts = 503 if cfg.upsd.listen == [] 504 then [ defaultPort ] 505 else unique (forEach cfg.upsd.listen (listen: listen.port)); 506 }; 507 508 systemd.services.upsmon = let 509 secrets = mapAttrsToList (name: monitor: "upsmon_password_${name}") cfg.upsmon.monitor; 510 createUpsmonConf = installSecrets upsmonConf "/run/nut/upsmon.conf" secrets; 511 in { 512 enable = cfg.upsmon.enable; 513 description = "Uninterruptible Power Supplies (Monitor)"; 514 after = [ "network.target" ]; 515 wantedBy = [ "multi-user.target" ]; 516 serviceConfig = { 517 Type = "forking"; 518 ExecStartPre = "${createUpsmonConf}"; 519 ExecStart = "${pkgs.nut}/sbin/upsmon"; 520 ExecReload = "${pkgs.nut}/sbin/upsmon -c reload"; 521 LoadCredential = mapAttrsToList (name: monitor: "upsmon_password_${name}:${monitor.passwordFile}") cfg.upsmon.monitor; 522 }; 523 environment.NUT_CONFPATH = "/etc/nut"; 524 environment.NUT_STATEPATH = "/var/lib/nut"; 525 }; 526 527 systemd.services.upsd = let 528 secrets = mapAttrsToList (name: user: "upsdusers_password_${name}") cfg.users; 529 createUpsdUsers = installSecrets upsdUsers "/run/nut/upsd.users" secrets; 530 in { 531 enable = cfg.upsd.enable; 532 description = "Uninterruptible Power Supplies (Daemon)"; 533 after = [ "network.target" "upsmon.service" ]; 534 wantedBy = [ "multi-user.target" ]; 535 serviceConfig = { 536 Type = "forking"; 537 ExecStartPre = "${createUpsdUsers}"; 538 # TODO: replace 'root' by another username. 539 ExecStart = "${pkgs.nut}/sbin/upsd -u root"; 540 ExecReload = "${pkgs.nut}/sbin/upsd -c reload"; 541 LoadCredential = mapAttrsToList (name: user: "upsdusers_password_${name}:${user.passwordFile}") cfg.users; 542 }; 543 environment.NUT_CONFPATH = "/etc/nut"; 544 environment.NUT_STATEPATH = "/var/lib/nut"; 545 restartTriggers = [ 546 config.environment.etc."nut/upsd.conf".source 547 ]; 548 }; 549 550 systemd.services.upsdrv = { 551 enable = cfg.upsd.enable; 552 description = "Uninterruptible Power Supplies (Register all UPS)"; 553 after = [ "upsd.service" ]; 554 wantedBy = [ "multi-user.target" ]; 555 serviceConfig = { 556 Type = "oneshot"; 557 RemainAfterExit = true; 558 # TODO: replace 'root' by another username. 559 ExecStart = "${pkgs.nut}/bin/upsdrvctl -u root start"; 560 }; 561 environment.NUT_CONFPATH = "/etc/nut"; 562 environment.NUT_STATEPATH = "/var/lib/nut"; 563 }; 564 565 environment.etc = { 566 "nut/nut.conf".source = pkgs.writeText "nut.conf" 567 '' 568 MODE = ${cfg.mode} 569 ''; 570 "nut/ups.conf".source = pkgs.writeText "ups.conf" 571 '' 572 maxstartdelay = ${toString cfg.maxStartDelay} 573 574 ${concatStringsSep "\n\n" (forEach (attrValues cfg.ups) (ups: ups.summary))} 575 ''; 576 "nut/upsd.conf".source = pkgs.writeText "upsd.conf" 577 '' 578 ${concatStringsSep "\n" (forEach cfg.upsd.listen (listen: "LISTEN ${listen.address} ${toString listen.port}"))} 579 ${cfg.upsd.extraConfig} 580 ''; 581 "nut/upssched.conf".source = cfg.schedulerRules; 582 "nut/upsd.users".source = "/run/nut/upsd.users"; 583 "nut/upsmon.conf".source = "/run/nut/upsmon.conf"; 584 }; 585 586 power.ups.schedulerRules = mkDefault "${pkgs.nut}/etc/upssched.conf.sample"; 587 588 systemd.tmpfiles.rules = [ 589 "d /var/state/ups -" 590 "d /var/lib/nut 700" 591 ]; 592 593 services.udev.packages = [ pkgs.nut ]; 594 595/* 596 users.users.nut = 597 { uid = 84; 598 home = "/var/lib/nut"; 599 createHome = true; 600 group = "nut"; 601 description = "UPnP A/V Media Server user"; 602 }; 603 604 users.groups."nut" = 605 { gid = 84; }; 606*/ 607 608 }; 609}