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