1{
2 config,
3 pkgs,
4 lib,
5 options,
6 utils,
7 ...
8}:
9
10let
11 inherit (lib)
12 concatStrings
13 foldl
14 foldl'
15 genAttrs
16 literalExpression
17 maintainers
18 mapAttrs
19 mapAttrsToList
20 mkDefault
21 mkEnableOption
22 mkIf
23 mkMerge
24 mkOption
25 optional
26 types
27 mkOptionDefault
28 flip
29 attrNames
30 xor
31 ;
32
33 cfg = config.services.prometheus.exporters;
34
35 # each attribute in `exporterOpts` is expected to have specified:
36 # - port (types.int): port on which the exporter listens
37 # - serviceOpts (types.attrs): config that is merged with the
38 # default definition of the exporter's
39 # systemd service
40 # - extraOpts (types.attrs): extra configuration options to
41 # configure the exporter with, which
42 # are appended to the default options
43 #
44 # Note that `extraOpts` is optional, but a script for the exporter's
45 # systemd service must be provided by specifying either
46 # `serviceOpts.script` or `serviceOpts.serviceConfig.ExecStart`
47
48 exporterOpts =
49 (genAttrs
50 [
51 "apcupsd"
52 "artifactory"
53 "bind"
54 "bird"
55 "bitcoin"
56 "blackbox"
57 "borgmatic"
58 "buildkite-agent"
59 "ecoflow"
60 "chrony"
61 "collectd"
62 "deluge"
63 "dmarc"
64 "dnsmasq"
65 "dnssec"
66 "domain"
67 "dovecot"
68 "ebpf"
69 "fastly"
70 "flow"
71 "fritz"
72 "fritzbox"
73 "frr"
74 "graphite"
75 "idrac"
76 "imap-mailstat"
77 "influxdb"
78 "ipmi"
79 "jitsi"
80 "json"
81 "junos-czerwonk"
82 "kafka"
83 "kea"
84 "keylight"
85 "klipper"
86 "knot"
87 "libvirt"
88 "lnd"
89 "mail"
90 "mailman3"
91 "mikrotik"
92 "modemmanager"
93 "mongodb"
94 "mqtt"
95 "mysqld"
96 "nats"
97 "nextcloud"
98 "nginx"
99 "nginxlog"
100 "node"
101 "node-cert"
102 "nut"
103 "nvidia-gpu"
104 "pgbouncer"
105 "php-fpm"
106 "pihole"
107 "ping"
108 "postfix"
109 "postgres"
110 "process"
111 "pve"
112 "py-air-control"
113 "rasdaemon"
114 "redis"
115 "restic"
116 "rspamd"
117 "rtl_433"
118 "sabnzbd"
119 "scaphandre"
120 "script"
121 "shelly"
122 "smartctl"
123 "smokeping"
124 "snmp"
125 "sql"
126 "statsd"
127 "storagebox"
128 "surfboard"
129 "systemd"
130 "tibber"
131 "unbound"
132 "unpoller"
133 "v2ray"
134 "varnish"
135 "wireguard"
136 "zfs"
137 ]
138 (
139 name:
140 import (./. + "/exporters/${name}.nix") {
141 inherit
142 config
143 lib
144 pkgs
145 options
146 utils
147 ;
148 }
149 )
150 )
151 // (mapAttrs
152 (
153 name: params:
154 import (./. + "/exporters/${params.name}.nix") {
155 inherit
156 config
157 lib
158 pkgs
159 options
160 utils
161 ;
162 type = params.type;
163 }
164 )
165 {
166 exportarr-bazarr = {
167 name = "exportarr";
168 type = "bazarr";
169 };
170 exportarr-lidarr = {
171 name = "exportarr";
172 type = "lidarr";
173 };
174 exportarr-prowlarr = {
175 name = "exportarr";
176 type = "prowlarr";
177 };
178 exportarr-radarr = {
179 name = "exportarr";
180 type = "radarr";
181 };
182 exportarr-readarr = {
183 name = "exportarr";
184 type = "readarr";
185 };
186 exportarr-sonarr = {
187 name = "exportarr";
188 type = "sonarr";
189 };
190 }
191 );
192
193 mkExporterOpts = (
194 { name, port }:
195 {
196 enable = mkEnableOption "the prometheus ${name} exporter";
197 port = mkOption {
198 type = types.port;
199 default = port;
200 description = ''
201 Port to listen on.
202 '';
203 };
204 listenAddress = mkOption {
205 type = types.str;
206 default = "0.0.0.0";
207 description = ''
208 Address to listen on.
209 '';
210 };
211 extraFlags = mkOption {
212 type = types.listOf types.str;
213 default = [ ];
214 description = ''
215 Extra commandline options to pass to the ${name} exporter.
216 '';
217 };
218 openFirewall = mkOption {
219 type = types.bool;
220 default = false;
221 description = ''
222 Open port in firewall for incoming connections.
223 '';
224 };
225 firewallFilter = mkOption {
226 type = types.nullOr types.str;
227 default = null;
228 example = literalExpression ''
229 "-i eth0 -p tcp -m tcp --dport ${toString port}"
230 '';
231 description = ''
232 Specify a filter for iptables to use when
233 {option}`services.prometheus.exporters.${name}.openFirewall`
234 is true. It is used as `ip46tables -I nixos-fw firewallFilter -j nixos-fw-accept`.
235 '';
236 };
237 firewallRules = mkOption {
238 type = types.nullOr types.lines;
239 default = null;
240 example = literalExpression ''
241 iifname "eth0" tcp dport ${toString port} counter accept
242 '';
243 description = ''
244 Specify rules for nftables to add to the input chain
245 when {option}`services.prometheus.exporters.${name}.openFirewall` is true.
246 '';
247 };
248 user = mkOption {
249 type = types.str;
250 default = "${name}-exporter";
251 description = ''
252 User name under which the ${name} exporter shall be run.
253 '';
254 };
255 group = mkOption {
256 type = types.str;
257 default = "${name}-exporter";
258 description = ''
259 Group under which the ${name} exporter shall be run.
260 '';
261 };
262 }
263 );
264
265 mkSubModule =
266 {
267 name,
268 port,
269 extraOpts,
270 imports,
271 }:
272 {
273 ${name} = mkOption {
274 type = types.submodule [
275 {
276 inherit imports;
277 options = (
278 mkExporterOpts {
279 inherit name port;
280 }
281 // extraOpts
282 );
283 }
284 (
285 { config, ... }:
286 mkIf config.openFirewall {
287 firewallFilter = mkDefault "-p tcp -m tcp --dport ${toString config.port}";
288 firewallRules = mkDefault ''tcp dport ${toString config.port} accept comment "${name}-exporter"'';
289 }
290 )
291 ];
292 internal = true;
293 default = { };
294 };
295 };
296
297 mkSubModules = (
298 foldl' (a: b: a // b) { } (
299 mapAttrsToList (
300 name: opts:
301 mkSubModule {
302 inherit name;
303 inherit (opts) port;
304 extraOpts = opts.extraOpts or { };
305 imports = opts.imports or [ ];
306 }
307 ) exporterOpts
308 )
309 );
310
311 mkExporterConf =
312 {
313 name,
314 conf,
315 serviceOpts,
316 }:
317 let
318 enableDynamicUser = serviceOpts.serviceConfig.DynamicUser or true;
319 nftables = config.networking.nftables.enable;
320 in
321 mkIf conf.enable {
322 warnings = conf.warnings or [ ];
323 assertions = conf.assertions or [ ];
324 users.users."${name}-exporter" = (
325 mkIf (conf.user == "${name}-exporter" && !enableDynamicUser) {
326 description = "Prometheus ${name} exporter service user";
327 isSystemUser = true;
328 inherit (conf) group;
329 }
330 );
331 users.groups = mkMerge [
332 (mkIf (conf.group == "${name}-exporter" && !enableDynamicUser) {
333 "${name}-exporter" = { };
334 })
335 (mkIf (name == "smartctl") {
336 "smartctl-exporter-access" = { };
337 })
338 ];
339 services.udev.extraRules = mkIf (name == "smartctl") ''
340 ACTION=="add", SUBSYSTEM=="nvme", KERNEL=="nvme[0-9]*", RUN+="${pkgs.acl}/bin/setfacl -m g:smartctl-exporter-access:rw /dev/$kernel"
341 '';
342 networking.firewall.extraCommands = mkIf (conf.openFirewall && !nftables) (concatStrings [
343 "ip46tables -A nixos-fw ${conf.firewallFilter} "
344 "-m comment --comment ${name}-exporter -j nixos-fw-accept"
345 ]);
346 networking.firewall.extraInputRules = mkIf (conf.openFirewall && nftables) conf.firewallRules;
347 systemd.services."prometheus-${name}-exporter" = mkMerge ([
348 {
349 wantedBy = [ "multi-user.target" ];
350 after = [ "network.target" ];
351 serviceConfig.Restart = mkDefault "always";
352 serviceConfig.PrivateTmp = mkDefault true;
353 serviceConfig.WorkingDirectory = mkDefault /tmp;
354 serviceConfig.DynamicUser = mkDefault enableDynamicUser;
355 serviceConfig.User = mkDefault conf.user;
356 serviceConfig.Group = conf.group;
357 # Hardening
358 serviceConfig.CapabilityBoundingSet = mkDefault [ "" ];
359 serviceConfig.DeviceAllow = [ "" ];
360 serviceConfig.LockPersonality = true;
361 serviceConfig.MemoryDenyWriteExecute = true;
362 serviceConfig.NoNewPrivileges = true;
363 serviceConfig.PrivateDevices = mkDefault true;
364 serviceConfig.ProtectClock = mkDefault true;
365 serviceConfig.ProtectControlGroups = true;
366 serviceConfig.ProtectHome = true;
367 serviceConfig.ProtectHostname = true;
368 serviceConfig.ProtectKernelLogs = true;
369 serviceConfig.ProtectKernelModules = true;
370 serviceConfig.ProtectKernelTunables = true;
371 serviceConfig.ProtectSystem = mkDefault "strict";
372 serviceConfig.RemoveIPC = true;
373 serviceConfig.RestrictAddressFamilies = [
374 "AF_INET"
375 "AF_INET6"
376 ];
377 serviceConfig.RestrictNamespaces = true;
378 serviceConfig.RestrictRealtime = true;
379 serviceConfig.RestrictSUIDSGID = true;
380 serviceConfig.SystemCallArchitectures = "native";
381 serviceConfig.UMask = "0077";
382 }
383 serviceOpts
384 ]);
385 };
386in
387{
388
389 options.services.prometheus.exporters = mkOption {
390 type = types.submodule {
391 options = (mkSubModules);
392 imports = [
393 ../../../misc/assertions.nix
394 (lib.mkRenamedOptionModule [ "unifi-poller" ] [ "unpoller" ])
395 (lib.mkRemovedOptionModule [ "minio" ] ''
396 The Minio exporter has been removed, as it was broken and unmaintained.
397 See the 24.11 release notes for more information.
398 '')
399 (lib.mkRemovedOptionModule [ "tor" ] ''
400 The Tor exporter has been removed, as it was broken and unmaintained.
401 '')
402 ];
403 };
404 description = "Prometheus exporter configuration";
405 default = { };
406 example = literalExpression ''
407 {
408 node = {
409 enable = true;
410 enabledCollectors = [ "systemd" ];
411 };
412 varnish.enable = true;
413 }
414 '';
415 };
416
417 config = mkMerge (
418 [
419 {
420 assertions = [
421 {
422 assertion =
423 cfg.ipmi.enable -> (cfg.ipmi.configFile != null) -> (!(lib.hasPrefix "/tmp/" cfg.ipmi.configFile));
424 message = ''
425 Config file specified in `services.prometheus.exporters.ipmi.configFile' must
426 not reside within /tmp - it won't be visible to the systemd service.
427 '';
428 }
429 {
430 assertion =
431 cfg.ipmi.enable
432 -> (cfg.ipmi.webConfigFile != null)
433 -> (!(lib.hasPrefix "/tmp/" cfg.ipmi.webConfigFile));
434 message = ''
435 Config file specified in `services.prometheus.exporters.ipmi.webConfigFile' must
436 not reside within /tmp - it won't be visible to the systemd service.
437 '';
438 }
439 {
440 assertion =
441 cfg.restic.enable -> ((cfg.restic.repository == null) != (cfg.restic.repositoryFile == null));
442 message = ''
443 Please specify either 'services.prometheus.exporters.restic.repository'
444 or 'services.prometheus.exporters.restic.repositoryFile'.
445 '';
446 }
447 {
448 assertion =
449 cfg.snmp.enable -> ((cfg.snmp.configurationPath == null) != (cfg.snmp.configuration == null));
450 message = ''
451 Please ensure you have either `services.prometheus.exporters.snmp.configuration'
452 or `services.prometheus.exporters.snmp.configurationPath' set!
453 '';
454 }
455 {
456 assertion =
457 cfg.mikrotik.enable -> ((cfg.mikrotik.configFile == null) != (cfg.mikrotik.configuration == null));
458 message = ''
459 Please specify either `services.prometheus.exporters.mikrotik.configuration'
460 or `services.prometheus.exporters.mikrotik.configFile'.
461 '';
462 }
463 {
464 assertion = cfg.mail.enable -> ((cfg.mail.configFile == null) != (cfg.mail.configuration == null));
465 message = ''
466 Please specify either 'services.prometheus.exporters.mail.configuration'
467 or 'services.prometheus.exporters.mail.configFile'.
468 '';
469 }
470 {
471 assertion = cfg.mysqld.runAsLocalSuperUser -> config.services.mysql.enable;
472 message = ''
473 The exporter is configured to run as 'services.mysql.user', but
474 'services.mysql.enable' is set to false.
475 '';
476 }
477 {
478 assertion =
479 cfg.nextcloud.enable -> ((cfg.nextcloud.passwordFile == null) != (cfg.nextcloud.tokenFile == null));
480 message = ''
481 Please specify either 'services.prometheus.exporters.nextcloud.passwordFile' or
482 'services.prometheus.exporters.nextcloud.tokenFile'
483 '';
484 }
485 {
486 assertion = cfg.sql.enable -> ((cfg.sql.configFile == null) != (cfg.sql.configuration == null));
487 message = ''
488 Please specify either 'services.prometheus.exporters.sql.configuration' or
489 'services.prometheus.exporters.sql.configFile'
490 '';
491 }
492 {
493 assertion = cfg.scaphandre.enable -> (pkgs.stdenv.targetPlatform.isx86_64 == true);
494 message = ''
495 Scaphandre only support x86_64 architectures.
496 '';
497 }
498 {
499 assertion =
500 cfg.scaphandre.enable
501 -> ((lib.kernel.whenHelpers pkgs.linux.version).whenOlder "5.11" true).condition == false;
502 message = ''
503 Scaphandre requires a kernel version newer than '5.11', '${pkgs.linux.version}' given.
504 '';
505 }
506 {
507 assertion = cfg.scaphandre.enable -> (builtins.elem "intel_rapl_common" config.boot.kernelModules);
508 message = ''
509 Scaphandre needs 'intel_rapl_common' kernel module to be enabled. Please add it in 'boot.kernelModules'.
510 '';
511 }
512 {
513 assertion =
514 cfg.idrac.enable -> ((cfg.idrac.configurationPath == null) != (cfg.idrac.configuration == null));
515 message = ''
516 Please ensure you have either `services.prometheus.exporters.idrac.configuration'
517 or `services.prometheus.exporters.idrac.configurationPath' set!
518 '';
519 }
520 {
521 assertion =
522 cfg.deluge.enable
523 -> ((cfg.deluge.delugePassword == null) != (cfg.deluge.delugePasswordFile == null));
524 message = ''
525 Please ensure you have either `services.prometheus.exporters.deluge.delugePassword'
526 or `services.prometheus.exporters.deluge.delugePasswordFile' set!
527 '';
528 }
529 {
530 assertion =
531 cfg.pgbouncer.enable
532 -> (xor (cfg.pgbouncer.connectionEnvFile == null) (cfg.pgbouncer.connectionString == null));
533 message = ''
534 Options `services.prometheus.exporters.pgbouncer.connectionEnvFile` and
535 `services.prometheus.exporters.pgbouncer.connectionString` are mutually exclusive!
536 '';
537 }
538 ]
539 ++ (flip map (attrNames exporterOpts) (exporter: {
540 assertion = cfg.${exporter}.firewallFilter != null -> cfg.${exporter}.openFirewall;
541 message = ''
542 The `firewallFilter'-option of exporter ${exporter} doesn't have any effect unless
543 `openFirewall' is set to `true'!
544 '';
545 }))
546 ++ config.services.prometheus.exporters.assertions;
547 warnings = [
548 (mkIf
549 (
550 config.services.prometheus.exporters.idrac.enable
551 && config.services.prometheus.exporters.idrac.configurationPath != null
552 )
553 ''
554 Configuration file in `services.prometheus.exporters.idrac.configurationPath` may override
555 `services.prometheus.exporters.idrac.listenAddress` and/or `services.prometheus.exporters.idrac.port`.
556 Consider using `services.prometheus.exporters.idrac.configuration` instead.
557 ''
558 )
559 ]
560 ++ config.services.prometheus.exporters.warnings;
561 }
562 ]
563 ++ [
564 (mkIf config.services.prometheus.exporters.rtl_433.enable {
565 hardware.rtl-sdr.enable = mkDefault true;
566 })
567 ]
568 ++ [
569 (mkIf config.services.postfix.enable {
570 services.prometheus.exporters.postfix.group = mkDefault config.services.postfix.setgidGroup;
571 })
572 ]
573 ++ [
574 (mkIf config.services.prometheus.exporters.deluge.enable {
575 system.activationScripts = {
576 deluge-exported.text = ''
577 mkdir -p /etc/deluge-exporter
578 echo "DELUGE_PASSWORD=$(cat ${config.services.prometheus.exporters.deluge.delugePasswordFile})" > /etc/deluge-exporter/password
579 '';
580 };
581 })
582 ]
583 ++ (mapAttrsToList (
584 name: conf:
585 mkExporterConf {
586 inherit name;
587 inherit (conf) serviceOpts;
588 conf = cfg.${name};
589 }
590 ) exporterOpts)
591 );
592
593 meta = {
594 doc = ./exporters.md;
595 maintainers = [ ];
596 };
597}