1{ config, lib, pkgs, ... }:
2
3with lib;
4let
5
6 cfg = config.services.smokeping;
7 smokepingHome = "/var/lib/smokeping";
8 smokepingPidDir = "/run";
9 configFile =
10 if cfg.config == null
11 then
12 ''
13 *** General ***
14 cgiurl = ${cfg.cgiUrl}
15 contact = ${cfg.ownerEmail}
16 datadir = ${smokepingHome}/data
17 imgcache = ${smokepingHome}/cache
18 imgurl = ${cfg.imgUrl}
19 linkstyle = ${cfg.linkStyle}
20 ${lib.optionalString (cfg.mailHost != "") "mailhost = ${cfg.mailHost}"}
21 owner = ${cfg.owner}
22 pagedir = ${smokepingHome}/cache
23 piddir = ${smokepingPidDir}
24 ${lib.optionalString (cfg.sendmail != null) "sendmail = ${cfg.sendmail}"}
25 smokemail = ${cfg.smokeMailTemplate}
26 *** Presentation ***
27 template = ${cfg.presentationTemplate}
28 ${cfg.presentationConfig}
29 *** Alerts ***
30 ${cfg.alertConfig}
31 *** Database ***
32 ${cfg.databaseConfig}
33 *** Probes ***
34 ${cfg.probeConfig}
35 *** Targets ***
36 ${cfg.targetConfig}
37 ${cfg.extraConfig}
38 ''
39 else
40 cfg.config;
41
42 configPath = pkgs.writeText "smokeping.conf" configFile;
43 cgiHome = pkgs.writeScript "smokeping.fcgi" ''
44 #!${pkgs.bash}/bin/bash
45 ${cfg.package}/bin/smokeping_cgi /etc/smokeping.conf
46 '';
47in
48
49{
50 options = {
51 services.smokeping = {
52 enable = mkEnableOption (lib.mdDoc "smokeping service");
53
54 alertConfig = mkOption {
55 type = types.lines;
56 default = ''
57 to = root@localhost
58 from = smokeping@localhost
59 '';
60 example = ''
61 to = alertee@address.somewhere
62 from = smokealert@company.xy
63
64 +someloss
65 type = loss
66 # in percent
67 pattern = >0%,*12*,>0%,*12*,>0%
68 comment = loss 3 times in a row;
69 '';
70 description = lib.mdDoc "Configuration for alerts.";
71 };
72 cgiUrl = mkOption {
73 type = types.str;
74 default = "http://${cfg.hostName}:${toString cfg.port}/smokeping.cgi";
75 defaultText = literalExpression ''"http://''${hostName}:''${toString port}/smokeping.cgi"'';
76 example = "https://somewhere.example.com/smokeping.cgi";
77 description = lib.mdDoc "URL to the smokeping cgi.";
78 };
79 config = mkOption {
80 type = types.nullOr types.lines;
81 default = null;
82 description = lib.mdDoc ''
83 Full smokeping config supplied by the user. Overrides
84 and replaces any other configuration supplied.
85 '';
86 };
87 databaseConfig = mkOption {
88 type = types.lines;
89 default = ''
90 step = 300
91 pings = 20
92 # consfn mrhb steps total
93 AVERAGE 0.5 1 1008
94 AVERAGE 0.5 12 4320
95 MIN 0.5 12 4320
96 MAX 0.5 12 4320
97 AVERAGE 0.5 144 720
98 MAX 0.5 144 720
99 MIN 0.5 144 720
100
101 '';
102 example = ''
103 # near constant pings.
104 step = 30
105 pings = 20
106 # consfn mrhb steps total
107 AVERAGE 0.5 1 10080
108 AVERAGE 0.5 12 43200
109 MIN 0.5 12 43200
110 MAX 0.5 12 43200
111 AVERAGE 0.5 144 7200
112 MAX 0.5 144 7200
113 MIN 0.5 144 7200
114 '';
115 description = lib.mdDoc ''Configure the ping frequency and retention of the rrd files.
116 Once set, changing the interval will require deletion or migration of all
117 the collected data.'';
118 };
119 extraConfig = mkOption {
120 type = types.lines;
121 default = "";
122 description = lib.mdDoc "Any additional customization not already included.";
123 };
124 hostName = mkOption {
125 type = types.str;
126 default = config.networking.fqdn;
127 defaultText = literalExpression "config.networking.fqdn";
128 example = "somewhere.example.com";
129 description = lib.mdDoc "DNS name for the urls generated in the cgi.";
130 };
131 imgUrl = mkOption {
132 type = types.str;
133 default = "cache";
134 defaultText = literalExpression ''"cache"'';
135 example = "https://somewhere.example.com/cache";
136 description = lib.mdDoc ''
137 Base url for images generated in the cgi.
138
139 The default is a relative URL to ensure it works also when e.g. forwarding
140 the GUI port via SSH.
141 '';
142 };
143 linkStyle = mkOption {
144 type = types.enum ["original" "absolute" "relative"];
145 default = "relative";
146 example = "absolute";
147 description = lib.mdDoc "DNS name for the urls generated in the cgi.";
148 };
149 mailHost = mkOption {
150 type = types.str;
151 default = "";
152 example = "localhost";
153 description = lib.mdDoc "Use this SMTP server to send alerts";
154 };
155 owner = mkOption {
156 type = types.str;
157 default = "nobody";
158 example = "Bob Foobawr";
159 description = lib.mdDoc "Real name of the owner of the instance";
160 };
161 ownerEmail = mkOption {
162 type = types.str;
163 default = "no-reply@${cfg.hostName}";
164 defaultText = literalExpression ''"no-reply@''${hostName}"'';
165 example = "no-reply@yourdomain.com";
166 description = lib.mdDoc "Email contact for owner";
167 };
168 package = mkOption {
169 type = types.package;
170 default = pkgs.smokeping;
171 defaultText = literalExpression "pkgs.smokeping";
172 description = lib.mdDoc "Specify a custom smokeping package";
173 };
174 host = mkOption {
175 type = types.nullOr types.str;
176 default = "localhost";
177 example = "192.0.2.1"; # rfc5737 example IP for documentation
178 description = lib.mdDoc ''
179 Host/IP to bind to for the web server.
180
181 Setting it to `null` skips passing the -h option to thttpd,
182 which makes it bind to all interfaces.
183 '';
184 };
185 port = mkOption {
186 type = types.port;
187 default = 8081;
188 description = lib.mdDoc "TCP port to use for the web server.";
189 };
190 presentationConfig = mkOption {
191 type = types.lines;
192 default = ''
193 + charts
194 menu = Charts
195 title = The most interesting destinations
196 ++ stddev
197 sorter = StdDev(entries=>4)
198 title = Top Standard Deviation
199 menu = Std Deviation
200 format = Standard Deviation %f
201 ++ max
202 sorter = Max(entries=>5)
203 title = Top Max Roundtrip Time
204 menu = by Max
205 format = Max Roundtrip Time %f seconds
206 ++ loss
207 sorter = Loss(entries=>5)
208 title = Top Packet Loss
209 menu = Loss
210 format = Packets Lost %f
211 ++ median
212 sorter = Median(entries=>5)
213 title = Top Median Roundtrip Time
214 menu = by Median
215 format = Median RTT %f seconds
216 + overview
217 width = 600
218 height = 50
219 range = 10h
220 + detail
221 width = 600
222 height = 200
223 unison_tolerance = 2
224 "Last 3 Hours" 3h
225 "Last 30 Hours" 30h
226 "Last 10 Days" 10d
227 "Last 360 Days" 360d
228 '';
229 description = lib.mdDoc "presentation graph style";
230 };
231 presentationTemplate = mkOption {
232 type = types.str;
233 default = "${pkgs.smokeping}/etc/basepage.html.dist";
234 defaultText = literalExpression ''"''${pkgs.smokeping}/etc/basepage.html.dist"'';
235 description = lib.mdDoc "Default page layout for the web UI.";
236 };
237 probeConfig = mkOption {
238 type = types.lines;
239 default = ''
240 + FPing
241 binary = ${config.security.wrapperDir}/fping
242 '';
243 defaultText = literalExpression ''
244 '''
245 + FPing
246 binary = ''${config.security.wrapperDir}/fping
247 '''
248 '';
249 description = lib.mdDoc "Probe configuration";
250 };
251 sendmail = mkOption {
252 type = types.nullOr types.path;
253 default = null;
254 example = "/run/wrappers/bin/sendmail";
255 description = lib.mdDoc "Use this sendmail compatible script to deliver alerts";
256 };
257 smokeMailTemplate = mkOption {
258 type = types.str;
259 default = "${cfg.package}/etc/smokemail.dist";
260 defaultText = literalExpression ''"''${package}/etc/smokemail.dist"'';
261 description = lib.mdDoc "Specify the smokemail template for alerts.";
262 };
263 targetConfig = mkOption {
264 type = types.lines;
265 default = ''
266 probe = FPing
267 menu = Top
268 title = Network Latency Grapher
269 remark = Welcome to the SmokePing website of xxx Company. \
270 Here you will learn all about the latency of our network.
271 + Local
272 menu = Local
273 title = Local Network
274 ++ LocalMachine
275 menu = Local Machine
276 title = This host
277 host = localhost
278 '';
279 description = lib.mdDoc "Target configuration";
280 };
281 user = mkOption {
282 type = types.str;
283 default = "smokeping";
284 description = lib.mdDoc "User that runs smokeping and (optionally) thttpd. A group of the same name will be created as well.";
285 };
286 webService = mkOption {
287 type = types.bool;
288 default = true;
289 description = lib.mdDoc "Enable a smokeping web interface";
290 };
291 };
292
293 };
294
295 config = mkIf cfg.enable {
296 assertions = [
297 {
298 assertion = !(cfg.sendmail != null && cfg.mailHost != "");
299 message = "services.smokeping: sendmail and Mailhost cannot both be enabled.";
300 }
301 ];
302 security.wrappers = {
303 fping =
304 { setuid = true;
305 owner = "root";
306 group = "root";
307 source = "${pkgs.fping}/bin/fping";
308 };
309 };
310 environment.etc."smokeping.conf".source = configPath;
311 environment.systemPackages = [ pkgs.fping ];
312 users.users.${cfg.user} = {
313 isNormalUser = false;
314 isSystemUser = true;
315 group = cfg.user;
316 description = "smokeping daemon user";
317 home = smokepingHome;
318 createHome = true;
319 # When `cfg.webService` is enabled, `thttpd` makes SmokePing available
320 # under `${cfg.host}:${cfg.port}/smokeping.fcgi` as per the `ln -s` below.
321 # We also want that going to `${cfg.host}:${cfg.port}` without `smokeping.fcgi`
322 # makes it easy for the user to find SmokePing.
323 # However `thttpd` does not seem to support easy redirections from `/` to `smokeping.fcgi`
324 # and only allows directory listings or `/` -> `index.html` resolution if the directory
325 # has `chmod 755` (see https://acme.com/software/thttpd/thttpd_man.html#PERMISSIONS,
326 # " directories should be 755 if you want to allow indexing").
327 # Otherwise it shows `403 Forbidden` on `/`.
328 # Thus, we need to make `smokepingHome` (which is given to `thttpd -d` below) `755`.
329 homeMode = "755";
330 };
331 users.groups.${cfg.user} = { };
332 systemd.services.smokeping = {
333 reloadTriggers = [ configPath ];
334 requiredBy = [ "multi-user.target" ];
335 serviceConfig = {
336 User = cfg.user;
337 Restart = "on-failure";
338 ExecStart = "${cfg.package}/bin/smokeping --config=/etc/smokeping.conf --nodaemon";
339 };
340 preStart = ''
341 mkdir -m 0755 -p ${smokepingHome}/cache ${smokepingHome}/data
342 ln -sf ${cfg.package}/htdocs/css ${smokepingHome}/css
343 ln -sf ${cfg.package}/htdocs/js ${smokepingHome}/js
344 ln -sf ${cgiHome} ${smokepingHome}/smokeping.fcgi
345 ${cfg.package}/bin/smokeping --check --config=${configPath}
346 ${cfg.package}/bin/smokeping --static --config=${configPath}
347 '';
348 };
349 systemd.services.thttpd = mkIf cfg.webService {
350 requiredBy = [ "multi-user.target"];
351 requires = [ "smokeping.service"];
352 path = with pkgs; [ bash rrdtool smokeping thttpd ];
353 serviceConfig = {
354 Restart = "always";
355 ExecStart = lib.concatStringsSep " " (lib.concatLists [
356 [ "${pkgs.thttpd}/bin/thttpd" ]
357 [ "-u ${cfg.user}" ]
358 [ ''-c "**.fcgi"'' ]
359 [ "-d ${smokepingHome}" ]
360 (lib.optional (cfg.host != null) "-h ${cfg.host}")
361 [ "-p ${builtins.toString cfg.port}" ]
362 [ "-D -nos" ]
363 ]);
364 };
365 };
366 };
367
368 meta.maintainers = with lib.maintainers; [
369 erictapen
370 nh2
371 ];
372}
373