1{ config, pkgs, lib, options, utils, ... }:
2
3let
4 inherit (lib) concatStrings foldl foldl' genAttrs literalExpression maintainers
5 mapAttrs mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption
6 optional types mkOptionDefault flip attrNames;
7
8 cfg = config.services.prometheus.exporters;
9
10 # each attribute in `exporterOpts` is expected to have specified:
11 # - port (types.int): port on which the exporter listens
12 # - serviceOpts (types.attrs): config that is merged with the
13 # default definition of the exporter's
14 # systemd service
15 # - extraOpts (types.attrs): extra configuration options to
16 # configure the exporter with, which
17 # are appended to the default options
18 #
19 # Note that `extraOpts` is optional, but a script for the exporter's
20 # systemd service must be provided by specifying either
21 # `serviceOpts.script` or `serviceOpts.serviceConfig.ExecStart`
22
23 exporterOpts = (genAttrs [
24 "apcupsd"
25 "artifactory"
26 "bind"
27 "bird"
28 "bitcoin"
29 "blackbox"
30 "buildkite-agent"
31 "collectd"
32 "dmarc"
33 "dnsmasq"
34 "dnssec"
35 "domain"
36 "dovecot"
37 "fastly"
38 "flow"
39 "fritz"
40 "fritzbox"
41 "graphite"
42 "idrac"
43 "imap-mailstat"
44 "influxdb"
45 "ipmi"
46 "jitsi"
47 "json"
48 "junos-czerwonk"
49 "kea"
50 "keylight"
51 "knot"
52 "lnd"
53 "mail"
54 "mikrotik"
55 "minio"
56 "modemmanager"
57 "mongodb"
58 "mysqld"
59 "nats"
60 "nextcloud"
61 "nginx"
62 "nginxlog"
63 "node"
64 "nut"
65 "openldap"
66 "pgbouncer"
67 "php-fpm"
68 "pihole"
69 "ping"
70 "postfix"
71 "postgres"
72 "process"
73 "pve"
74 "py-air-control"
75 "redis"
76 "restic"
77 "rspamd"
78 "rtl_433"
79 "sabnzbd"
80 "scaphandre"
81 "script"
82 "shelly"
83 "smartctl"
84 "smokeping"
85 "snmp"
86 "sql"
87 "statsd"
88 "surfboard"
89 "systemd"
90 "tor"
91 "unbound"
92 "unifi"
93 "unpoller"
94 "v2ray"
95 "varnish"
96 "wireguard"
97 "zfs"
98 ]
99 (name:
100 import (./. + "/exporters/${name}.nix") { inherit config lib pkgs options utils; }
101 )) // (mapAttrs
102 (name: params:
103 import (./. + "/exporters/${params.name}.nix") { inherit config lib pkgs options utils; type = params.type ; })
104 {
105 exportarr-bazarr = {
106 name = "exportarr";
107 type = "bazarr";
108 };
109 exportarr-lidarr = {
110 name = "exportarr";
111 type = "lidarr";
112 };
113 exportarr-prowlarr = {
114 name = "exportarr";
115 type = "prowlarr";
116 };
117 exportarr-radarr = {
118 name = "exportarr";
119 type = "radarr";
120 };
121 exportarr-readarr = {
122 name = "exportarr";
123 type = "readarr";
124 };
125 exportarr-sonarr = {
126 name = "exportarr";
127 type = "sonarr";
128 };
129 }
130 );
131
132 mkExporterOpts = ({ name, port }: {
133 enable = mkEnableOption "the prometheus ${name} exporter";
134 port = mkOption {
135 type = types.port;
136 default = port;
137 description = ''
138 Port to listen on.
139 '';
140 };
141 listenAddress = mkOption {
142 type = types.str;
143 default = "0.0.0.0";
144 description = ''
145 Address to listen on.
146 '';
147 };
148 extraFlags = mkOption {
149 type = types.listOf types.str;
150 default = [];
151 description = ''
152 Extra commandline options to pass to the ${name} exporter.
153 '';
154 };
155 openFirewall = mkOption {
156 type = types.bool;
157 default = false;
158 description = ''
159 Open port in firewall for incoming connections.
160 '';
161 };
162 firewallFilter = mkOption {
163 type = types.nullOr types.str;
164 default = null;
165 example = literalExpression ''
166 "-i eth0 -p tcp -m tcp --dport ${toString port}"
167 '';
168 description = ''
169 Specify a filter for iptables to use when
170 {option}`services.prometheus.exporters.${name}.openFirewall`
171 is true. It is used as `ip46tables -I nixos-fw firewallFilter -j nixos-fw-accept`.
172 '';
173 };
174 firewallRules = mkOption {
175 type = types.nullOr types.lines;
176 default = null;
177 example = literalExpression ''
178 iifname "eth0" tcp dport ${toString port} counter accept
179 '';
180 description = ''
181 Specify rules for nftables to add to the input chain
182 when {option}`services.prometheus.exporters.${name}.openFirewall` is true.
183 '';
184 };
185 user = mkOption {
186 type = types.str;
187 default = "${name}-exporter";
188 description = ''
189 User name under which the ${name} exporter shall be run.
190 '';
191 };
192 group = mkOption {
193 type = types.str;
194 default = "${name}-exporter";
195 description = ''
196 Group under which the ${name} exporter shall be run.
197 '';
198 };
199 });
200
201 mkSubModule = { name, port, extraOpts, imports }: {
202 ${name} = mkOption {
203 type = types.submodule [{
204 inherit imports;
205 options = (mkExporterOpts {
206 inherit name port;
207 } // extraOpts);
208 } ({ config, ... }: mkIf config.openFirewall {
209 firewallFilter = mkDefault "-p tcp -m tcp --dport ${toString config.port}";
210 firewallRules = mkDefault ''tcp dport ${toString config.port} accept comment "${name}-exporter"'';
211 })];
212 internal = true;
213 default = {};
214 };
215 };
216
217 mkSubModules = (foldl' (a: b: a//b) {}
218 (mapAttrsToList (name: opts: mkSubModule {
219 inherit name;
220 inherit (opts) port;
221 extraOpts = opts.extraOpts or {};
222 imports = opts.imports or [];
223 }) exporterOpts)
224 );
225
226 mkExporterConf = { name, conf, serviceOpts }:
227 let
228 enableDynamicUser = serviceOpts.serviceConfig.DynamicUser or true;
229 nftables = config.networking.nftables.enable;
230 in
231 mkIf conf.enable {
232 warnings = conf.warnings or [];
233 users.users."${name}-exporter" = (mkIf (conf.user == "${name}-exporter" && !enableDynamicUser) {
234 description = "Prometheus ${name} exporter service user";
235 isSystemUser = true;
236 inherit (conf) group;
237 });
238 users.groups = (mkIf (conf.group == "${name}-exporter" && !enableDynamicUser) {
239 "${name}-exporter" = {};
240 });
241 networking.firewall.extraCommands = mkIf (conf.openFirewall && !nftables) (concatStrings [
242 "ip46tables -A nixos-fw ${conf.firewallFilter} "
243 "-m comment --comment ${name}-exporter -j nixos-fw-accept"
244 ]);
245 networking.firewall.extraInputRules = mkIf (conf.openFirewall && nftables) conf.firewallRules;
246 systemd.services."prometheus-${name}-exporter" = mkMerge ([{
247 wantedBy = [ "multi-user.target" ];
248 after = [ "network.target" ];
249 serviceConfig.Restart = mkDefault "always";
250 serviceConfig.PrivateTmp = mkDefault true;
251 serviceConfig.WorkingDirectory = mkDefault /tmp;
252 serviceConfig.DynamicUser = mkDefault enableDynamicUser;
253 serviceConfig.User = mkDefault conf.user;
254 serviceConfig.Group = conf.group;
255 # Hardening
256 serviceConfig.CapabilityBoundingSet = mkDefault [ "" ];
257 serviceConfig.DeviceAllow = [ "" ];
258 serviceConfig.LockPersonality = true;
259 serviceConfig.MemoryDenyWriteExecute = true;
260 serviceConfig.NoNewPrivileges = true;
261 serviceConfig.PrivateDevices = mkDefault true;
262 serviceConfig.ProtectClock = mkDefault true;
263 serviceConfig.ProtectControlGroups = true;
264 serviceConfig.ProtectHome = true;
265 serviceConfig.ProtectHostname = true;
266 serviceConfig.ProtectKernelLogs = true;
267 serviceConfig.ProtectKernelModules = true;
268 serviceConfig.ProtectKernelTunables = true;
269 serviceConfig.ProtectSystem = mkDefault "strict";
270 serviceConfig.RemoveIPC = true;
271 serviceConfig.RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
272 serviceConfig.RestrictNamespaces = true;
273 serviceConfig.RestrictRealtime = true;
274 serviceConfig.RestrictSUIDSGID = true;
275 serviceConfig.SystemCallArchitectures = "native";
276 serviceConfig.UMask = "0077";
277 } serviceOpts ]);
278 };
279in
280{
281
282 imports = (lib.forEach [ "blackboxExporter" "collectdExporter" "fritzboxExporter"
283 "jsonExporter" "minioExporter" "nginxExporter" "nodeExporter"
284 "snmpExporter" "unifiExporter" "varnishExporter" ]
285 (opt: lib.mkRemovedOptionModule [ "services" "prometheus" "${opt}" ] ''
286 The prometheus exporters are now configured using `services.prometheus.exporters'.
287 See the 18.03 release notes for more information.
288 '' ));
289
290 options.services.prometheus.exporters = mkOption {
291 type = types.submodule {
292 options = (mkSubModules);
293 imports = [
294 ../../../misc/assertions.nix
295 (lib.mkRenamedOptionModule [ "unifi-poller" ] [ "unpoller" ])
296 ];
297 };
298 description = "Prometheus exporter configuration";
299 default = {};
300 example = literalExpression ''
301 {
302 node = {
303 enable = true;
304 enabledCollectors = [ "systemd" ];
305 };
306 varnish.enable = true;
307 }
308 '';
309 };
310
311 config = mkMerge ([{
312 assertions = [ {
313 assertion = cfg.ipmi.enable -> (cfg.ipmi.configFile != null) -> (
314 !(lib.hasPrefix "/tmp/" cfg.ipmi.configFile)
315 );
316 message = ''
317 Config file specified in `services.prometheus.exporters.ipmi.configFile' must
318 not reside within /tmp - it won't be visible to the systemd service.
319 '';
320 } {
321 assertion = cfg.ipmi.enable -> (cfg.ipmi.webConfigFile != null) -> (
322 !(lib.hasPrefix "/tmp/" cfg.ipmi.webConfigFile)
323 );
324 message = ''
325 Config file specified in `services.prometheus.exporters.ipmi.webConfigFile' must
326 not reside within /tmp - it won't be visible to the systemd service.
327 '';
328 } {
329 assertion = cfg.snmp.enable -> (
330 (cfg.snmp.configurationPath == null) != (cfg.snmp.configuration == null)
331 );
332 message = ''
333 Please ensure you have either `services.prometheus.exporters.snmp.configuration'
334 or `services.prometheus.exporters.snmp.configurationPath' set!
335 '';
336 } {
337 assertion = cfg.mikrotik.enable -> (
338 (cfg.mikrotik.configFile == null) != (cfg.mikrotik.configuration == null)
339 );
340 message = ''
341 Please specify either `services.prometheus.exporters.mikrotik.configuration'
342 or `services.prometheus.exporters.mikrotik.configFile'.
343 '';
344 } {
345 assertion = cfg.mail.enable -> (
346 (cfg.mail.configFile == null) != (cfg.mail.configuration == null)
347 );
348 message = ''
349 Please specify either 'services.prometheus.exporters.mail.configuration'
350 or 'services.prometheus.exporters.mail.configFile'.
351 '';
352 } {
353 assertion = cfg.mysqld.runAsLocalSuperUser -> config.services.mysql.enable;
354 message = ''
355 The exporter is configured to run as 'services.mysql.user', but
356 'services.mysql.enable' is set to false.
357 '';
358 } {
359 assertion = cfg.nextcloud.enable -> (
360 (cfg.nextcloud.passwordFile == null) != (cfg.nextcloud.tokenFile == null)
361 );
362 message = ''
363 Please specify either 'services.prometheus.exporters.nextcloud.passwordFile' or
364 'services.prometheus.exporters.nextcloud.tokenFile'
365 '';
366 } {
367 assertion = cfg.pgbouncer.enable -> (
368 (cfg.pgbouncer.connectionStringFile != null || cfg.pgbouncer.connectionString != "")
369 );
370 message = ''
371 PgBouncer exporter needs either connectionStringFile or connectionString configured"
372 '';
373 } {
374 assertion = cfg.pgbouncer.enable -> (
375 config.services.pgbouncer.ignoreStartupParameters != null && builtins.match ".*extra_float_digits.*" config.services.pgbouncer.ignoreStartupParameters != null
376 );
377 message = ''
378 Prometheus PgBouncer exporter requires including `extra_float_digits` in services.pgbouncer.ignoreStartupParameters
379
380 Example:
381 services.pgbouncer.ignoreStartupParameters = extra_float_digits;
382
383 See https://github.com/prometheus-community/pgbouncer_exporter#pgbouncer-configuration
384 '';
385 } {
386 assertion = cfg.sql.enable -> (
387 (cfg.sql.configFile == null) != (cfg.sql.configuration == null)
388 );
389 message = ''
390 Please specify either 'services.prometheus.exporters.sql.configuration' or
391 'services.prometheus.exporters.sql.configFile'
392 '';
393 } {
394 assertion = cfg.scaphandre.enable -> (pkgs.stdenv.targetPlatform.isx86_64 == true);
395 message = ''
396 Scaphandre only support x86_64 architectures.
397 '';
398 } {
399 assertion = cfg.scaphandre.enable -> ((lib.kernel.whenHelpers pkgs.linux.version).whenOlder "5.11" true).condition == false;
400 message = ''
401 Scaphandre requires a kernel version newer than '5.11', '${pkgs.linux.version}' given.
402 '';
403 } {
404 assertion = cfg.scaphandre.enable -> (builtins.elem "intel_rapl_common" config.boot.kernelModules);
405 message = ''
406 Scaphandre needs 'intel_rapl_common' kernel module to be enabled. Please add it in 'boot.kernelModules'.
407 '';
408 } {
409 assertion = cfg.idrac.enable -> (
410 (cfg.idrac.configurationPath == null) != (cfg.idrac.configuration == null)
411 );
412 message = ''
413 Please ensure you have either `services.prometheus.exporters.idrac.configuration'
414 or `services.prometheus.exporters.idrac.configurationPath' set!
415 '';
416 } ] ++ (flip map (attrNames exporterOpts) (exporter: {
417 assertion = cfg.${exporter}.firewallFilter != null -> cfg.${exporter}.openFirewall;
418 message = ''
419 The `firewallFilter'-option of exporter ${exporter} doesn't have any effect unless
420 `openFirewall' is set to `true'!
421 '';
422 })) ++ config.services.prometheus.exporters.assertions;
423 warnings = [
424 (mkIf (config.services.prometheus.exporters.idrac.enable && config.services.prometheus.exporters.idrac.configurationPath != null) ''
425 Configuration file in `services.prometheus.exporters.idrac.configurationPath` may override
426 `services.prometheus.exporters.idrac.listenAddress` and/or `services.prometheus.exporters.idrac.port`.
427 Consider using `services.prometheus.exporters.idrac.configuration` instead.
428 ''
429 )
430 (mkIf
431 (cfg.pgbouncer.enable && cfg.pgbouncer.connectionString != "") ''
432 config.services.prometheus.exporters.pgbouncer.connectionString is insecure. Use connectionStringFile instead.
433 ''
434 )
435 (mkIf
436 (cfg.pgbouncer.enable && config.services.pgbouncer.authType != "any") ''
437 Admin user (with password or passwordless) MUST exist in the services.pgbouncer.authFile if authType other than any is used.
438 ''
439 )
440 ] ++ config.services.prometheus.exporters.warnings;
441 }] ++ [(mkIf config.services.minio.enable {
442 services.prometheus.exporters.minio.minioAddress = mkDefault "http://localhost:9000";
443 services.prometheus.exporters.minio.minioAccessKey = mkDefault config.services.minio.accessKey;
444 services.prometheus.exporters.minio.minioAccessSecret = mkDefault config.services.minio.secretKey;
445 })] ++ [(mkIf config.services.prometheus.exporters.rtl_433.enable {
446 hardware.rtl-sdr.enable = mkDefault true;
447 })] ++ [(mkIf config.services.postfix.enable {
448 services.prometheus.exporters.postfix.group = mkDefault config.services.postfix.setgidGroup;
449 })] ++ (mapAttrsToList (name: conf:
450 mkExporterConf {
451 inherit name;
452 inherit (conf) serviceOpts;
453 conf = cfg.${name};
454 }) exporterOpts)
455 );
456
457 meta = {
458 doc = ./exporters.md;
459 maintainers = [ maintainers.willibutz ];
460 };
461}