diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index b516b1785195..c54bc6098d3e 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -554,6 +554,7 @@ ./services/monitoring/telegraf.nix ./services/monitoring/thanos.nix ./services/monitoring/tuptime.nix + ./services/monitoring/unifi-poller.nix ./services/monitoring/ups.nix ./services/monitoring/uptime.nix ./services/monitoring/vnstat.nix diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix index a5492d972f15..cc71451bf206 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters.nix @@ -46,6 +46,7 @@ let "surfboard" "tor" "unifi" + "unifi-poller" "varnish" "wireguard" ] (name: diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix b/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix new file mode 100644 index 000000000000..394e6e201f03 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/unifi-poller.nix @@ -0,0 +1,34 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.unifi-poller; + + configFile = pkgs.writeText "prometheus-unifi-poller-exporter.json" (generators.toJSON {} { + poller = { inherit (cfg.log) debug quiet; }; + unifi = { inherit (cfg) controllers; }; + influxdb.disable = true; + prometheus = { + http_listen = "${cfg.listenAddress}:${toString cfg.port}"; + report_errors = cfg.log.prometheusErrors; + }; + }); + +in { + port = 9130; + + extraOpts = { + inherit (options.services.unifi-poller.unifi) controllers; + log = { + debug = mkEnableOption "debug logging including line numbers, high resolution timestamps, per-device logs."; + quiet = mkEnableOption "startup and error logs only."; + prometheusErrors = mkEnableOption "emitting errors to prometheus."; + }; + }; + + serviceOpts.serviceConfig = { + ExecStart = "${pkgs.unifi-poller}/bin/unifi-poller --config ${configFile}"; + DynamicUser = false; + }; +} diff --git a/nixos/modules/services/monitoring/unifi-poller.nix b/nixos/modules/services/monitoring/unifi-poller.nix new file mode 100644 index 000000000000..208f5e4875b4 --- /dev/null +++ b/nixos/modules/services/monitoring/unifi-poller.nix @@ -0,0 +1,242 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.unifi-poller; + + configFile = pkgs.writeText "unifi-poller.json" (generators.toJSON {} { + inherit (cfg) poller influxdb prometheus unifi; + }); + +in { + options.services.unifi-poller = { + enable = mkEnableOption "unifi-poller"; + + poller = { + debug = mkOption { + type = types.bool; + default = false; + description = '' + Turns on line numbers, microsecond logging, and a per-device log. + This may be noisy if you have a lot of devices. It adds one line per device. + ''; + }; + quiet = mkOption { + type = types.bool; + default = false; + description = '' + Turns off per-interval logs. Only startup and error logs will be emitted. + ''; + }; + plugins = mkOption { + type = with types; listOf str; + default = []; + description = '' + Load additional plugins. + ''; + }; + }; + + prometheus = { + disable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to disable the prometheus ouput plugin. + ''; + }; + http_listen = mkOption { + type = types.str; + default = "[::]:9130"; + description = '' + Bind the prometheus exporter to this IP or hostname. + ''; + }; + report_errors = mkOption { + type = types.bool; + default = false; + description = '' + Whether to report errors. + ''; + }; + }; + + influxdb = { + disable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to disable the influxdb ouput plugin. + ''; + }; + url = mkOption { + type = types.str; + default = "http://127.0.0.1:8086"; + description = '' + URL of the influxdb host. + ''; + }; + user = mkOption { + type = types.str; + default = "unifipoller"; + description = '' + Username for the influxdb. + ''; + }; + pass = mkOption { + type = types.path; + default = pkgs.writeText "unifi-poller-influxdb-default.password" "unifipoller"; + defaultText = "unifi-poller-influxdb-default.password"; + description = '' + Path of a file containing the password for influxdb. + This file needs to be readable by the unifi-poller user. + ''; + apply = v: "file://${v}"; + }; + db = mkOption { + type = types.str; + default = "unifi"; + description = '' + Database name. Database should exist. + ''; + }; + verify_ssl = mkOption { + type = types.bool; + default = true; + description = '' + Verify the influxdb's certificate. + ''; + }; + interval = mkOption { + type = types.str; + default = "30s"; + description = '' + Setting this lower than the Unifi controller's refresh + interval may lead to zeroes in your database. + ''; + }; + }; + + unifi = let + controllerOptions = { + user = mkOption { + type = types.str; + default = "unifi"; + description = '' + Unifi service user name. + ''; + }; + pass = mkOption { + type = types.path; + default = pkgs.writeText "unifi-poller-unifi-default.password" "unifi"; + defaultText = "unifi-poller-unifi-default.password"; + description = '' + Path of a file containing the password for the unifi service user. + This file needs to be readable by the unifi-poller user. + ''; + apply = v: "file://${v}"; + }; + url = mkOption { + type = types.str; + default = "https://unifi:8443"; + description = '' + URL of the Unifi controller. + ''; + }; + sites = mkOption { + type = with types; either (enum [ "default" "all" ]) (listOf str); + default = "all"; + description = '' + List of site names for which statistics should be exported. + Or the string "default" for the default site or the string "all" for all sites. + ''; + apply = toList; + }; + save_ids = mkOption { + type = types.bool; + default = false; + description = '' + Collect and save data from the intrusion detection system to influxdb. + ''; + }; + save_dpi = mkOption { + type = types.bool; + default = false; + description = '' + Collect and save data from deep packet inspection. + Adds around 150 data points and impacts performance. + ''; + }; + save_sites = mkOption { + type = types.bool; + default = true; + description = '' + Collect and save site data. + ''; + }; + hash_pii = mkOption { + type = types.bool; + default = false; + description = '' + Hash, with md5, client names and MAC addresses. This attempts + to protect personally identifiable information. + ''; + }; + verify_ssl = mkOption { + type = types.bool; + default = true; + description = '' + Verify the Unifi controller's certificate. + ''; + }; + }; + + in { + dynamic = mkOption { + type = types.bool; + default = false; + description = '' + Let prometheus select which controller to poll when scraping. + Use with default credentials. See unifi-poller wiki for more. + ''; + }; + + defaults = controllerOptions; + + controllers = mkOption { + type = with types; listOf (submodule { options = controllerOptions; }); + default = []; + description = '' + List of Unifi controllers to poll. Use defaults if empty. + ''; + apply = map (flip removeAttrs [ "_module" ]); + }; + }; + }; + + config = mkIf cfg.enable { + users.groups.unifi-poller = { }; + users.users.unifi-poller = { + description = "unifi-poller Service User"; + group = "unifi-poller"; + isSystemUser = true; + }; + + systemd.services.unifi-poller = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + ExecStart = "${pkgs.unifi-poller}/bin/unifi-poller --config ${configFile}"; + Restart = "always"; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "full"; + DevicePolicy = "closed"; + NoNewPrivileges = true; + User = "unifi-poller"; + WorkingDirectory = "/tmp"; + }; + }; + }; +} diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix index b912e3425e0e..fdcc40721324 100644 --- a/nixos/tests/prometheus-exporters.nix +++ b/nixos/tests/prometheus-exporters.nix @@ -22,6 +22,9 @@ let * `metricProvider` (optional) * this attribute contains additional machine config * + * `nodeName` (optional) + * override an incompatible testnode name + * * Example: * exporterTests. = { * exporterConfig = { @@ -590,6 +593,19 @@ let ''; }; + unifi-poller = { + nodeName = "unifi_poller"; + exporterConfig.enable = true; + exporterConfig.controllers = [ { } ]; + exporterTest = '' + wait_for_unit("prometheus-unifi-poller-exporter.service") + wait_for_open_port(9130) + succeed( + "curl -sSf localhost:9130/metrics | grep -q 'unifipoller_build_info{.\\+} 1'" + ) + ''; + }; + varnish = { exporterConfig = { enable = true; @@ -646,24 +662,27 @@ let }; }; in -mapAttrs (exporter: testConfig: (makeTest { +mapAttrs (exporter: testConfig: (makeTest (let + nodeName = testConfig.nodeName or exporter; + +in { name = "prometheus-${exporter}-exporter"; - nodes.${exporter} = mkMerge [{ + nodes.${nodeName} = mkMerge [{ services.prometheus.exporters.${exporter} = testConfig.exporterConfig; } testConfig.metricProvider or {}]; testScript = '' - ${exporter}.start() + ${nodeName}.start() ${concatStringsSep "\n" (map (line: if (builtins.substring 0 1 line == " " || builtins.substring 0 1 line == ")") then line - else "${exporter}.${line}" + else "${nodeName}.${line}" ) (splitString "\n" (removeSuffix "\n" testConfig.exporterTest)))} - ${exporter}.shutdown() + ${nodeName}.shutdown() ''; meta = with maintainers; { - maintainers = [ willibutz ]; + maintainers = [ willibutz elseym ]; }; -})) exporterTests +}))) exporterTests diff --git a/pkgs/servers/monitoring/unifi-poller/default.nix b/pkgs/servers/monitoring/unifi-poller/default.nix new file mode 100644 index 000000000000..7ddc53f38e8f --- /dev/null +++ b/pkgs/servers/monitoring/unifi-poller/default.nix @@ -0,0 +1,31 @@ +{ stdenv, buildGoModule, fetchFromGitHub }: + +buildGoModule rec { + pname = "unifi-poller"; + version = "2.0.1"; + + src = fetchFromGitHub { + owner = "unifi-poller"; + repo = "unifi-poller"; + rev = "v${version}"; + sha256 = "16q9hrbl9qgilj3vb7865l1yx0xhm7m4sx5j1ys5vi63drq59g93"; + }; + + vendorSha256 = "1fgcbg34g0a0f85qv7bjanv2lpnnszcrspfppp2lnj9kv52j4c1w"; + + buildFlagsArray = '' + -ldflags=-w -s + -X github.com/prometheus/common/version.Branch=master + -X github.com/prometheus/common/version.BuildDate=unknown + -X github.com/prometheus/common/version.Revision=${src.rev} + -X github.com/prometheus/common/version.Version=${version}-0 + ''; + + meta = with stdenv.lib; { + description = "Collect ALL UniFi Controller, Site, Device & Client Data - Export to InfluxDB or Prometheus"; + homepage = "https://github.com/unifi-poller/unifi-poller"; + license = licenses.mit; + maintainers = with maintainers; [ elseym ]; + platforms = platforms.unix; + }; +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index da2c58d2fbca..e191074c363c 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -27896,4 +27896,7 @@ in navidrome = callPackage ../servers/misc/navidrome {}; zettlr = callPackage ../applications/misc/zettlr { }; + + unifi-poller = callPackage ../servers/monitoring/unifi-poller {}; + }