at 25.11-pre 7.1 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.services.apcupsd; 9 10 configFile = pkgs.writeText "apcupsd.conf" '' 11 ## apcupsd.conf v1.1 ## 12 # apcupsd complains if the first line is not like above. 13 ${cfg.configText} 14 SCRIPTDIR ${toString scriptDir} 15 ''; 16 17 # List of events from "man apccontrol" 18 eventList = [ 19 "annoyme" 20 "battattach" 21 "battdetach" 22 "changeme" 23 "commfailure" 24 "commok" 25 "doreboot" 26 "doshutdown" 27 "emergency" 28 "failing" 29 "killpower" 30 "loadlimit" 31 "mainsback" 32 "onbattery" 33 "offbattery" 34 "powerout" 35 "remotedown" 36 "runlimit" 37 "timeout" 38 "startselftest" 39 "endselftest" 40 ]; 41 42 shellCmdsForEventScript = eventname: commands: '' 43 echo "#!${pkgs.runtimeShell}" > "$out/${eventname}" 44 echo '${commands}' >> "$out/${eventname}" 45 chmod a+x "$out/${eventname}" 46 ''; 47 48 eventToShellCmds = 49 event: 50 if builtins.hasAttr event cfg.hooks then 51 (shellCmdsForEventScript event (builtins.getAttr event cfg.hooks)) 52 else 53 ""; 54 55 scriptDir = pkgs.runCommand "apcupsd-scriptdir" { preferLocalBuild = true; } ( 56 '' 57 mkdir "$out" 58 # Copy SCRIPTDIR from apcupsd package 59 cp -r ${pkgs.apcupsd}/etc/apcupsd/* "$out"/ 60 # Make the files writeable (nix will unset the write bits afterwards) 61 chmod u+w "$out"/* 62 # Remove the sample event notification scripts, because they don't work 63 # anyways (they try to send mail to "root" with the "mail" command) 64 (cd "$out" && rm changeme commok commfailure onbattery offbattery) 65 # Remove the sample apcupsd.conf file (we're generating our own) 66 rm "$out/apcupsd.conf" 67 # Set the SCRIPTDIR= line in apccontrol to the dir we're creating now 68 sed -i -e "s|^SCRIPTDIR=.*|SCRIPTDIR=$out|" "$out/apccontrol" 69 '' 70 + lib.concatStringsSep "\n" (map eventToShellCmds eventList) 71 72 ); 73 74 # Ensure the CLI uses our generated configFile 75 wrappedBinaries = 76 pkgs.runCommand "apcupsd-wrapped-binaries" 77 { 78 preferLocalBuild = true; 79 nativeBuildInputs = [ pkgs.makeWrapper ]; 80 } 81 '' 82 for p in "${lib.getBin pkgs.apcupsd}/bin/"*; do 83 bname=$(basename "$p") 84 makeWrapper "$p" "$out/bin/$bname" --add-flags "-f ${configFile}" 85 done 86 ''; 87 88 apcupsdWrapped = pkgs.symlinkJoin { 89 name = "apcupsd-wrapped"; 90 # Put wrappers first so they "win" 91 paths = [ 92 wrappedBinaries 93 pkgs.apcupsd 94 ]; 95 }; 96in 97 98{ 99 100 ###### interface 101 102 options = { 103 104 services.apcupsd = { 105 106 enable = lib.mkOption { 107 default = false; 108 type = lib.types.bool; 109 description = '' 110 Whether to enable the APC UPS daemon. apcupsd monitors your UPS and 111 permits orderly shutdown of your computer in the event of a power 112 failure. User manual: http://www.apcupsd.com/manual/manual.html. 113 Note that apcupsd runs as root (to allow shutdown of computer). 114 You can check the status of your UPS with the "apcaccess" command. 115 ''; 116 }; 117 118 configText = lib.mkOption { 119 default = '' 120 UPSTYPE usb 121 NISIP 127.0.0.1 122 BATTERYLEVEL 50 123 MINUTES 5 124 ''; 125 type = lib.types.lines; 126 description = '' 127 Contents of the runtime configuration file, apcupsd.conf. The default 128 settings makes apcupsd autodetect USB UPSes, limit network access to 129 localhost and shutdown the system when the battery level is below 50 130 percent, or when the UPS has calculated that it has 5 minutes or less 131 of remaining power-on time. See man apcupsd.conf for details. 132 ''; 133 }; 134 135 hooks = lib.mkOption { 136 default = { }; 137 example = { 138 doshutdown = "# shell commands to notify that the computer is shutting down"; 139 }; 140 type = lib.types.attrsOf lib.types.lines; 141 description = '' 142 Each attribute in this option names an apcupsd event and the string 143 value it contains will be executed in a shell, in response to that 144 event (prior to the default action). See "man apccontrol" for the 145 list of events and what they represent. 146 147 A hook script can stop apccontrol from doing its default action by 148 exiting with value 99. Do not do this unless you know what you're 149 doing. 150 ''; 151 }; 152 153 }; 154 155 }; 156 157 ###### implementation 158 159 config = lib.mkIf cfg.enable { 160 161 assertions = [ 162 { 163 assertion = 164 let 165 hooknames = builtins.attrNames cfg.hooks; 166 in 167 lib.all (x: lib.elem x eventList) hooknames; 168 message = '' 169 One (or more) attribute names in services.apcupsd.hooks are invalid. 170 Current attribute names: ${toString (builtins.attrNames cfg.hooks)} 171 Valid attribute names : ${toString eventList} 172 ''; 173 } 174 ]; 175 176 # Give users access to the "apcaccess" tool 177 environment.systemPackages = [ apcupsdWrapped ]; 178 179 # NOTE 1: apcupsd runs as root because it needs permission to run 180 # "shutdown" 181 # 182 # NOTE 2: When apcupsd calls "wall", it prints an error because stdout is 183 # not connected to a tty (it is connected to the journal): 184 # wall: cannot get tty name: Inappropriate ioctl for device 185 # The message still gets through. 186 systemd.services.apcupsd = { 187 description = "APC UPS Daemon"; 188 wantedBy = [ "multi-user.target" ]; 189 preStart = "mkdir -p /run/apcupsd/"; 190 serviceConfig = { 191 ExecStart = "${pkgs.apcupsd}/bin/apcupsd -b -f ${configFile} -d1"; 192 # TODO: When apcupsd has initiated a shutdown, systemd always ends up 193 # waiting for it to stop ("A stop job is running for UPS daemon"). This 194 # is weird, because in the journal one can clearly see that apcupsd has 195 # received the SIGTERM signal and has already quit (or so it seems). 196 # This reduces the wait time from 90 seconds (default) to just 5. Then 197 # systemd kills it with SIGKILL. 198 TimeoutStopSec = 5; 199 }; 200 unitConfig.Documentation = "man:apcupsd(8)"; 201 }; 202 203 # A special service to tell the UPS to power down/hibernate just before the 204 # computer shuts down. (The UPS has a built in delay before it actually 205 # shuts off power.) Copied from here: 206 # http://forums.opensuse.org/english/get-technical-help-here/applications/479499-apcupsd-systemd-killpower-issues.html 207 systemd.services.apcupsd-killpower = { 208 description = "APC UPS Kill Power"; 209 after = [ "shutdown.target" ]; # append umount.target? 210 before = [ "final.target" ]; 211 wantedBy = [ "shutdown.target" ]; 212 unitConfig = { 213 ConditionPathExists = "/run/apcupsd/powerfail"; 214 DefaultDependencies = "no"; 215 }; 216 serviceConfig = { 217 Type = "oneshot"; 218 ExecStart = "${pkgs.apcupsd}/bin/apcupsd --killpower -f ${configFile}"; 219 TimeoutSec = "infinity"; 220 StandardOutput = "tty"; 221 RemainAfterExit = "yes"; 222 }; 223 }; 224 225 }; 226 227}