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}