1{ config, lib, pkgs, ... }:
2
3# TODO: support munin-async
4# TODO: LWP/Pg perl libs aren't recognized
5
6# TODO: support fastcgi
7# https://guide.munin-monitoring.org/en/latest/example/webserver/apache-cgi.html
8# spawn-fcgi -s /run/munin/fastcgi-graph.sock -U www-data -u munin -g munin /usr/lib/munin/cgi/munin-cgi-graph
9# spawn-fcgi -s /run/munin/fastcgi-html.sock -U www-data -u munin -g munin /usr/lib/munin/cgi/munin-cgi-html
10# https://paste.sh/vofcctHP#-KbDSXVeWoifYncZmLfZzgum
11# nginx https://munin.readthedocs.org/en/latest/example/webserver/nginx.html
12
13
14with lib;
15
16let
17 nodeCfg = config.services.munin-node;
18 cronCfg = config.services.munin-cron;
19
20 muninConf = pkgs.writeText "munin.conf"
21 ''
22 dbdir /var/lib/munin
23 htmldir /var/www/munin
24 logdir /var/log/munin
25 rundir /run/munin
26
27 ${lib.optionalString (cronCfg.extraCSS != "") "staticdir ${customStaticDir}"}
28
29 ${cronCfg.extraGlobalConfig}
30
31 ${cronCfg.hosts}
32 '';
33
34 nodeConf = pkgs.writeText "munin-node.conf"
35 ''
36 log_level 3
37 log_file Sys::Syslog
38 port 4949
39 host *
40 background 0
41 user root
42 group root
43 host_name ${config.networking.hostName}
44 setsid 0
45
46 # wrapped plugins by makeWrapper being with dots
47 ignore_file ^\.
48
49 allow ^::1$
50 allow ^127\.0\.0\.1$
51
52 ${nodeCfg.extraConfig}
53 '';
54
55 pluginConf = pkgs.writeText "munin-plugin-conf"
56 ''
57 [hddtemp_smartctl]
58 user root
59 group root
60
61 [meminfo]
62 user root
63 group root
64
65 [ipmi*]
66 user root
67 group root
68
69 [munin*]
70 env.UPDATE_STATSFILE /var/lib/munin/munin-update.stats
71
72 ${nodeCfg.extraPluginConfig}
73 '';
74
75 pluginConfDir = pkgs.stdenv.mkDerivation {
76 name = "munin-plugin-conf.d";
77 buildCommand = ''
78 mkdir $out
79 ln -s ${pluginConf} $out/nixos-config
80 '';
81 };
82
83 # Copy one Munin plugin into the Nix store with a specific name.
84 # This is suitable for use with plugins going directly into /etc/munin/plugins,
85 # i.e. munin.extraPlugins.
86 internOnePlugin = { name, path }:
87 "cp -a '${path}' '${name}'";
88
89 # Copy an entire tree of Munin plugins into a single directory in the Nix
90 # store, with no renaming. The output is suitable for use with
91 # munin-node-configure --suggest, i.e. munin.extraAutoPlugins.
92 # Note that this flattens the input; this is intentional, as
93 # munin-node-configure won't recurse into subdirectories.
94 internManyPlugins = path:
95 "find '${path}' -type f -perm /a+x -exec cp -a -t . '{}' '+'";
96
97 # Use the appropriate intern-fn to copy the plugins into the store and patch
98 # them afterwards in an attempt to get them to run on NixOS.
99 # This is a bit hairy because we can't just fix shebangs; lots of munin plugins
100 # hardcode paths like /sbin/mount rather than trusting $PATH, so we have to
101 # look for and update those throughout the script. At the same time, if the
102 # plugin comes from a package that is already nixified, we don't want to
103 # rewrite paths like /nix/store/foo/sbin/mount.
104 # For now we make the simplifying assumption that no file will contain lines
105 # which mix store paths and FHS paths, and thus run our substitution only on
106 # lines which do not contain store paths.
107 internAndFixPlugins = name: intern-fn: paths:
108 pkgs.runCommand name {} ''
109 mkdir -p "$out"
110 cd "$out"
111 ${lib.concatStringsSep "\n" (map intern-fn paths)}
112 chmod -R u+w .
113 ${pkgs.findutils}/bin/find . -type f -exec ${pkgs.gnused}/bin/sed -E -i "
114 \%''${NIX_STORE}/%! s,(/usr)?/s?bin/,/run/current-system/sw/bin/,g
115 " '{}' '+'
116 '';
117
118 # TODO: write a derivation for munin-contrib, so that for contrib plugins
119 # you can just refer to them by name rather than needing to include a copy
120 # of munin-contrib in your nixos configuration.
121 extraPluginDir = internAndFixPlugins "munin-extra-plugins.d"
122 internOnePlugin
123 (lib.attrsets.mapAttrsToList (k: v: { name = k; path = v; }) nodeCfg.extraPlugins);
124
125 extraAutoPluginDir = internAndFixPlugins "munin-extra-auto-plugins.d"
126 internManyPlugins nodeCfg.extraAutoPlugins;
127
128 customStaticDir = pkgs.runCommand "munin-custom-static-data" {} ''
129 cp -a "${pkgs.munin}/etc/opt/munin/static" "$out"
130 cd "$out"
131 chmod -R u+w .
132 echo "${cronCfg.extraCSS}" >> style.css
133 echo "${cronCfg.extraCSS}" >> style-new.css
134 '';
135in
136
137{
138
139 options = {
140
141 services.munin-node = {
142
143 enable = mkOption {
144 default = false;
145 type = types.bool;
146 description = ''
147 Enable Munin Node agent. Munin node listens on 0.0.0.0 and
148 by default accepts connections only from 127.0.0.1 for security reasons.
149
150 See <https://guide.munin-monitoring.org/en/latest/architecture/index.html>.
151 '';
152 };
153
154 extraConfig = mkOption {
155 default = "";
156 type = types.lines;
157 description = ''
158 {file}`munin-node.conf` extra configuration. See
159 <https://guide.munin-monitoring.org/en/latest/reference/munin-node.conf.html>
160 '';
161 };
162
163 extraPluginConfig = mkOption {
164 default = "";
165 type = types.lines;
166 description = ''
167 {file}`plugin-conf.d` extra plugin configuration. See
168 <https://guide.munin-monitoring.org/en/latest/plugin/use.html>
169 '';
170 example = ''
171 [fail2ban_*]
172 user root
173 '';
174 };
175
176 extraPlugins = mkOption {
177 default = {};
178 type = with types; attrsOf path;
179 description = ''
180 Additional Munin plugins to activate. Keys are the name of the plugin
181 symlink, values are the path to the underlying plugin script. You
182 can use the same plugin script multiple times (e.g. for wildcard
183 plugins).
184
185 Note that these plugins do not participate in autoconfiguration. If
186 you want to autoconfigure additional plugins, use
187 {option}`services.munin-node.extraAutoPlugins`.
188
189 Plugins enabled in this manner take precedence over autoconfigured
190 plugins.
191
192 Plugins will be copied into the Nix store, and it will attempt to
193 modify them to run properly by fixing hardcoded references to
194 `/bin`, `/usr/bin`,
195 `/sbin`, and `/usr/sbin`.
196 '';
197 example = literalExpression ''
198 {
199 zfs_usage_bigpool = /src/munin-contrib/plugins/zfs/zfs_usage_;
200 zfs_usage_smallpool = /src/munin-contrib/plugins/zfs/zfs_usage_;
201 zfs_list = /src/munin-contrib/plugins/zfs/zfs_list;
202 };
203 '';
204 };
205
206 extraAutoPlugins = mkOption {
207 default = [];
208 type = with types; listOf path;
209 description = ''
210 Additional Munin plugins to autoconfigure, using
211 `munin-node-configure --suggest`. These should be
212 the actual paths to the plugin files (or directories containing them),
213 not just their names.
214
215 If you want to manually enable individual plugins instead, use
216 {option}`services.munin-node.extraPlugins`.
217
218 Note that only plugins that have the 'autoconfig' capability will do
219 anything if listed here, since plugins that cannot autoconfigure
220 won't be automatically enabled by
221 `munin-node-configure`.
222
223 Plugins will be copied into the Nix store, and it will attempt to
224 modify them to run properly by fixing hardcoded references to
225 `/bin`, `/usr/bin`,
226 `/sbin`, and `/usr/sbin`.
227 '';
228 example = literalExpression ''
229 [
230 /src/munin-contrib/plugins/zfs
231 /src/munin-contrib/plugins/ssh
232 ];
233 '';
234 };
235
236 disabledPlugins = mkOption {
237 # TODO: figure out why Munin isn't writing the log file and fix it.
238 # In the meantime this at least suppresses a useless graph full of
239 # NaNs in the output.
240 default = [ "munin_stats" ];
241 type = with types; listOf str;
242 description = ''
243 Munin plugins to disable, even if
244 `munin-node-configure --suggest` tries to enable
245 them. To disable a wildcard plugin, use an actual wildcard, as in
246 the example.
247
248 munin_stats is disabled by default as it tries to read
249 `/var/log/munin/munin-update.log` for timing
250 information, and the NixOS build of Munin does not write this file.
251 '';
252 example = [ "diskstats" "zfs_usage_*" ];
253 };
254 };
255
256 services.munin-cron = {
257
258 enable = mkOption {
259 default = false;
260 type = types.bool;
261 description = ''
262 Enable munin-cron. Takes care of all heavy lifting to collect data from
263 nodes and draws graphs to html. Runs munin-update, munin-limits,
264 munin-graphs and munin-html in that order.
265
266 HTML output is in {file}`/var/www/munin/`, configure your
267 favourite webserver to serve static files.
268 '';
269 };
270
271 extraGlobalConfig = mkOption {
272 default = "";
273 type = types.lines;
274 description = ''
275 {file}`munin.conf` extra global configuration.
276 See <https://guide.munin-monitoring.org/en/latest/reference/munin.conf.html>.
277 Useful to setup notifications, see
278 <https://guide.munin-monitoring.org/en/latest/tutorial/alert.html>
279 '';
280 example = ''
281 contact.email.command mail -s "Munin notification for ''${var:host}" someone@example.com
282 '';
283 };
284
285 hosts = mkOption {
286 default = "";
287 type = types.lines;
288 description = ''
289 Definitions of hosts of nodes to collect data from. Needs at least one
290 host for cron to succeed. See
291 <https://guide.munin-monitoring.org/en/latest/reference/munin.conf.html>
292 '';
293 example = literalExpression ''
294 '''
295 [''${config.networking.hostName}]
296 address localhost
297 '''
298 '';
299 };
300
301 extraCSS = mkOption {
302 default = "";
303 type = types.lines;
304 description = ''
305 Custom styling for the HTML that munin-cron generates. This will be
306 appended to the CSS files used by munin-cron and will thus take
307 precedence over the builtin styles.
308 '';
309 example = ''
310 /* A simple dark theme. */
311 html, body { background: #222222; }
312 #header, #footer { background: #333333; }
313 img.i, img.iwarn, img.icrit, img.iunkn {
314 filter: invert(100%) hue-rotate(-30deg);
315 }
316 '';
317 };
318
319 };
320
321 };
322
323 config = mkMerge [ (mkIf (nodeCfg.enable || cronCfg.enable) {
324
325 environment.systemPackages = [ pkgs.munin ];
326
327 users.users.munin = {
328 description = "Munin monitoring user";
329 group = "munin";
330 uid = config.ids.uids.munin;
331 home = "/var/lib/munin";
332 };
333
334 users.groups.munin = {
335 gid = config.ids.gids.munin;
336 };
337
338 }) (mkIf nodeCfg.enable {
339
340 systemd.services.munin-node = {
341 description = "Munin Node";
342 after = [ "network.target" ];
343 wantedBy = [ "multi-user.target" ];
344 path = with pkgs; [ munin smartmontools "/run/current-system/sw" "/run/wrappers" ];
345 environment.MUNIN_LIBDIR = "${pkgs.munin}/lib";
346 environment.MUNIN_PLUGSTATE = "/run/munin";
347 environment.MUNIN_LOGDIR = "/var/log/munin";
348 preStart = ''
349 echo "Updating munin plugins..."
350
351 mkdir -p /etc/munin/plugins
352 rm -rf /etc/munin/plugins/*
353
354 # Autoconfigure builtin plugins
355 ${pkgs.munin}/bin/munin-node-configure --suggest --shell --families contrib,auto,manual --config ${nodeConf} --libdir=${pkgs.munin}/lib/plugins --servicedir=/etc/munin/plugins --sconfdir=${pluginConfDir} 2>/dev/null | ${pkgs.bash}/bin/bash
356
357 # Autoconfigure extra plugins
358 ${pkgs.munin}/bin/munin-node-configure --suggest --shell --families contrib,auto,manual --config ${nodeConf} --libdir=${extraAutoPluginDir} --servicedir=/etc/munin/plugins --sconfdir=${pluginConfDir} 2>/dev/null | ${pkgs.bash}/bin/bash
359
360 ${lib.optionalString (nodeCfg.extraPlugins != {}) ''
361 # Link in manually enabled plugins
362 ln -f -s -t /etc/munin/plugins ${extraPluginDir}/*
363 ''}
364
365 ${lib.optionalString (nodeCfg.disabledPlugins != []) ''
366 # Disable plugins
367 cd /etc/munin/plugins
368 rm -f ${toString nodeCfg.disabledPlugins}
369 ''}
370 '';
371 serviceConfig = {
372 ExecStart = "${pkgs.munin}/sbin/munin-node --config ${nodeConf} --servicedir /etc/munin/plugins/ --sconfdir=${pluginConfDir}";
373 };
374 };
375
376 # munin_stats plugin breaks as of 2.0.33 when this doesn't exist
377 systemd.tmpfiles.settings."10-munin"."/run/munin".d = {
378 mode = "0755";
379 user = "munin";
380 group = "munin";
381 };
382
383 }) (mkIf cronCfg.enable {
384
385 # Munin is hardcoded to use DejaVu Mono and the graphs come out wrong if
386 # it's not available.
387 fonts.packages = [ pkgs.dejavu_fonts ];
388
389 systemd.timers.munin-cron = {
390 description = "batch Munin master programs";
391 wantedBy = [ "timers.target" ];
392 timerConfig.OnCalendar = "*:0/5";
393 };
394
395 systemd.services.munin-cron = {
396 description = "batch Munin master programs";
397 unitConfig.Documentation = "man:munin-cron(8)";
398
399 serviceConfig = {
400 Type = "oneshot";
401 User = "munin";
402 ExecStart = "${pkgs.munin}/bin/munin-cron --config ${muninConf}";
403 };
404 };
405
406 systemd.tmpfiles.settings."20-munin" = let
407 defaultConfig = {
408 mode = "0755";
409 user = "munin";
410 group = "munin";
411 };
412 in {
413 "/run/munin".d = defaultConfig;
414 "/var/log/munin".d = defaultConfig;
415 "/var/www/munin".d = defaultConfig;
416 "/var/lib/munin".d = defaultConfig;
417 };
418 })];
419}