1{ config, pkgs, lib, options, ... }:
2
3let
4 inherit (lib) concatStrings foldl foldl' genAttrs literalExpression maintainers
5 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 "fritzbox"
38 "influxdb"
39 "ipmi"
40 "json"
41 "jitsi"
42 "kea"
43 "keylight"
44 "knot"
45 "lnd"
46 "mail"
47 "mikrotik"
48 "minio"
49 "modemmanager"
50 "nextcloud"
51 "nginx"
52 "nginxlog"
53 "node"
54 "nut"
55 "openldap"
56 "openvpn"
57 "pihole"
58 "postfix"
59 "postgres"
60 "process"
61 "pve"
62 "py-air-control"
63 "redis"
64 "rspamd"
65 "rtl_433"
66 "script"
67 "shelly"
68 "snmp"
69 "smartctl"
70 "smokeping"
71 "sql"
72 "statsd"
73 "surfboard"
74 "systemd"
75 "tor"
76 "unbound"
77 "unifi"
78 "unpoller"
79 "v2ray"
80 "varnish"
81 "wireguard"
82 "flow"
83 "zfs"
84 ] (name:
85 import (./. + "/exporters/${name}.nix") { inherit config lib pkgs options; }
86 );
87
88 mkExporterOpts = ({ name, port }: {
89 enable = mkEnableOption (lib.mdDoc "the prometheus ${name} exporter");
90 port = mkOption {
91 type = types.port;
92 default = port;
93 description = lib.mdDoc ''
94 Port to listen on.
95 '';
96 };
97 listenAddress = mkOption {
98 type = types.str;
99 default = "0.0.0.0";
100 description = lib.mdDoc ''
101 Address to listen on.
102 '';
103 };
104 extraFlags = mkOption {
105 type = types.listOf types.str;
106 default = [];
107 description = lib.mdDoc ''
108 Extra commandline options to pass to the ${name} exporter.
109 '';
110 };
111 openFirewall = mkOption {
112 type = types.bool;
113 default = false;
114 description = lib.mdDoc ''
115 Open port in firewall for incoming connections.
116 '';
117 };
118 firewallFilter = mkOption {
119 type = types.nullOr types.str;
120 default = null;
121 example = literalExpression ''
122 "-i eth0 -p tcp -m tcp --dport ${toString port}"
123 '';
124 description = lib.mdDoc ''
125 Specify a filter for iptables to use when
126 {option}`services.prometheus.exporters.${name}.openFirewall`
127 is true. It is used as `ip46tables -I nixos-fw firewallFilter -j nixos-fw-accept`.
128 '';
129 };
130 user = mkOption {
131 type = types.str;
132 default = "${name}-exporter";
133 description = lib.mdDoc ''
134 User name under which the ${name} exporter shall be run.
135 '';
136 };
137 group = mkOption {
138 type = types.str;
139 default = "${name}-exporter";
140 description = lib.mdDoc ''
141 Group under which the ${name} exporter shall be run.
142 '';
143 };
144 });
145
146 mkSubModule = { name, port, extraOpts, imports }: {
147 ${name} = mkOption {
148 type = types.submodule [{
149 inherit imports;
150 options = (mkExporterOpts {
151 inherit name port;
152 } // extraOpts);
153 } ({ config, ... }: mkIf config.openFirewall {
154 firewallFilter = mkDefault "-p tcp -m tcp --dport ${toString config.port}";
155 })];
156 internal = true;
157 default = {};
158 };
159 };
160
161 mkSubModules = (foldl' (a: b: a//b) {}
162 (mapAttrsToList (name: opts: mkSubModule {
163 inherit name;
164 inherit (opts) port;
165 extraOpts = opts.extraOpts or {};
166 imports = opts.imports or [];
167 }) exporterOpts)
168 );
169
170 mkExporterConf = { name, conf, serviceOpts }:
171 let
172 enableDynamicUser = serviceOpts.serviceConfig.DynamicUser or true;
173 in
174 mkIf conf.enable {
175 warnings = conf.warnings or [];
176 users.users."${name}-exporter" = (mkIf (conf.user == "${name}-exporter" && !enableDynamicUser) {
177 description = "Prometheus ${name} exporter service user";
178 isSystemUser = true;
179 inherit (conf) group;
180 });
181 users.groups = (mkIf (conf.group == "${name}-exporter" && !enableDynamicUser) {
182 "${name}-exporter" = {};
183 });
184 networking.firewall.extraCommands = mkIf conf.openFirewall (concatStrings [
185 "ip46tables -A nixos-fw ${conf.firewallFilter} "
186 "-m comment --comment ${name}-exporter -j nixos-fw-accept"
187 ]);
188 systemd.services."prometheus-${name}-exporter" = mkMerge ([{
189 wantedBy = [ "multi-user.target" ];
190 after = [ "network.target" ];
191 serviceConfig.Restart = mkDefault "always";
192 serviceConfig.PrivateTmp = mkDefault true;
193 serviceConfig.WorkingDirectory = mkDefault /tmp;
194 serviceConfig.DynamicUser = mkDefault enableDynamicUser;
195 serviceConfig.User = mkDefault conf.user;
196 serviceConfig.Group = conf.group;
197 # Hardening
198 serviceConfig.CapabilityBoundingSet = mkDefault [ "" ];
199 serviceConfig.DeviceAllow = [ "" ];
200 serviceConfig.LockPersonality = true;
201 serviceConfig.MemoryDenyWriteExecute = true;
202 serviceConfig.NoNewPrivileges = true;
203 serviceConfig.PrivateDevices = mkDefault true;
204 serviceConfig.ProtectClock = mkDefault true;
205 serviceConfig.ProtectControlGroups = true;
206 serviceConfig.ProtectHome = true;
207 serviceConfig.ProtectHostname = true;
208 serviceConfig.ProtectKernelLogs = true;
209 serviceConfig.ProtectKernelModules = true;
210 serviceConfig.ProtectKernelTunables = true;
211 serviceConfig.ProtectSystem = mkDefault "strict";
212 serviceConfig.RemoveIPC = true;
213 serviceConfig.RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
214 serviceConfig.RestrictNamespaces = true;
215 serviceConfig.RestrictRealtime = true;
216 serviceConfig.RestrictSUIDSGID = true;
217 serviceConfig.SystemCallArchitectures = "native";
218 serviceConfig.UMask = "0077";
219 } serviceOpts ]);
220 };
221in
222{
223
224 imports = (lib.forEach [ "blackboxExporter" "collectdExporter" "fritzboxExporter"
225 "jsonExporter" "minioExporter" "nginxExporter" "nodeExporter"
226 "snmpExporter" "unifiExporter" "varnishExporter" ]
227 (opt: lib.mkRemovedOptionModule [ "services" "prometheus" "${opt}" ] ''
228 The prometheus exporters are now configured using `services.prometheus.exporters'.
229 See the 18.03 release notes for more information.
230 '' ));
231
232 options.services.prometheus.exporters = mkOption {
233 type = types.submodule {
234 options = (mkSubModules);
235 imports = [
236 ../../../misc/assertions.nix
237 (lib.mkRenamedOptionModule [ "unifi-poller" ] [ "unpoller" ])
238 ];
239 };
240 description = lib.mdDoc "Prometheus exporter configuration";
241 default = {};
242 example = literalExpression ''
243 {
244 node = {
245 enable = true;
246 enabledCollectors = [ "systemd" ];
247 };
248 varnish.enable = true;
249 }
250 '';
251 };
252
253 config = mkMerge ([{
254 assertions = [ {
255 assertion = cfg.ipmi.enable -> (cfg.ipmi.configFile != null) -> (
256 !(lib.hasPrefix "/tmp/" cfg.ipmi.configFile)
257 );
258 message = ''
259 Config file specified in `services.prometheus.exporters.ipmi.configFile' must
260 not reside within /tmp - it won't be visible to the systemd service.
261 '';
262 } {
263 assertion = cfg.ipmi.enable -> (cfg.ipmi.webConfigFile != null) -> (
264 !(lib.hasPrefix "/tmp/" cfg.ipmi.webConfigFile)
265 );
266 message = ''
267 Config file specified in `services.prometheus.exporters.ipmi.webConfigFile' must
268 not reside within /tmp - it won't be visible to the systemd service.
269 '';
270 } {
271 assertion = cfg.snmp.enable -> (
272 (cfg.snmp.configurationPath == null) != (cfg.snmp.configuration == null)
273 );
274 message = ''
275 Please ensure you have either `services.prometheus.exporters.snmp.configuration'
276 or `services.prometheus.exporters.snmp.configurationPath' set!
277 '';
278 } {
279 assertion = cfg.mikrotik.enable -> (
280 (cfg.mikrotik.configFile == null) != (cfg.mikrotik.configuration == null)
281 );
282 message = ''
283 Please specify either `services.prometheus.exporters.mikrotik.configuration'
284 or `services.prometheus.exporters.mikrotik.configFile'.
285 '';
286 } {
287 assertion = cfg.mail.enable -> (
288 (cfg.mail.configFile == null) != (cfg.mail.configuration == null)
289 );
290 message = ''
291 Please specify either 'services.prometheus.exporters.mail.configuration'
292 or 'services.prometheus.exporters.mail.configFile'.
293 '';
294 } {
295 assertion = cfg.sql.enable -> (
296 (cfg.sql.configFile == null) != (cfg.sql.configuration == null)
297 );
298 message = ''
299 Please specify either 'services.prometheus.exporters.sql.configuration' or
300 'services.prometheus.exporters.sql.configFile'
301 '';
302 } ] ++ (flip map (attrNames exporterOpts) (exporter: {
303 assertion = cfg.${exporter}.firewallFilter != null -> cfg.${exporter}.openFirewall;
304 message = ''
305 The `firewallFilter'-option of exporter ${exporter} doesn't have any effect unless
306 `openFirewall' is set to `true'!
307 '';
308 })) ++ config.services.prometheus.exporters.assertions;
309 warnings = config.services.prometheus.exporters.warnings;
310 }] ++ [(mkIf config.services.minio.enable {
311 services.prometheus.exporters.minio.minioAddress = mkDefault "http://localhost:9000";
312 services.prometheus.exporters.minio.minioAccessKey = mkDefault config.services.minio.accessKey;
313 services.prometheus.exporters.minio.minioAccessSecret = mkDefault config.services.minio.secretKey;
314 })] ++ [(mkIf config.services.prometheus.exporters.rtl_433.enable {
315 hardware.rtl-sdr.enable = mkDefault true;
316 })] ++ [(mkIf config.services.postfix.enable {
317 services.prometheus.exporters.postfix.group = mkDefault config.services.postfix.setgidGroup;
318 })] ++ (mapAttrsToList (name: conf:
319 mkExporterConf {
320 inherit name;
321 inherit (conf) serviceOpts;
322 conf = cfg.${name};
323 }) exporterOpts)
324 );
325
326 meta = {
327 doc = ./exporters.md;
328 maintainers = [ maintainers.willibutz ];
329 };
330}