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