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