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