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