diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index 428dbdcc54c29..a53fe3b17704e 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -905,6 +905,9 @@ "module-services-prometheus-exporters-update-exporter-module": [ "index.html#module-services-prometheus-exporters-update-exporter-module" ], + "module-services-prometheus-exporters-downstream-exporter": [ + "index.html#module-services-prometheus-exporters-downstream-exporter" + ], "module-services-parsedmarc": [ "index.html#module-services-parsedmarc" ], diff --git a/nixos/modules/services/monitoring/prometheus/exporters.md b/nixos/modules/services/monitoring/prometheus/exporters.md index 23464b9d315ef..c7154b0c4597f 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters.md +++ b/nixos/modules/services/monitoring/prometheus/exporters.md @@ -178,3 +178,20 @@ in ]; } ``` + +## Adding a downstream exporter {#module-services-prometheus-exporters-downstream-exporter} + +First create the exporter file, `EXPORTER-exporter.nix` as you would with an upstream exporter + +Then create a module that imports the exporter + +```nix +{ modulesPath, ... }: { + imports = [ + (import "${modulesPath}/services/monitoring/prometheus/mk-downstream-exporter.nix" { + name = "EXPORTER"; + file = ./EXPORTER-exporter.nix; + }) + ]; +} +``` diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix index f805920c5b87a..ef92e6469bca7 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters.nix @@ -1,5 +1,7 @@ { config, pkgs, lib, options, utils, ... }: +with (import ./mk-exporter.nix config.networking.nftables.enable lib pkgs); + let inherit (lib) concatStrings foldl foldl' genAttrs literalExpression maintainers mapAttrs mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption @@ -133,91 +135,6 @@ let } ); - 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; @@ -226,69 +143,6 @@ let 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 { diff --git a/nixos/modules/services/monitoring/prometheus/mk-downstream-exporter.nix b/nixos/modules/services/monitoring/prometheus/mk-downstream-exporter.nix new file mode 100644 index 0000000000000..6f55e19e2300c --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/mk-downstream-exporter.nix @@ -0,0 +1,8 @@ +{ name, file }: { config, pkgs, lib, options, utils, ... }@args: + +with (import ./mk-exporter.nix config.networking.nftables.enable lib pkgs); + +{ + options.services.prometheus.exporters = mkDownstreamOptions name (import file args); + config = mkDownstreamConfig name (import file args) config.services.prometheus.exporters.${name}; +} diff --git a/nixos/modules/services/monitoring/prometheus/mk-exporter.nix b/nixos/modules/services/monitoring/prometheus/mk-exporter.nix new file mode 100644 index 0000000000000..c055f9b5c36c5 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/mk-exporter.nix @@ -0,0 +1,187 @@ +nftables: lib: pkgs: + +let + inherit (lib) concatStrings foldl foldl' genAttrs literalExpression maintainers + mapAttrs mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption + optional types mkOptionDefault flip attrNames; + + # 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` + + 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 = {}; + }; + }; + + mkExporterConf = { name, conf, serviceOpts }: + let + enableDynamicUser = serviceOpts.serviceConfig.DynamicUser or true; + 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 ]); + }; + + mkDownstreamOptions = name: opts: + mkSubModule { + inherit name; + inherit (opts) port; + extraOpts = opts.extraOpts or {}; + imports = opts.imports or []; + }; + + mkDownstreamConfig = name: opts: cfg: + mkExporterConf { + inherit name; + inherit (opts) serviceOpts; + conf = cfg; + }; +in +{ + inherit + mkExporterConf mkSubModule + mkDownstreamOptions mkDownstreamConfig; +}