diff options
Diffstat (limited to 'nixos/modules/services/monitoring')
28 files changed, 1395 insertions, 70 deletions
diff --git a/nixos/modules/services/monitoring/bosun.nix b/nixos/modules/services/monitoring/bosun.nix index dc75fda6ed8..1dc19743461 100644 --- a/nixos/modules/services/monitoring/bosun.nix +++ b/nixos/modules/services/monitoring/bosun.nix @@ -108,7 +108,7 @@ in { option. A detailed description of the supported syntax can be found at-spi2-atk - http://bosun.org/configuration.html + https://bosun.org/configuration.html ''; }; diff --git a/nixos/modules/services/monitoring/certspotter.md b/nixos/modules/services/monitoring/certspotter.md new file mode 100644 index 00000000000..9bf6e1d946a --- /dev/null +++ b/nixos/modules/services/monitoring/certspotter.md @@ -0,0 +1,74 @@ +# Cert Spotter {#module-services-certspotter} + +Cert Spotter is a tool for monitoring [Certificate Transparency](https://en.wikipedia.org/wiki/Certificate_Transparency) +logs. + +## Service Configuration {#modules-services-certspotter-service-configuration} + +A basic config that notifies you of all certificate changes for your +domain would look as follows: + +```nix +services.certspotter = { + enable = true; + # replace example.org with your domain name + watchlist = [ ".example.org" ]; + emailRecipients = [ "webmaster@example.org" ]; +}; + +# Configure an SMTP client +programs.msmtp.enable = true; +# Or you can use any other module that provides sendmail, like +# services.nullmailer, services.opensmtpd, services.postfix +``` + +In this case, the leading dot in `".example.org"` means that Cert +Spotter should monitor not only `example.org`, but also all of its +subdomains. + +## Operation {#modules-services-certspotter-operation} + +**By default, NixOS configures Cert Spotter to skip all certificates +issued before its first launch**, because checking the entire +Certificate Transparency logs requires downloading tens of terabytes of +data. If you want to check the *entire* logs for previously issued +certificates, you have to set `services.certspotter.startAtEnd` to +`false` and remove all previously saved log state in +`/var/lib/certspotter/logs`. The downloaded logs aren't saved, so if you +add a new domain to the watchlist and want Cert Spotter to go through +the logs again, you will have to remove `/var/lib/certspotter/logs` +again. + +After catching up with the logs, Cert Spotter will start monitoring live +logs. As of October 2023, it uses around **20 Mbps** of traffic on +average. + +## Hooks {#modules-services-certspotter-hooks} + +Cert Spotter supports running custom hooks instead of (or in addition +to) sending emails. Hooks are shell scripts that will be passed certain +environment variables. + +To see hook documentation, see Cert Spotter's man pages: + +```ShellSession +nix-shell -p certspotter --run 'man 8 certspotter-script' +``` + +For example, you can remove `emailRecipients` and send email +notifications manually using the following hook: + +```nix +services.certspotter.hooks = [ + (pkgs.writeShellScript "certspotter-hook" '' + function print_email() { + echo "Subject: [certspotter] $SUMMARY" + echo "Mime-Version: 1.0" + echo "Content-Type: text/plain; charset=US-ASCII" + echo + cat "$TEXT_FILENAME" + } + print_email | ${config.services.certspotter.sendmailPath} -i webmaster@example.org + '') +]; +``` diff --git a/nixos/modules/services/monitoring/certspotter.nix b/nixos/modules/services/monitoring/certspotter.nix new file mode 100644 index 00000000000..aafa29daa87 --- /dev/null +++ b/nixos/modules/services/monitoring/certspotter.nix @@ -0,0 +1,143 @@ +{ config +, lib +, pkgs +, ... }: + +let + cfg = config.services.certspotter; + + configDir = pkgs.linkFarm "certspotter-config" ( + lib.toList { + name = "watchlist"; + path = pkgs.writeText "certspotter-watchlist" (builtins.concatStringsSep "\n" cfg.watchlist); + } + ++ lib.optional (cfg.emailRecipients != [ ]) { + name = "email_recipients"; + path = pkgs.writeText "certspotter-email_recipients" (builtins.concatStringsSep "\n" cfg.emailRecipients); + } + # always generate hooks dir when no emails are provided to allow running cert spotter with no hooks/emails + ++ lib.optional (cfg.emailRecipients == [ ] || cfg.hooks != [ ]) { + name = "hooks.d"; + path = pkgs.linkFarm "certspotter-hooks" (lib.imap1 (i: path: { + inherit path; + name = "hook${toString i}"; + }) cfg.hooks); + }); +in +{ + options.services.certspotter = { + enable = lib.mkEnableOption "Cert Spotter, a Certificate Transparency log monitor"; + + package = lib.mkPackageOptionMD pkgs "certspotter" { }; + + startAtEnd = lib.mkOption { + type = lib.types.bool; + description = '' + Whether to skip certificates issued before the first launch of Cert Spotter. + Setting this to `false` will cause Cert Spotter to download tens of terabytes of data. + ''; + default = true; + }; + + sendmailPath = lib.mkOption { + type = with lib.types; nullOr path; + description = '' + Path to the `sendmail` binary. By default, the local sendmail wrapper is used + (see {option}`services.mail.sendmailSetuidWrapper`}). + ''; + example = lib.literalExpression ''"''${pkgs.system-sendmail}/bin/sendmail"''; + }; + + watchlist = lib.mkOption { + type = with lib.types; listOf str; + description = "Domain names to watch. To monitor a domain with all subdomains, prefix its name with `.` (e.g. `.example.org`)."; + default = [ ]; + example = [ ".example.org" "another.example.com" ]; + }; + + emailRecipients = lib.mkOption { + type = with lib.types; listOf str; + description = "A list of email addresses to send certificate updates to."; + default = [ ]; + }; + + hooks = lib.mkOption { + type = with lib.types; listOf path; + description = '' + Scripts to run upon the detection of a new certificate. See `man 8 certspotter-script` or + [the GitHub page](https://github.com/SSLMate/certspotter/blob/${pkgs.certspotter.src.rev or "master"}/man/certspotter-script.md) + for more info. + ''; + default = [ ]; + example = lib.literalExpression '' + [ + (pkgs.writeShellScript "certspotter-hook" ''' + echo "Event summary: $SUMMARY." + ''') + ] + ''; + }; + + extraFlags = lib.mkOption { + type = with lib.types; listOf str; + description = "Extra command-line arguments to pass to Cert Spotter"; + example = [ "-no_save" ]; + default = [ ]; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = (cfg.emailRecipients != [ ]) -> (cfg.sendmailPath != null); + message = '' + You must configure the sendmail setuid wrapper (services.mail.sendmailSetuidWrapper) + or services.certspotter.sendmailPath + ''; + } + ]; + + services.certspotter.sendmailPath = let + inherit (config.security) wrapperDir; + inherit (config.services.mail) sendmailSetuidWrapper; + in lib.mkMerge [ + (lib.mkIf (sendmailSetuidWrapper != null) (lib.mkOptionDefault "${wrapperDir}/${sendmailSetuidWrapper.program}")) + (lib.mkIf (sendmailSetuidWrapper == null) (lib.mkOptionDefault null)) + ]; + + users.users.certspotter = { + description = "Cert Spotter user"; + group = "certspotter"; + home = "/var/lib/certspotter"; + isSystemUser = true; + }; + users.groups.certspotter = { }; + + systemd.services.certspotter = { + description = "Cert Spotter - Certificate Transparency Monitor"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment.CERTSPOTTER_CONFIG_DIR = configDir; + environment.SENDMAIL_PATH = if cfg.sendmailPath != null then cfg.sendmailPath else "/run/current-system/sw/bin/false"; + script = '' + export CERTSPOTTER_STATE_DIR="$STATE_DIRECTORY" + cd "$CERTSPOTTER_STATE_DIR" + ${lib.optionalString cfg.startAtEnd '' + if [[ ! -d logs ]]; then + # Don't download certificates issued before the first launch + exec ${cfg.package}/bin/certspotter -start_at_end ${lib.escapeShellArgs cfg.extraFlags} + fi + ''} + exec ${cfg.package}/bin/certspotter ${lib.escapeShellArgs cfg.extraFlags} + ''; + serviceConfig = { + User = "certspotter"; + Group = "certspotter"; + StateDirectory = "certspotter"; + }; + }; + }; + + meta.maintainers = with lib.maintainers; [ chayleaf ]; + meta.doc = ./certspotter.md; +} diff --git a/nixos/modules/services/monitoring/goss.md b/nixos/modules/services/monitoring/goss.md new file mode 100644 index 00000000000..1e636aa3bdf --- /dev/null +++ b/nixos/modules/services/monitoring/goss.md @@ -0,0 +1,44 @@ +# Goss {#module-services-goss} + +[goss](https://goss.rocks/) is a YAML based serverspec alternative tool +for validating a server's configuration. + +## Basic Usage {#module-services-goss-basic-usage} + +A minimal configuration looks like this: + +``` +{ + services.goss = { + enable = true; + + environment = { + GOSS_FMT = "json"; + GOSS_LOGLEVEL = "TRACE"; + }; + + settings = { + addr."tcp://localhost:8080" = { + reachable = true; + local-address = "127.0.0.1"; + }; + command."check-goss-version" = { + exec = "${lib.getExe pkgs.goss} --version"; + exit-status = 0; + }; + dns.localhost.resolvable = true; + file."/nix" = { + filetype = "directory"; + exists = true; + }; + group.root.exists = true; + kernel-param."kernel.ostype".value = "Linux"; + service.goss = { + enabled = true; + running = true; + }; + user.root.exists = true; + }; + }; +} +``` diff --git a/nixos/modules/services/monitoring/goss.nix b/nixos/modules/services/monitoring/goss.nix new file mode 100644 index 00000000000..64a8dad0703 --- /dev/null +++ b/nixos/modules/services/monitoring/goss.nix @@ -0,0 +1,86 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.goss; + + settingsFormat = pkgs.formats.yaml { }; + configFile = settingsFormat.generate "goss.yaml" cfg.settings; + +in { + meta = { + doc = ./goss.md; + maintainers = [ lib.maintainers.anthonyroussel ]; + }; + + options = { + services.goss = { + enable = lib.mkEnableOption (lib.mdDoc "Goss daemon"); + + package = lib.mkPackageOptionMD pkgs "goss" { }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + example = { + GOSS_FMT = "json"; + GOSS_LOGLEVEL = "FATAL"; + GOSS_LISTEN = ":8080"; + }; + description = lib.mdDoc '' + Environment variables to set for the goss service. + + See <https://github.com/goss-org/goss/blob/master/docs/manual.md> + ''; + }; + + settings = lib.mkOption { + type = lib.types.submodule { freeformType = settingsFormat.type; }; + default = { }; + example = { + addr."tcp://localhost:8080" = { + reachable = true; + local-address = "127.0.0.1"; + }; + service.goss = { + enabled = true; + running = true; + }; + }; + description = lib.mdDoc '' + The global options in `config` file in yaml format. + + Refer to <https://github.com/goss-org/goss/blob/master/docs/goss-json-schema.yaml> for schema. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; + + systemd.services.goss = { + description = "Goss - Quick and Easy server validation"; + unitConfig.Documentation = "https://github.com/goss-org/goss/blob/master/docs/manual.md"; + + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + + environment = { + GOSS_FILE = configFile; + } // cfg.environment; + + reloadTriggers = [ configFile ]; + + serviceConfig = { + DynamicUser = true; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + ExecStart = "${cfg.package}/bin/goss serve"; + Group = "goss"; + Restart = "on-failure"; + RestartSec = 5; + User = "goss"; + }; + }; + }; +} diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix index 36258866646..afe9eb4d7b9 100644 --- a/nixos/modules/services/monitoring/grafana-image-renderer.nix +++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix @@ -108,7 +108,7 @@ in { services.grafana.settings.rendering = mkIf cfg.provisionGrafana { server_url = "http://localhost:${toString cfg.settings.service.port}/render"; - callback_url = "http://localhost:${toString config.services.grafana.settings.server.http_port}"; + callback_url = "http://${config.services.grafana.settings.server.http_addr}:${toString config.services.grafana.settings.server.http_port}"; }; services.grafana-image-renderer.chromium = mkDefault pkgs.chromium; diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix index e90a0e9d16d..f84d677f14d 100644 --- a/nixos/modules/services/monitoring/grafana.nix +++ b/nixos/modules/services/monitoring/grafana.nix @@ -88,7 +88,7 @@ let # Get a submodule without any embedded metadata: _filter = x: filterAttrs (k: v: k != "_module") x; - # http://docs.grafana.org/administration/provisioning/#datasources + # https://grafana.com/docs/grafana/latest/administration/provisioning/#datasources grafanaTypes.datasourceConfig = types.submodule { freeformType = provisioningSettingsFormat.type; @@ -140,7 +140,7 @@ let }; }; - # http://docs.grafana.org/administration/provisioning/#dashboards + # https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards grafanaTypes.dashboardConfig = types.submodule { freeformType = provisioningSettingsFormat.type; @@ -1841,6 +1841,7 @@ in serviceConfig = { WorkingDirectory = cfg.dataDir; User = "grafana"; + Restart = "on-failure"; RuntimeDirectory = "grafana"; RuntimeDirectoryMode = "0755"; # Hardening diff --git a/nixos/modules/services/monitoring/graphite.nix b/nixos/modules/services/monitoring/graphite.nix index 65c91b8f79b..cc3d7097620 100644 --- a/nixos/modules/services/monitoring/graphite.nix +++ b/nixos/modules/services/monitoring/graphite.nix @@ -102,7 +102,7 @@ in { default = ""; description = lib.mdDoc '' Graphite webapp settings. See: - <http://graphite.readthedocs.io/en/latest/config-local-settings.html> + <https://graphite.readthedocs.io/en/latest/config-local-settings.html> ''; }; }; diff --git a/nixos/modules/services/monitoring/librenms.nix b/nixos/modules/services/monitoring/librenms.nix new file mode 100644 index 00000000000..08a46754e0e --- /dev/null +++ b/nixos/modules/services/monitoring/librenms.nix @@ -0,0 +1,624 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.librenms; + settingsFormat = pkgs.formats.json {}; + configJson = settingsFormat.generate "librenms-config.json" cfg.settings; + + package = pkgs.librenms.override { + logDir = cfg.logDir; + dataDir = cfg.dataDir; + }; + + phpOptions = '' + log_errors = on + post_max_size = 100M + upload_max_filesize = 100M + date.timezone = "${config.time.timeZone}" + ''; + phpIni = pkgs.runCommand "php.ini" { + inherit (package) phpPackage; + inherit phpOptions; + preferLocalBuild = true; + passAsFile = [ "phpOptions" ]; + } '' + cat $phpPackage/etc/php.ini $phpOptionsPath > $out + ''; + + artisanWrapper = pkgs.writeShellScriptBin "librenms-artisan" '' + cd ${package} + sudo=exec + if [[ "$USER" != ${cfg.user} ]]; then + sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}' + fi + $sudo ${package}/artisan $* + ''; + + lnmsWrapper = pkgs.writeShellScriptBin "lnms" '' + cd ${package} + exec ${package}/lnms $* + ''; + + configFile = pkgs.writeText "config.php" '' + <?php + $new_config = json_decode(file_get_contents("${cfg.dataDir}/config.json"), true); + $config = ($config == null) ? $new_config : array_merge($config, $new_config); + + ${lib.optionalString (cfg.extraConfig != null) cfg.extraConfig} + ''; + +in { + options.services.librenms = with lib; { + enable = mkEnableOption "LibreNMS network monitoring system"; + + user = mkOption { + type = types.str; + default = "librenms"; + description = '' + Name of the LibreNMS user. + ''; + }; + + group = mkOption { + type = types.str; + default = "librenms"; + description = '' + Name of the LibreNMS group. + ''; + }; + + hostname = mkOption { + type = types.str; + default = config.networking.fqdnOrHostName; + defaultText = literalExpression "config.networking.fqdnOrHostName"; + description = '' + The hostname to serve LibreNMS on. + ''; + }; + + pollerThreads = mkOption { + type = types.int; + default = 16; + description = '' + Amount of threads of the cron-poller. + ''; + }; + + enableOneMinutePolling = mkOption { + type = types.bool; + default = false; + description = '' + Enables the [1-Minute Polling](https://docs.librenms.org/Support/1-Minute-Polling/). + Changing this option will automatically convert your existing rrd files. + ''; + }; + + useDistributedPollers = mkOption { + type = types.bool; + default = false; + description = '' + Enables (distributed pollers)[https://docs.librenms.org/Extensions/Distributed-Poller/] + for this LibreNMS instance. This will enable a local `rrdcached` and `memcached` server. + + To use this feature, make sure to configure your firewall that the distributed pollers + can reach the local `mysql`, `rrdcached` and `memcached` ports. + ''; + }; + + distributedPoller = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Configure this LibreNMS instance as a (distributed poller)[https://docs.librenms.org/Extensions/Distributed-Poller/]. + This will disable all web features and just configure the poller features. + Use the `mysql` database of your main LibreNMS instance in the database settings. + ''; + }; + + name = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Custom name of this poller. + ''; + }; + + group = mkOption { + type = types.str; + default = "0"; + example = "1,2"; + description = '' + Group(s) of this poller. + ''; + }; + + distributedBilling = mkOption { + type = types.bool; + default = false; + description = '' + Enable distributed billing on this poller. + ''; + }; + + memcachedHost = mkOption { + type = types.str; + description = '' + Hostname or IP of the `memcached` server. + ''; + }; + + memcachedPort = mkOption { + type = types.port; + default = 11211; + description = '' + Port of the `memcached` server. + ''; + }; + + rrdcachedHost = mkOption { + type = types.str; + description = '' + Hostname or IP of the `rrdcached` server. + ''; + }; + + rrdcachedPort = mkOption { + type = types.port; + default = 42217; + description = '' + Port of the `memcached` server. + ''; + }; + }; + + poolConfig = mkOption { + type = with types; attrsOf (oneOf [ str int bool ]); + default = { + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 4; + "pm.max_requests" = 500; + }; + description = '' + Options for the LibreNMS PHP pool. See the documentation on `php-fpm.conf` + for details on configuration directives. + ''; + }; + + nginx = mkOption { + type = types.submodule ( + recursiveUpdate + (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {} + ); + default = { }; + example = literalExpression '' + { + serverAliases = [ + "librenms.''${config.networking.domain}" + ]; + # To enable encryption and let let's encrypt take care of certificate + forceSSL = true; + enableACME = true; + # To set the LibreNMS virtualHost as the default virtualHost; + default = true; + } + ''; + description = '' + With this option, you can customize the nginx virtualHost settings. + ''; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/librenms"; + description = '' + Path of the LibreNMS state directory. + ''; + }; + + logDir = mkOption { + type = types.path; + default = "/var/log/librenms"; + description = '' + Path of the LibreNMS logging directory. + ''; + }; + + database = { + createLocally = mkOption { + type = types.bool; + default = false; + description = '' + Whether to create a local database automatically. + ''; + }; + + host = mkOption { + default = "localhost"; + description = '' + Hostname or IP of the MySQL/MariaDB server. + ''; + }; + + port = mkOption { + type = types.port; + default = 3306; + description = '' + Port of the MySQL/MariaDB server. + ''; + }; + + database = mkOption { + type = types.str; + default = "librenms"; + description = '' + Name of the database on the MySQL/MariaDB server. + ''; + }; + + username = mkOption { + type = types.str; + default = "librenms"; + description = '' + Name of the user on the MySQL/MariaDB server. + ''; + }; + + passwordFile = mkOption { + type = types.path; + example = "/run/secrets/mysql.pass"; + description = '' + A file containing the password for the user of the MySQL/MariaDB server. + Must be readable for the LibreNMS user. + ''; + }; + }; + + environmentFile = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + File containing env-vars to be substituted into the final config. Useful for secrets. + Does not apply to settings defined in `extraConfig`. + ''; + }; + + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = {}; + }; + description = '' + Attrset of the LibreNMS configuration. + See https://docs.librenms.org/Support/Configuration/ for reference. + All possible options are listed [here](https://github.com/librenms/librenms/blob/master/misc/config_definitions.json). + See https://docs.librenms.org/Extensions/Authentication/ for setting other authentication methods. + ''; + default = { }; + example = { + base_url = "/librenms/"; + top_devices = true; + top_ports = false; + }; + }; + + extraConfig = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Additional config for LibreNMS that will be appended to the `config.php`. See + https://github.com/librenms/librenms/blob/master/misc/config_definitions.json + for possible options. Useful if you want to use PHP-Functions in your config. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = config.time.timeZone != null; + message = "You must set `time.timeZone` to use the LibreNMS module."; + } + { + assertion = cfg.database.createLocally -> cfg.database.host == "localhost"; + message = "The database host must be \"localhost\" if services.librenms.database.createLocally is set to true."; + } + { + assertion = !(cfg.useDistributedPollers && cfg.distributedPoller.enable); + message = "The LibreNMS instance can't be a distributed poller and a full instance at the same time."; + } + ]; + + users.users.${cfg.user} = { + group = "${cfg.group}"; + isSystemUser = true; + }; + + users.groups.${cfg.group} = { }; + + services.librenms.settings = { + # basic configs + "user" = cfg.user; + "own_hostname" = cfg.hostname; + "base_url" = lib.mkDefault "/"; + "auth_mechanism" = lib.mkDefault "mysql"; + + # disable auto update function (won't work with NixOS) + "update" = false; + + # enable fast ping by default + "ping_rrd_step" = 60; + + # one minute polling + "rrd.step" = if cfg.enableOneMinutePolling then 60 else 300; + "rrd.heartbeat" = if cfg.enableOneMinutePolling then 120 else 600; + } // (lib.optionalAttrs cfg.distributedPoller.enable { + "distributed_poller" = true; + "distributed_poller_name" = lib.mkIf (cfg.distributedPoller.name != null) cfg.distributedPoller.name; + "distributed_poller_group" = cfg.distributedPoller.group; + "distributed_billing" = cfg.distributedPoller.distributedBilling; + "distributed_poller_memcached_host" = cfg.distributedPoller.memcachedHost; + "distributed_poller_memcached_port" = cfg.distributedPoller.memcachedPort; + "rrdcached" = "${cfg.distributedPoller.rrdcachedHost}:${toString cfg.distributedPoller.rrdcachedPort}"; + }) // (lib.optionalAttrs cfg.useDistributedPollers { + "distributed_poller" = true; + # still enable a local poller with distributed polling + "distributed_poller_group" = lib.mkDefault "0"; + "distributed_billing" = lib.mkDefault true; + "distributed_poller_memcached_host" = "localhost"; + "distributed_poller_memcached_port" = 11211; + "rrdcached" = "localhost:42217"; + }); + + services.memcached = lib.mkIf cfg.useDistributedPollers { + enable = true; + listen = "0.0.0.0"; + }; + + systemd.services.rrdcached = lib.mkIf cfg.useDistributedPollers { + description = "rrdcached"; + after = [ "librenms-setup.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "forking"; + User = cfg.user; + Group = cfg.group; + LimitNOFILE = 16384; + RuntimeDirectory = "rrdcached"; + PidFile = "/run/rrdcached/rrdcached.pid"; + # rrdcached params from https://docs.librenms.org/Extensions/Distributed-Poller/#config-sample + ExecStart = "${pkgs.rrdtool}/bin/rrdcached -l 0:42217 -R -j ${cfg.dataDir}/rrdcached-journal/ -F -b ${cfg.dataDir}/rrd -B -w 1800 -z 900 -p /run/rrdcached/rrdcached.pid"; + }; + }; + + services.mysql = lib.mkIf cfg.database.createLocally { + enable = true; + package = lib.mkDefault pkgs.mariadb; + settings.mysqld = { + innodb_file_per_table = 1; + lower_case_table_names = 0; + } // (lib.optionalAttrs cfg.useDistributedPollers { + bind-address = "0.0.0.0"; + }); + ensureDatabases = [ cfg.database.database ]; + ensureUsers = [ + { + name = cfg.database.username; + ensurePermissions = { + "${cfg.database.database}.*" = "ALL PRIVILEGES"; + }; + } + ]; + initialScript = lib.mkIf cfg.useDistributedPollers (pkgs.writeText "mysql-librenms-init" '' + CREATE USER IF NOT EXISTS '${cfg.database.username}'@'%'; + GRANT ALL PRIVILEGES ON ${cfg.database.database}.* TO '${cfg.database.username}'@'%'; + ''); + }; + + services.nginx = lib.mkIf (!cfg.distributedPoller.enable) { + enable = true; + virtualHosts."${cfg.hostname}" = lib.mkMerge [ + cfg.nginx + { + root = lib.mkForce "${package}/html"; + locations."/" = { + index = "index.php"; + tryFiles = "$uri $uri/ /index.php?$query_string"; + }; + locations."~ .php$".extraConfig = '' + fastcgi_pass unix:${config.services.phpfpm.pools."librenms".socket}; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + ''; + } + ]; + }; + + services.phpfpm.pools.librenms = lib.mkIf (!cfg.distributedPoller.enable) { + user = cfg.user; + group = cfg.group; + inherit (package) phpPackage; + inherit phpOptions; + settings = { + "listen.mode" = "0660"; + "listen.owner" = config.services.nginx.user; + "listen.group" = config.services.nginx.group; + } // cfg.poolConfig; + }; + + systemd.services.librenms-scheduler = { + description = "LibreNMS Scheduler"; + path = [ pkgs.unixtools.whereis ]; + serviceConfig = { + Type = "oneshot"; + WorkingDirectory = package; + User = cfg.user; + Group = cfg.group; + ExecStart = "${artisanWrapper}/bin/librenms-artisan schedule:run"; + }; + }; + + systemd.timers.librenms-scheduler = { + description = "LibreNMS Scheduler"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "minutely"; + AccuracySec = "1second"; + }; + }; + + systemd.services.librenms-setup = { + description = "Preparation tasks for LibreNMS"; + before = [ "phpfpm-librenms.service" ]; + after = [ "systemd-tmpfiles-setup.service" ] + ++ (lib.optional (cfg.database.host == "localhost") "mysql.service"); + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ package configFile ]; + path = [ pkgs.mariadb pkgs.unixtools.whereis pkgs.gnused ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; + User = cfg.user; + Group = cfg.group; + ExecStartPre = lib.mkIf cfg.database.createLocally [ "!${pkgs.writeShellScript "librenms-db-init" '' + DB_PASSWORD=$(cat ${cfg.database.passwordFile} | tr -d '\n') + echo "ALTER USER '${cfg.database.username}'@'localhost' IDENTIFIED BY '$DB_PASSWORD';" | ${pkgs.mariadb}/bin/mysql + ${lib.optionalString cfg.useDistributedPollers '' + echo "ALTER USER '${cfg.database.username}'@'%' IDENTIFIED BY '$DB_PASSWORD';" | ${pkgs.mariadb}/bin/mysql + ''} + ''}"]; + }; + script = '' + set -euo pipefail + + # config setup + ln -sf ${configFile} ${cfg.dataDir}/config.php + ${pkgs.envsubst}/bin/envsubst -i ${configJson} -o ${cfg.dataDir}/config.json + export PHPRC=${phpIni} + + if [[ ! -s ${cfg.dataDir}/.env ]]; then + # init .env file + echo "APP_KEY=" > ${cfg.dataDir}/.env + ${artisanWrapper}/bin/librenms-artisan key:generate --ansi + ${artisanWrapper}/bin/librenms-artisan webpush:vapid + echo "" >> ${cfg.dataDir}/.env + echo -n "NODE_ID=" >> ${cfg.dataDir}/.env + ${package.phpPackage}/bin/php -r "echo uniqid();" >> ${cfg.dataDir}/.env + echo "" >> ${cfg.dataDir}/.env + else + # .env file already exists --> only update database and cache config + ${pkgs.gnused}/bin/sed -i /^DB_/d ${cfg.dataDir}/.env + ${pkgs.gnused}/bin/sed -i /^CACHE_DRIVER/d ${cfg.dataDir}/.env + fi + ${lib.optionalString (cfg.useDistributedPollers || cfg.distributedPoller.enable) '' + echo "CACHE_DRIVER=memcached" >> ${cfg.dataDir}/.env + ''} + echo "DB_HOST=${cfg.database.host}" >> ${cfg.dataDir}/.env + echo "DB_PORT=${toString cfg.database.port}" >> ${cfg.dataDir}/.env + echo "DB_DATABASE=${cfg.database.database}" >> ${cfg.dataDir}/.env + echo "DB_USERNAME=${cfg.database.username}" >> ${cfg.dataDir}/.env + echo -n "DB_PASSWORD=" >> ${cfg.dataDir}/.env + cat ${cfg.database.passwordFile} >> ${cfg.dataDir}/.env + + # clear cache after update + OLD_VERSION=$(cat ${cfg.dataDir}/version) + if [[ $OLD_VERSION != "${package.version}" ]]; then + rm -r ${cfg.dataDir}/cache/* + echo "${package.version}" > ${cfg.dataDir}/version + fi + + # convert rrd files when the oneMinutePolling option is changed + OLD_ENABLED=$(cat ${cfg.dataDir}/one_minute_enabled) + if [[ $OLD_ENABLED != "${lib.boolToString cfg.enableOneMinutePolling}" ]]; then + ${package}/scripts/rrdstep.php -h all + echo "${lib.boolToString cfg.enableOneMinutePolling}" > ${cfg.dataDir}/one_minute_enabled + fi + + # migrate db + ${artisanWrapper}/bin/librenms-artisan migrate --force --no-interaction + ''; + }; + + programs.mtr.enable = true; + + services.logrotate = { + enable = true; + settings."${cfg.logDir}/librenms.log" = { + su = "${cfg.user} ${cfg.group}"; + create = "0640 ${cfg.user} ${cfg.group}"; + rotate = 6; + frequency = "weekly"; + compress = true; + delaycompress = true; + missingok = true; + notifempty = true; + }; + }; + + services.cron = { + enable = true; + systemCronJobs = let + env = "PHPRC=${phpIni}"; + in [ + # based on crontab provided by LibreNMS + "33 */6 * * * ${cfg.user} ${env} ${package}/cronic ${package}/discovery-wrapper.py 1" + "*/5 * * * * ${cfg.user} ${env} ${package}/discovery.php -h new >> /dev/null 2>&1" + + "${if cfg.enableOneMinutePolling then "*" else "*/5"} * * * * ${cfg.user} ${env} ${package}/cronic ${package}/poller-wrapper.py ${toString cfg.pollerThreads}" + "* * * * * ${cfg.user} ${env} ${package}/alerts.php >> /dev/null 2>&1" + + "*/5 * * * * ${cfg.user} ${env} ${package}/poll-billing.php >> /dev/null 2>&1" + "01 * * * * ${cfg.user} ${env} ${package}/billing-calculate.php >> /dev/null 2>&1" + "*/5 * * * * ${cfg.user} ${env} ${package}/check-services.php >> /dev/null 2>&1" + + # extra: fast ping + "* * * * * ${cfg.user} ${env} ${package}/ping.php >> /dev/null 2>&1" + + # daily.sh tasks are split to exclude update + "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh cleanup >> /dev/null 2>&1" + "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh notifications >> /dev/null 2>&1" + "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh peeringdb >> /dev/null 2>&1" + "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh mac_oui >> /dev/null 2>&1" + ]; + }; + + security.wrappers = { + fping = { + setuid = true; + owner = "root"; + group = "root"; + source = "${pkgs.fping}/bin/fping"; + }; + }; + + environment.systemPackages = [ artisanWrapper lnmsWrapper ]; + + systemd.tmpfiles.rules = [ + "d ${cfg.logDir} 0750 ${cfg.user} ${cfg.group} - -" + "f ${cfg.logDir}/librenms.log 0640 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -" + "f ${cfg.dataDir}/.env 0600 ${cfg.user} ${cfg.group} - -" + "f ${cfg.dataDir}/version 0600 ${cfg.user} ${cfg.group} - -" + "f ${cfg.dataDir}/one_minute_enabled 0600 ${cfg.user} ${cfg.group} - -" + "f ${cfg.dataDir}/config.json 0600 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/app 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/debugbar 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/framework 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/framework/cache 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/framework/sessions 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/framework/views 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/logs 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/rrd 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/cache 0700 ${cfg.user} ${cfg.group} - -" + ] ++ lib.optionals cfg.useDistributedPollers [ + "d ${cfg.dataDir}/rrdcached-journal 0700 ${cfg.user} ${cfg.group} - -" + ]; + + }; + + meta.maintainers = lib.teams.wdz.members; +} diff --git a/nixos/modules/services/monitoring/mackerel-agent.nix b/nixos/modules/services/monitoring/mackerel-agent.nix index 67dc1bc19ed..62a7858500f 100644 --- a/nixos/modules/services/monitoring/mackerel-agent.nix +++ b/nixos/modules/services/monitoring/mackerel-agent.nix @@ -11,10 +11,10 @@ in { # the upstream package runs as root, but doesn't seem to be strictly # necessary for basic functionality - runAsRoot = mkEnableOption (lib.mdDoc "Whether to run as root"); + runAsRoot = mkEnableOption (lib.mdDoc "running as root"); autoRetirement = mkEnableOption (lib.mdDoc '' - Whether to automatically retire the host upon OS shutdown. + retiring the host upon OS shutdown ''); apiKeyFile = mkOption { @@ -59,7 +59,7 @@ in { }; options.diagnostic = - mkEnableOption (lib.mdDoc "Collect memory usage for the agent itself"); + mkEnableOption (lib.mdDoc "collecting memory usage for the agent itself"); }; }; }; diff --git a/nixos/modules/services/monitoring/munin.nix b/nixos/modules/services/monitoring/munin.nix index f37f2689927..5ed7cac48ae 100644 --- a/nixos/modules/services/monitoring/munin.nix +++ b/nixos/modules/services/monitoring/munin.nix @@ -4,7 +4,7 @@ # TODO: LWP/Pg perl libs aren't recognized # TODO: support fastcgi -# http://guide.munin-monitoring.org/en/latest/example/webserver/apache-cgi.html +# https://guide.munin-monitoring.org/en/latest/example/webserver/apache-cgi.html # spawn-fcgi -s /run/munin/fastcgi-graph.sock -U www-data -u munin -g munin /usr/lib/munin/cgi/munin-cgi-graph # spawn-fcgi -s /run/munin/fastcgi-html.sock -U www-data -u munin -g munin /usr/lib/munin/cgi/munin-cgi-html # https://paste.sh/vofcctHP#-KbDSXVeWoifYncZmLfZzgum @@ -83,42 +83,47 @@ let # Copy one Munin plugin into the Nix store with a specific name. # This is suitable for use with plugins going directly into /etc/munin/plugins, # i.e. munin.extraPlugins. - internOnePlugin = name: path: + internOnePlugin = { name, path }: "cp -a '${path}' '${name}'"; # Copy an entire tree of Munin plugins into a single directory in the Nix - # store, with no renaming. - # This is suitable for use with munin-node-configure --suggest, i.e. - # munin.extraAutoPlugins. - internManyPlugins = name: path: + # store, with no renaming. The output is suitable for use with + # munin-node-configure --suggest, i.e. munin.extraAutoPlugins. + # Note that this flattens the input; this is intentional, as + # munin-node-configure won't recurse into subdirectories. + internManyPlugins = path: "find '${path}' -type f -perm /a+x -exec cp -a -t . '{}' '+'"; # Use the appropriate intern-fn to copy the plugins into the store and patch # them afterwards in an attempt to get them to run on NixOS. + # This is a bit hairy because we can't just fix shebangs; lots of munin plugins + # hardcode paths like /sbin/mount rather than trusting $PATH, so we have to + # look for and update those throughout the script. At the same time, if the + # plugin comes from a package that is already nixified, we don't want to + # rewrite paths like /nix/store/foo/sbin/mount. + # For now we make the simplifying assumption that no file will contain lines + # which mix store paths and FHS paths, and thus run our substitution only on + # lines which do not contain store paths. internAndFixPlugins = name: intern-fn: paths: pkgs.runCommand name {} '' mkdir -p "$out" cd "$out" - ${lib.concatStringsSep "\n" - (lib.attrsets.mapAttrsToList intern-fn paths)} + ${lib.concatStringsSep "\n" (map intern-fn paths)} chmod -R u+w . - find . -type f -exec sed -E -i ' - s,(/usr)?/s?bin/,/run/current-system/sw/bin/,g - ' '{}' '+' + ${pkgs.findutils}/bin/find . -type f -exec ${pkgs.gnused}/bin/sed -E -i " + \%''${NIX_STORE}/%! s,(/usr)?/s?bin/,/run/current-system/sw/bin/,g + " '{}' '+' ''; # TODO: write a derivation for munin-contrib, so that for contrib plugins # you can just refer to them by name rather than needing to include a copy # of munin-contrib in your nixos configuration. extraPluginDir = internAndFixPlugins "munin-extra-plugins.d" - internOnePlugin nodeCfg.extraPlugins; + internOnePlugin + (lib.attrsets.mapAttrsToList (k: v: { name = k; path = v; }) nodeCfg.extraPlugins); extraAutoPluginDir = internAndFixPlugins "munin-extra-auto-plugins.d" - internManyPlugins - (builtins.listToAttrs - (map - (path: { name = baseNameOf path; value = path; }) - nodeCfg.extraAutoPlugins)); + internManyPlugins nodeCfg.extraAutoPlugins; customStaticDir = pkgs.runCommand "munin-custom-static-data" {} '' cp -a "${pkgs.munin}/etc/opt/munin/static" "$out" @@ -142,7 +147,7 @@ in Enable Munin Node agent. Munin node listens on 0.0.0.0 and by default accepts connections only from 127.0.0.1 for security reasons. - See <http://guide.munin-monitoring.org/en/latest/architecture/index.html>. + See <https://guide.munin-monitoring.org/en/latest/architecture/index.html>. ''; }; @@ -151,7 +156,7 @@ in type = types.lines; description = lib.mdDoc '' {file}`munin-node.conf` extra configuration. See - <http://guide.munin-monitoring.org/en/latest/reference/munin-node.conf.html> + <https://guide.munin-monitoring.org/en/latest/reference/munin-node.conf.html> ''; }; @@ -160,7 +165,7 @@ in type = types.lines; description = lib.mdDoc '' {file}`plugin-conf.d` extra plugin configuration. See - <http://guide.munin-monitoring.org/en/latest/plugin/use.html> + <https://guide.munin-monitoring.org/en/latest/plugin/use.html> ''; example = '' [fail2ban_*] @@ -268,9 +273,9 @@ in type = types.lines; description = lib.mdDoc '' {file}`munin.conf` extra global configuration. - See <http://guide.munin-monitoring.org/en/latest/reference/munin.conf.html>. + See <https://guide.munin-monitoring.org/en/latest/reference/munin.conf.html>. Useful to setup notifications, see - <http://guide.munin-monitoring.org/en/latest/tutorial/alert.html> + <https://guide.munin-monitoring.org/en/latest/tutorial/alert.html> ''; example = '' contact.email.command mail -s "Munin notification for ''${var:host}" someone@example.com @@ -283,7 +288,7 @@ in description = lib.mdDoc '' Definitions of hosts of nodes to collect data from. Needs at least one host for cron to succeed. See - <http://guide.munin-monitoring.org/en/latest/reference/munin.conf.html> + <https://guide.munin-monitoring.org/en/latest/reference/munin.conf.html> ''; example = literalExpression '' ''' diff --git a/nixos/modules/services/monitoring/nagios.nix b/nixos/modules/services/monitoring/nagios.nix index 8feff22c118..dc5fa1be292 100644 --- a/nixos/modules/services/monitoring/nagios.nix +++ b/nixos/modules/services/monitoring/nagios.nix @@ -88,7 +88,7 @@ in options = { services.nagios = { - enable = mkEnableOption (lib.mdDoc ''[Nagios](http://www.nagios.org/) to monitor your system or network.''); + enable = mkEnableOption (lib.mdDoc ''[Nagios](https://www.nagios.org/) to monitor your system or network.''); objectDefs = mkOption { description = lib.mdDoc '' diff --git a/nixos/modules/services/monitoring/netdata.nix b/nixos/modules/services/monitoring/netdata.nix index 3833418b5ad..de0e044453e 100644 --- a/nixos/modules/services/monitoring/netdata.nix +++ b/nixos/modules/services/monitoring/netdata.nix @@ -12,6 +12,7 @@ let ln -s /run/wrappers/bin/perf.plugin $out/libexec/netdata/plugins.d/perf.plugin ln -s /run/wrappers/bin/slabinfo.plugin $out/libexec/netdata/plugins.d/slabinfo.plugin ln -s /run/wrappers/bin/freeipmi.plugin $out/libexec/netdata/plugins.d/freeipmi.plugin + ln -s /run/wrappers/bin/systemd-journal.plugin $out/libexec/netdata/plugins.d/systemd-journal.plugin ''; plugins = [ @@ -254,7 +255,7 @@ in { # Capabilities CapabilityBoundingSet = [ "CAP_DAC_OVERRIDE" # is required for freeipmi and slabinfo plugins - "CAP_DAC_READ_SEARCH" # is required for apps plugin + "CAP_DAC_READ_SEARCH" # is required for apps and systemd-journal plugin "CAP_FOWNER" # is required for freeipmi plugin "CAP_SETPCAP" # is required for apps, perf and slabinfo plugins "CAP_SYS_ADMIN" # is required for perf plugin @@ -263,6 +264,7 @@ in { "CAP_NET_RAW" # is required for fping app "CAP_SYS_CHROOT" # is required for cgroups plugin "CAP_SETUID" # is required for cgroups and cgroups-network plugins + "CAP_SYSLOG" # is required for systemd-journal plugin ]; # Sandboxing ProtectSystem = "full"; @@ -318,6 +320,14 @@ in { permissions = "u+rx,g+x,o-rwx"; }; + "systemd-journal.plugin" = { + source = "${cfg.package}/libexec/netdata/plugins.d/systemd-journal.plugin.org"; + capabilities = "cap_dac_read_search,cap_syslog+ep"; + owner = cfg.user; + group = cfg.group; + permissions = "u+rx,g+x,o-rwx"; + }; + "slabinfo.plugin" = { source = "${cfg.package}/libexec/netdata/plugins.d/slabinfo.plugin.org"; capabilities = "cap_dac_override+ep"; diff --git a/nixos/modules/services/monitoring/parsedmarc.nix b/nixos/modules/services/monitoring/parsedmarc.nix index 44fc359b6a7..a146e7ab954 100644 --- a/nixos/modules/services/monitoring/parsedmarc.nix +++ b/nixos/modules/services/monitoring/parsedmarc.nix @@ -301,6 +301,7 @@ in description = lib.mdDoc '' The addresses to send outgoing mail to. ''; + apply = x: if x == [] then null else lib.concatStringsSep "," x; }; }; diff --git a/nixos/modules/services/monitoring/prometheus/alertmanager.nix b/nixos/modules/services/monitoring/prometheus/alertmanager.nix index 987f17c2c6e..5fb543ec619 100644 --- a/nixos/modules/services/monitoring/prometheus/alertmanager.nix +++ b/nixos/modules/services/monitoring/prometheus/alertmanager.nix @@ -8,7 +8,7 @@ let checkedConfig = file: if cfg.checkConfig then - pkgs.runCommand "checked-config" { buildInputs = [ cfg.package ]; } '' + pkgs.runCommand "checked-config" { nativeBuildInputs = [ cfg.package ]; } '' ln -s ${file} $out amtool check-config $out '' else file; diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix index 19ee3ae6f7d..a38855ccd40 100644 --- a/nixos/modules/services/monitoring/prometheus/default.nix +++ b/nixos/modules/services/monitoring/prometheus/default.nix @@ -31,7 +31,7 @@ let if checkConfigEnabled then pkgs.runCommandLocal "${name}-${replaceStrings [" "] [""] what}-checked" - { buildInputs = [ cfg.package.cli ]; } '' + { nativeBuildInputs = [ cfg.package.cli ]; } '' ln -s ${file} $out promtool ${what} $out '' else file; diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix index 1d06893bf1d..f89522c0986 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters.nix @@ -2,8 +2,8 @@ let inherit (lib) concatStrings foldl foldl' genAttrs literalExpression maintainers - mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption - optional types mkOptionDefault flip attrNames; + mapAttrs mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption + optional types mkOptionDefault flip attrNames; cfg = config.services.prometheus.exporters; @@ -20,7 +20,7 @@ let # systemd service must be provided by specifying either # `serviceOpts.script` or `serviceOpts.serviceConfig.ExecStart` - exporterOpts = genAttrs [ + exporterOpts = (genAttrs [ "apcupsd" "artifactory" "bind" @@ -34,13 +34,15 @@ let "domain" "dovecot" "fastly" + "flow" "fritzbox" "graphite" "idrac" + "imap-mailstat" "influxdb" "ipmi" - "json" "jitsi" + "json" "junos-czerwonk" "kea" "keylight" @@ -58,6 +60,7 @@ let "nut" "openldap" "openvpn" + "pgbouncer" "php-fpm" "pihole" "postfix" @@ -72,9 +75,9 @@ let "scaphandre" "script" "shelly" - "snmp" "smartctl" "smokeping" + "snmp" "sql" "statsd" "surfboard" @@ -86,10 +89,39 @@ let "v2ray" "varnish" "wireguard" - "flow" "zfs" - ] (name: - import (./. + "/exporters/${name}.nix") { inherit config lib pkgs options; } + ] + (name: + import (./. + "/exporters/${name}.nix") { inherit config lib pkgs options; } + )) // (mapAttrs + (name: params: + import (./. + "/exporters/${params.name}.nix") { inherit config lib pkgs options; 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 }: { @@ -313,6 +345,25 @@ in 'services.prometheus.exporters.nextcloud.tokenFile' ''; } { + assertion = cfg.pgbouncer.enable -> ( + (cfg.pgbouncer.connectionStringFile != null || cfg.pgbouncer.connectionString != "") + ); + message = '' + PgBouncer exporter needs either connectionStringFile or connectionString configured" + ''; + } { + assertion = cfg.pgbouncer.enable -> ( + config.services.pgbouncer.ignoreStartupParameters != null && builtins.match ".*extra_float_digits.*" config.services.pgbouncer.ignoreStartupParameters != null + ); + message = '' + Prometheus PgBouncer exporter requires including `extra_float_digits` in services.pgbouncer.ignoreStartupParameters + + Example: + services.pgbouncer.ignoreStartupParameters = extra_float_digits; + + See https://github.com/prometheus-community/pgbouncer_exporter#pgbouncer-configuration + ''; + } { assertion = cfg.sql.enable -> ( (cfg.sql.configFile == null) != (cfg.sql.configuration == null) ); @@ -350,12 +401,24 @@ in `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; + 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. + '' + ) + (mkIf + (cfg.pgbouncer.enable && cfg.pgbouncer.connectionString != "") '' + config.services.prometheus.exporters.pgbouncer.connectionString is insecure. Use connectionStringFile instead. + '' + ) + (mkIf + (cfg.pgbouncer.enable && config.services.pgbouncer.authType != "any") '' + Admin user (with password or passwordless) MUST exist in the services.pgbouncer.authFile if authType other than any is used. + '' + ) + ] ++ config.services.prometheus.exporters.warnings; }] ++ [(mkIf config.services.minio.enable { services.prometheus.exporters.minio.minioAddress = mkDefault "http://localhost:9000"; services.prometheus.exporters.minio.minioAccessKey = mkDefault config.services.minio.accessKey; diff --git a/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix b/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix index 407bff1d62d..ce2c391de52 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix @@ -25,7 +25,7 @@ let checkConfig = file: pkgs.runCommand "checked-blackbox-exporter.conf" { preferLocalBuild = true; - buildInputs = [ pkgs.buildPackages.prometheus-blackbox-exporter ]; + nativeBuildInputs = [ pkgs.buildPackages.prometheus-blackbox-exporter ]; } '' ln -s ${coerceConfigFile file} $out blackbox_exporter --config.check --config.file $out diff --git a/nixos/modules/services/monitoring/prometheus/exporters/exportarr.nix b/nixos/modules/services/monitoring/prometheus/exporters/exportarr.nix new file mode 100644 index 00000000000..13220933541 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/exportarr.nix @@ -0,0 +1,55 @@ +{ config, lib, pkgs, options, type }: + +let + cfg = config.services.prometheus.exporters."exportarr-${type}"; + exportarrEnvironment = ( + lib.mapAttrs (_: toString) cfg.environment + ) // { + PORT = toString cfg.port; + URL = cfg.url; + API_KEY_FILE = lib.mkIf (cfg.apiKeyFile != null) "%d/api-key"; + }; +in +{ + port = 9708; + extraOpts = { + url = lib.mkOption { + type = lib.types.str; + default = "http://127.0.0.1"; + description = lib.mdDoc '' + The full URL to Sonarr, Radarr, or Lidarr. + ''; + }; + + apiKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = lib.mdDoc '' + File containing the api-key. + ''; + }; + + package = lib.mkPackageOptionMD pkgs "exportarr" { }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = lib.mdDoc '' + See [the configuration guide](https://github.com/onedr0p/exportarr#configuration) for available options. + ''; + example = { + PROWLARR__BACKFILL = true; + }; + }; + }; + serviceOpts = { + serviceConfig = { + LoadCredential = lib.optionalString (cfg.apiKeyFile != null) "api-key:${cfg.apiKeyFile}"; + ExecStart = ''${cfg.package}/bin/exportarr ${type} "$@"''; + ProcSubset = "pid"; + ProtectProc = "invisible"; + SystemCallFilter = ["@system-service" "~@privileged"]; + }; + environment = exportarrEnvironment; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/imap-mailstat.nix b/nixos/modules/services/monitoring/prometheus/exporters/imap-mailstat.nix new file mode 100644 index 00000000000..c5024a258e7 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/imap-mailstat.nix @@ -0,0 +1,71 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.imap-mailstat; + valueToString = value: + if (builtins.typeOf value == "string") then "\"${value}\"" + else ( + if (builtins.typeOf value == "int") then "${toString value}" + else ( + if (builtins.typeOf value == "bool") then (if value then "true" else "false") + else "XXX ${toString value}" + ) + ); + createConfigFile = accounts: + # unfortunately on toTOML yet + # https://github.com/NixOS/nix/issues/3929 + pkgs.writeText "imap-mailstat-exporter.conf" '' + ${concatStrings (attrValues (mapAttrs (name: config: "[[Accounts]]\nname = \"${name}\"\n${concatStrings (attrValues (mapAttrs (k: v: "${k} = ${valueToString v}\n") config))}") accounts))} + ''; + mkOpt = type: description: mkOption { + type = types.nullOr type; + default = null; + description = lib.mdDoc description; + }; + accountOptions.options = { + mailaddress = mkOpt types.str "Your email address (at the moment used as login name)"; + username = mkOpt types.str "If empty string mailaddress value is used"; + password = mkOpt types.str ""; + serveraddress = mkOpt types.str "mailserver name or address"; + serverport = mkOpt types.int "imap port number (at the moment only tls connection is supported)"; + starttls = mkOpt types.bool "set to true for using STARTTLS to start a TLS connection"; + }; +in +{ + port = 8081; + extraOpts = { + oldestUnseenDate = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Enable metric with timestamp of oldest unseen mail + ''; + }; + accounts = mkOption { + type = types.attrsOf (types.submodule accountOptions); + default = {}; + description = lib.mdDoc '' + Accounts to monitor + ''; + }; + configurationFile = mkOption { + type = types.path; + example = "/path/to/config-file"; + description = lib.mdDoc '' + File containing the configuration + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-imap-mailstat-exporter}/bin/imap-mailstat-exporter \ + -config ${createConfigFile cfg.accounts} \ + ${optionalString cfg.oldestUnseenDate "-oldestunseendate"} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/kea.nix b/nixos/modules/services/monitoring/prometheus/exporters/kea.nix index ed33c72f644..8b1cd47d0a4 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/kea.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/kea.nix @@ -15,8 +15,8 @@ in { type = types.listOf types.str; example = literalExpression '' [ - "/run/kea/kea-dhcp4.socket" - "/run/kea/kea-dhcp6.socket" + "/run/kea-dhcp4/kea-dhcp4.socket" + "/run/kea-dhcp6/kea-dhcp6.socket" ] ''; description = lib.mdDoc '' diff --git a/nixos/modules/services/monitoring/prometheus/exporters/knot.nix b/nixos/modules/services/monitoring/prometheus/exporters/knot.nix index a73425b37da..77584875080 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/knot.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/knot.nix @@ -8,9 +8,9 @@ in { port = 9433; extraOpts = { knotLibraryPath = mkOption { - type = types.str; - default = "${pkgs.knot-dns.out}/lib/libknot.so"; - defaultText = literalExpression ''"''${pkgs.knot-dns.out}/lib/libknot.so"''; + type = types.nullOr types.str; + default = null; + example = literalExpression ''"''${pkgs.knot-dns.out}/lib/libknot.so"''; description = lib.mdDoc '' Path to the library of `knot-dns`. ''; @@ -25,7 +25,7 @@ in { }; knotSocketTimeout = mkOption { - type = types.int; + type = types.ints.positive; default = 2000; description = lib.mdDoc '' Timeout in seconds. @@ -33,17 +33,22 @@ in { }; }; serviceOpts = { + path = with pkgs; [ + procps + ]; serviceConfig = { ExecStart = '' - ${pkgs.prometheus-knot-exporter}/bin/knot_exporter \ + ${pkgs.prometheus-knot-exporter}/bin/knot-exporter \ --web-listen-addr ${cfg.listenAddress} \ --web-listen-port ${toString cfg.port} \ - --knot-library-path ${cfg.knotLibraryPath} \ --knot-socket-path ${cfg.knotSocketPath} \ --knot-socket-timeout ${toString cfg.knotSocketTimeout} \ + ${lib.optionalString (cfg.knotLibraryPath != null) "--knot-library-path ${cfg.knotLibraryPath}"} \ ${concatStringsSep " \\\n " cfg.extraFlags} ''; - SupplementaryGroups = [ "knot" ]; + SupplementaryGroups = [ + "knot" + ]; RestrictAddressFamilies = [ # Need AF_UNIX to collect data "AF_UNIX" diff --git a/nixos/modules/services/monitoring/prometheus/exporters/pgbouncer.nix b/nixos/modules/services/monitoring/prometheus/exporters/pgbouncer.nix new file mode 100644 index 00000000000..9e55cadae52 --- /dev/null +++ b/nixos/modules/services/monitoring/prometheus/exporters/pgbouncer.nix @@ -0,0 +1,145 @@ +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.pgbouncer; +in +{ + port = 9127; + extraOpts = { + + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = lib.mdDoc '' + Path under which to expose metrics. + ''; + }; + + connectionString = mkOption { + type = types.str; + default = ""; + example = "postgres://admin:@localhost:6432/pgbouncer?sslmode=require"; + description = lib.mdDoc '' + Connection string for accessing pgBouncer. + + NOTE: You MUST keep pgbouncer as database name (special internal db)!!! + + NOTE: Admin user (with password or passwordless) MUST exist + in the services.pgbouncer.authFile if authType other than any is used. + + WARNING: this secret is stored in the world-readable Nix store! + Use {option}`connectionStringFile` instead. + ''; + }; + + connectionStringFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/keys/pgBouncer-connection-string"; + description = lib.mdDoc '' + File that contains pgBouncer connection string in format: + postgres://admin:@localhost:6432/pgbouncer?sslmode=require + + NOTE: You MUST keep pgbouncer as database name (special internal db)!!! + + NOTE: Admin user (with password or passwordless) MUST exist + in the services.pgbouncer.authFile if authType other than any is used. + + {option}`connectionStringFile` takes precedence over {option}`connectionString` + ''; + }; + + pidFile = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc '' + Path to PgBouncer pid file. + + If provided, the standard process metrics get exported for the PgBouncer + process, prefixed with 'pgbouncer_process_...'. The pgbouncer_process exporter + needs to have read access to files owned by the PgBouncer process. Depends on + the availability of /proc. + + https://prometheus.io/docs/instrumenting/writing_clientlibs/#process-metrics. + + ''; + }; + + webSystemdSocket = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Use systemd socket activation listeners instead of port listeners (Linux only). + ''; + }; + + logLevel = mkOption { + type = types.enum ["debug" "info" "warn" "error" ]; + default = "info"; + description = lib.mdDoc '' + Only log messages with the given severity or above. + ''; + }; + + logFormat = mkOption { + type = types.enum ["logfmt" "json"]; + default = "logfmt"; + description = lib.mdDoc '' + Output format of log messages. One of: [logfmt, json] + ''; + }; + + webConfigFile = mkOption { + type = types.nullOr types.path; + default = null; + description = lib.mdDoc '' + Path to configuration file that can enable TLS or authentication. + ''; + }; + + extraFlags = mkOption { + type = types.listOf types.str; + default = [ ]; + description = lib.mdDoc '' + Extra commandline options when launching Prometheus. + ''; + }; + + }; + + serviceOpts = { + after = [ "pgbouncer.service" ]; + serviceConfig = let + startScript = pkgs.writeShellScriptBin "pgbouncer-start" "${concatStringsSep " " ([ + "${pkgs.prometheus-pgbouncer-exporter}/bin/pgbouncer_exporter" + "--web.listen-address ${cfg.listenAddress}:${toString cfg.port}" + "--pgBouncer.connectionString ${if cfg.connectionStringFile != null then + "$(head -n1 ${cfg.connectionStringFile})" else "${escapeShellArg cfg.connectionString}"}" + ] + ++ optionals (cfg.telemetryPath != null) [ + "--web.telemetry-path ${escapeShellArg cfg.telemetryPath}" + ] + ++ optionals (cfg.pidFile != null) [ + "--pgBouncer.pid-file= ${escapeShellArg cfg.pidFile}" + ] + ++ optionals (cfg.logLevel != null) [ + "--log.level ${escapeShellArg cfg.logLevel}" + ] + ++ optionals (cfg.logFormat != null) [ + "--log.format ${escapeShellArg cfg.logFormat}" + ] + ++ optionals (cfg.webSystemdSocket != false) [ + "--web.systemd-socket ${escapeShellArg cfg.webSystemdSocket}" + ] + ++ optionals (cfg.webConfigFile != null) [ + "--web.config.file ${escapeShellArg cfg.webConfigFile}" + ] + ++ cfg.extraFlags)}"; + in + { + ExecStart = "${startScript}/bin/pgbouncer-start"; + }; + }; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix b/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix index c98dcd9f64b..9b759031493 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix @@ -11,7 +11,7 @@ in { ({ options.warnings = options.warnings; options.assertions = options.assertions; }) ]; extraOpts = { - verbose = mkEnableOption (lib.mdDoc "Verbose logging mode for prometheus-wireguard-exporter"); + verbose = mkEnableOption (lib.mdDoc "verbose logging mode for prometheus-wireguard-exporter"); wireguardConfig = mkOption { type = with types; nullOr (either path str); diff --git a/nixos/modules/services/monitoring/smartd.nix b/nixos/modules/services/monitoring/smartd.nix index 1e654cad5dd..8b79ac0e0c1 100644 --- a/nixos/modules/services/monitoring/smartd.nix +++ b/nixos/modules/services/monitoring/smartd.nix @@ -19,7 +19,7 @@ let { ${pkgs.coreutils}/bin/cat << EOF From: smartd on ${host} <${nm.sender}> - To: undisclosed-recipients:; + To: ${nm.recipient} Subject: $SMARTD_SUBJECT $SMARTD_FULLMESSAGE diff --git a/nixos/modules/services/monitoring/ups.nix b/nixos/modules/services/monitoring/ups.nix index bb11b6a1c1d..efef2d777ac 100644 --- a/nixos/modules/services/monitoring/ups.nix +++ b/nixos/modules/services/monitoring/ups.nix @@ -239,11 +239,9 @@ in power.ups.schedulerRules = mkDefault "${pkgs.nut}/etc/upssched.conf.sample"; - system.activationScripts.upsSetup = stringAfter [ "users" "groups" ] - '' - # Used to store pid files of drivers. - mkdir -p /var/state/ups - ''; + systemd.tmpfiles.rules = [ + "d /var/state/ups -" + ]; /* diff --git a/nixos/modules/services/monitoring/zabbix-proxy.nix b/nixos/modules/services/monitoring/zabbix-proxy.nix index 85da416ba6c..503e81b48a5 100644 --- a/nixos/modules/services/monitoring/zabbix-proxy.nix +++ b/nixos/modules/services/monitoring/zabbix-proxy.nix @@ -203,7 +203,7 @@ in { assertion = !config.services.zabbixServer.enable; message = "Please choose one of services.zabbixServer or services.zabbixProxy."; } - { assertion = cfg.database.createLocally -> cfg.database.user == user; + { assertion = cfg.database.createLocally -> cfg.database.user == user && cfg.database.name == cfg.database.user; message = "services.zabbixProxy.database.user must be set to ${user} if services.zabbixProxy.database.createLocally is set true"; } { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null; @@ -252,7 +252,7 @@ in ensureDatabases = [ cfg.database.name ]; ensureUsers = [ { name = cfg.database.user; - ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; }; + ensureDBOwnership = true; } ]; }; diff --git a/nixos/modules/services/monitoring/zabbix-server.nix b/nixos/modules/services/monitoring/zabbix-server.nix index 2b50280e396..0607188d213 100644 --- a/nixos/modules/services/monitoring/zabbix-server.nix +++ b/nixos/modules/services/monitoring/zabbix-server.nix @@ -191,7 +191,7 @@ in config = mkIf cfg.enable { assertions = [ - { assertion = cfg.database.createLocally -> cfg.database.user == user; + { assertion = cfg.database.createLocally -> cfg.database.user == user && cfg.database.user == cfg.database.name; message = "services.zabbixServer.database.user must be set to ${user} if services.zabbixServer.database.createLocally is set true"; } { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null; @@ -240,7 +240,7 @@ in ensureDatabases = [ cfg.database.name ]; ensureUsers = [ { name = cfg.database.user; - ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; }; + ensureDBOwnership = true; } ]; }; |