nixpkgs/nixos/modules/services/monitoring/prometheus/exporters.nix
Wolfgang Walther 91a8fee3aa
treewide: remove redundant parentheses
Auto-fixed by nixf-diagnose.
2025-10-05 10:52:03 +02:00

598 lines
18 KiB
Nix

{
config,
pkgs,
lib,
options,
utils,
...
}:
let
inherit (lib)
concatStrings
foldl
foldl'
genAttrs
literalExpression
maintainers
mapAttrs
mapAttrsToList
mkDefault
mkEnableOption
mkIf
mkMerge
mkOption
optional
types
mkOptionDefault
flip
attrNames
xor
;
cfg = config.services.prometheus.exporters;
# each attribute in `exporterOpts` is expected to have specified:
# - port (types.int): port on which the exporter listens
# - serviceOpts (types.attrs): config that is merged with the
# default definition of the exporter's
# systemd service
# - extraOpts (types.attrs): extra configuration options to
# configure the exporter with, which
# are appended to the default options
#
# Note that `extraOpts` is optional, but a script for the exporter's
# systemd service must be provided by specifying either
# `serviceOpts.script` or `serviceOpts.serviceConfig.ExecStart`
exporterOpts =
(genAttrs
[
"apcupsd"
"artifactory"
"bind"
"bird"
"bitcoin"
"blackbox"
"borgmatic"
"buildkite-agent"
"ecoflow"
"chrony"
"collectd"
"deluge"
"dmarc"
"dnsmasq"
"dnssec"
"domain"
"dovecot"
"ebpf"
"fastly"
"flow"
"fritz"
"fritzbox"
"frr"
"graphite"
"idrac"
"imap-mailstat"
"influxdb"
"ipmi"
"jitsi"
"json"
"junos-czerwonk"
"kafka"
"kea"
"keylight"
"klipper"
"knot"
"libvirt"
"lnd"
"mail"
"mailman3"
"mikrotik"
"modemmanager"
"mongodb"
"mqtt"
"mysqld"
"nats"
"nextcloud"
"nginx"
"nginxlog"
"node"
"node-cert"
"nut"
"nvidia-gpu"
"pgbouncer"
"php-fpm"
"pihole"
"ping"
"postfix"
"postgres"
"process"
"pve"
"py-air-control"
"rasdaemon"
"redis"
"restic"
"rspamd"
"rtl_433"
"sabnzbd"
"scaphandre"
"script"
"shelly"
"smartctl"
"smokeping"
"snmp"
"sql"
"statsd"
"storagebox"
"surfboard"
"systemd"
"tibber"
"unbound"
"unpoller"
"v2ray"
"varnish"
"wireguard"
"zfs"
]
(
name:
import (./. + "/exporters/${name}.nix") {
inherit
config
lib
pkgs
options
utils
;
}
)
)
// (mapAttrs
(
name: params:
import (./. + "/exporters/${params.name}.nix") {
inherit
config
lib
pkgs
options
utils
;
type = params.type;
}
)
{
exportarr-bazarr = {
name = "exportarr";
type = "bazarr";
};
exportarr-lidarr = {
name = "exportarr";
type = "lidarr";
};
exportarr-prowlarr = {
name = "exportarr";
type = "prowlarr";
};
exportarr-radarr = {
name = "exportarr";
type = "radarr";
};
exportarr-readarr = {
name = "exportarr";
type = "readarr";
};
exportarr-sonarr = {
name = "exportarr";
type = "sonarr";
};
}
);
mkExporterOpts = (
{ name, port }:
{
enable = mkEnableOption "the prometheus ${name} exporter";
port = mkOption {
type = types.port;
default = port;
description = ''
Port to listen on.
'';
};
listenAddress = mkOption {
type = types.str;
default = "0.0.0.0";
description = ''
Address to listen on.
'';
};
extraFlags = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
Extra commandline options to pass to the ${name} exporter.
'';
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Open port in firewall for incoming connections.
'';
};
firewallFilter = mkOption {
type = types.nullOr types.str;
default = null;
example = literalExpression ''
"-i eth0 -p tcp -m tcp --dport ${toString port}"
'';
description = ''
Specify a filter for iptables to use when
{option}`services.prometheus.exporters.${name}.openFirewall`
is true. It is used as `ip46tables -I nixos-fw firewallFilter -j nixos-fw-accept`.
'';
};
firewallRules = mkOption {
type = types.nullOr types.lines;
default = null;
example = literalExpression ''
iifname "eth0" tcp dport ${toString port} counter accept
'';
description = ''
Specify rules for nftables to add to the input chain
when {option}`services.prometheus.exporters.${name}.openFirewall` is true.
'';
};
user = mkOption {
type = types.str;
default = "${name}-exporter";
description = ''
User name under which the ${name} exporter shall be run.
'';
};
group = mkOption {
type = types.str;
default = "${name}-exporter";
description = ''
Group under which the ${name} exporter shall be run.
'';
};
}
);
mkSubModule =
{
name,
port,
extraOpts,
imports,
}:
{
${name} = mkOption {
type = types.submodule [
{
inherit imports;
options = (
mkExporterOpts {
inherit name port;
}
// extraOpts
);
}
(
{ config, ... }:
mkIf config.openFirewall {
firewallFilter = mkDefault "-p tcp -m tcp --dport ${toString config.port}";
firewallRules = mkDefault ''tcp dport ${toString config.port} accept comment "${name}-exporter"'';
}
)
];
internal = true;
default = { };
};
};
mkSubModules = (
foldl' (a: b: a // b) { } (
mapAttrsToList (
name: opts:
mkSubModule {
inherit name;
inherit (opts) port;
extraOpts = opts.extraOpts or { };
imports = opts.imports or [ ];
}
) exporterOpts
)
);
mkExporterConf =
{
name,
conf,
serviceOpts,
}:
let
enableDynamicUser = serviceOpts.serviceConfig.DynamicUser or true;
nftables = config.networking.nftables.enable;
in
mkIf conf.enable {
warnings = conf.warnings or [ ];
assertions = conf.assertions or [ ];
users.users."${name}-exporter" = (
mkIf (conf.user == "${name}-exporter" && !enableDynamicUser) {
description = "Prometheus ${name} exporter service user";
isSystemUser = true;
inherit (conf) group;
}
);
users.groups = mkMerge [
(mkIf (conf.group == "${name}-exporter" && !enableDynamicUser) {
"${name}-exporter" = { };
})
(mkIf (name == "smartctl") {
"smartctl-exporter-access" = { };
})
];
services.udev.extraRules = mkIf (name == "smartctl") ''
ACTION=="add", SUBSYSTEM=="nvme", KERNEL=="nvme[0-9]*", RUN+="${pkgs.acl}/bin/setfacl -m g:smartctl-exporter-access:rw /dev/$kernel"
'';
networking.firewall.extraCommands = mkIf (conf.openFirewall && !nftables) (concatStrings [
"ip46tables -A nixos-fw ${conf.firewallFilter} "
"-m comment --comment ${name}-exporter -j nixos-fw-accept"
]);
networking.firewall.extraInputRules = mkIf (conf.openFirewall && nftables) conf.firewallRules;
systemd.services."prometheus-${name}-exporter" = mkMerge [
{
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig.Restart = mkDefault "always";
serviceConfig.PrivateTmp = mkDefault true;
serviceConfig.WorkingDirectory = mkDefault /tmp;
serviceConfig.DynamicUser = mkDefault enableDynamicUser;
serviceConfig.User = mkDefault conf.user;
serviceConfig.Group = conf.group;
# Hardening
serviceConfig.CapabilityBoundingSet = mkDefault [ "" ];
serviceConfig.DeviceAllow = [ "" ];
serviceConfig.LockPersonality = true;
serviceConfig.MemoryDenyWriteExecute = true;
serviceConfig.NoNewPrivileges = true;
serviceConfig.PrivateDevices = mkDefault true;
serviceConfig.ProtectClock = mkDefault true;
serviceConfig.ProtectControlGroups = true;
serviceConfig.ProtectHome = true;
serviceConfig.ProtectHostname = true;
serviceConfig.ProtectKernelLogs = true;
serviceConfig.ProtectKernelModules = true;
serviceConfig.ProtectKernelTunables = true;
serviceConfig.ProtectSystem = mkDefault "strict";
serviceConfig.RemoveIPC = true;
serviceConfig.RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
serviceConfig.RestrictNamespaces = true;
serviceConfig.RestrictRealtime = true;
serviceConfig.RestrictSUIDSGID = true;
serviceConfig.SystemCallArchitectures = "native";
serviceConfig.UMask = "0077";
}
serviceOpts
];
};
in
{
options.services.prometheus.exporters = mkOption {
type = types.submodule {
options = mkSubModules;
imports = [
../../../misc/assertions.nix
(lib.mkRenamedOptionModule [ "unifi-poller" ] [ "unpoller" ])
(lib.mkRemovedOptionModule [ "minio" ] ''
The Minio exporter has been removed, as it was broken and unmaintained.
See the 24.11 release notes for more information.
'')
(lib.mkRemovedOptionModule [ "tor" ] ''
The Tor exporter has been removed, as it was broken and unmaintained.
'')
];
};
description = "Prometheus exporter configuration";
default = { };
example = literalExpression ''
{
node = {
enable = true;
enabledCollectors = [ "systemd" ];
};
varnish.enable = true;
}
'';
};
config = mkMerge (
[
{
assertions = [
{
assertion =
cfg.ipmi.enable -> (cfg.ipmi.configFile != null) -> (!(lib.hasPrefix "/tmp/" cfg.ipmi.configFile));
message = ''
Config file specified in `services.prometheus.exporters.ipmi.configFile' must
not reside within /tmp - it won't be visible to the systemd service.
'';
}
{
assertion =
cfg.ipmi.enable
-> (cfg.ipmi.webConfigFile != null)
-> (!(lib.hasPrefix "/tmp/" cfg.ipmi.webConfigFile));
message = ''
Config file specified in `services.prometheus.exporters.ipmi.webConfigFile' must
not reside within /tmp - it won't be visible to the systemd service.
'';
}
{
assertion =
cfg.restic.enable -> ((cfg.restic.repository == null) != (cfg.restic.repositoryFile == null));
message = ''
Please specify either 'services.prometheus.exporters.restic.repository'
or 'services.prometheus.exporters.restic.repositoryFile'.
'';
}
{
assertion =
cfg.snmp.enable -> ((cfg.snmp.configurationPath == null) != (cfg.snmp.configuration == null));
message = ''
Please ensure you have either `services.prometheus.exporters.snmp.configuration'
or `services.prometheus.exporters.snmp.configurationPath' set!
'';
}
{
assertion =
cfg.mikrotik.enable -> ((cfg.mikrotik.configFile == null) != (cfg.mikrotik.configuration == null));
message = ''
Please specify either `services.prometheus.exporters.mikrotik.configuration'
or `services.prometheus.exporters.mikrotik.configFile'.
'';
}
{
assertion = cfg.mail.enable -> ((cfg.mail.configFile == null) != (cfg.mail.configuration == null));
message = ''
Please specify either 'services.prometheus.exporters.mail.configuration'
or 'services.prometheus.exporters.mail.configFile'.
'';
}
{
assertion = cfg.mysqld.runAsLocalSuperUser -> config.services.mysql.enable;
message = ''
The exporter is configured to run as 'services.mysql.user', but
'services.mysql.enable' is set to false.
'';
}
{
assertion =
cfg.nextcloud.enable -> ((cfg.nextcloud.passwordFile == null) != (cfg.nextcloud.tokenFile == null));
message = ''
Please specify either 'services.prometheus.exporters.nextcloud.passwordFile' or
'services.prometheus.exporters.nextcloud.tokenFile'
'';
}
{
assertion = cfg.sql.enable -> ((cfg.sql.configFile == null) != (cfg.sql.configuration == null));
message = ''
Please specify either 'services.prometheus.exporters.sql.configuration' or
'services.prometheus.exporters.sql.configFile'
'';
}
{
assertion = cfg.scaphandre.enable -> (pkgs.stdenv.targetPlatform.isx86_64 == true);
message = ''
Scaphandre only support x86_64 architectures.
'';
}
{
assertion =
cfg.scaphandre.enable
-> ((lib.kernel.whenHelpers pkgs.linux.version).whenOlder "5.11" true).condition == false;
message = ''
Scaphandre requires a kernel version newer than '5.11', '${pkgs.linux.version}' given.
'';
}
{
assertion = cfg.scaphandre.enable -> (builtins.elem "intel_rapl_common" config.boot.kernelModules);
message = ''
Scaphandre needs 'intel_rapl_common' kernel module to be enabled. Please add it in 'boot.kernelModules'.
'';
}
{
assertion =
cfg.idrac.enable -> ((cfg.idrac.configurationPath == null) != (cfg.idrac.configuration == null));
message = ''
Please ensure you have either `services.prometheus.exporters.idrac.configuration'
or `services.prometheus.exporters.idrac.configurationPath' set!
'';
}
{
assertion =
cfg.deluge.enable
-> ((cfg.deluge.delugePassword == null) != (cfg.deluge.delugePasswordFile == null));
message = ''
Please ensure you have either `services.prometheus.exporters.deluge.delugePassword'
or `services.prometheus.exporters.deluge.delugePasswordFile' set!
'';
}
{
assertion =
cfg.pgbouncer.enable
-> (xor (cfg.pgbouncer.connectionEnvFile == null) (cfg.pgbouncer.connectionString == null));
message = ''
Options `services.prometheus.exporters.pgbouncer.connectionEnvFile` and
`services.prometheus.exporters.pgbouncer.connectionString` are mutually exclusive!
'';
}
]
++ (flip map (attrNames exporterOpts) (exporter: {
assertion = cfg.${exporter}.firewallFilter != null -> cfg.${exporter}.openFirewall;
message = ''
The `firewallFilter'-option of exporter ${exporter} doesn't have any effect unless
`openFirewall' is set to `true'!
'';
}))
++ config.services.prometheus.exporters.assertions;
warnings = [
(mkIf
(
config.services.prometheus.exporters.idrac.enable
&& config.services.prometheus.exporters.idrac.configurationPath != null
)
''
Configuration file in `services.prometheus.exporters.idrac.configurationPath` may override
`services.prometheus.exporters.idrac.listenAddress` and/or `services.prometheus.exporters.idrac.port`.
Consider using `services.prometheus.exporters.idrac.configuration` instead.
''
)
]
++ config.services.prometheus.exporters.warnings;
}
]
++ [
(mkIf config.services.prometheus.exporters.rtl_433.enable {
hardware.rtl-sdr.enable = mkDefault true;
})
]
++ [
(mkIf config.services.postfix.enable {
services.prometheus.exporters.postfix.group = mkDefault config.services.postfix.setgidGroup;
})
]
++ [
(mkIf config.services.prometheus.exporters.deluge.enable {
system.activationScripts = {
deluge-exported.text = ''
mkdir -p /etc/deluge-exporter
echo "DELUGE_PASSWORD=$(cat ${config.services.prometheus.exporters.deluge.delugePasswordFile})" > /etc/deluge-exporter/password
'';
};
})
]
++ (mapAttrsToList (
name: conf:
mkExporterConf {
inherit name;
inherit (conf) serviceOpts;
conf = cfg.${name};
}
) exporterOpts)
);
meta = {
doc = ./exporters.md;
maintainers = [ ];
};
}