diff options
Diffstat (limited to 'nixpkgs/nixos/modules/services/monitoring')
153 files changed, 21559 insertions, 0 deletions
diff --git a/nixpkgs/nixos/modules/services/monitoring/alerta.nix b/nixpkgs/nixos/modules/services/monitoring/alerta.nix new file mode 100644 index 000000000000..32c71e730102 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/alerta.nix @@ -0,0 +1,112 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.alerta; + + alertaConf = pkgs.writeTextFile { + name = "alertad.conf"; + text = '' + DATABASE_URL = '${cfg.databaseUrl}' + DATABASE_NAME = '${cfg.databaseName}' + LOG_FILE = '${cfg.logDir}/alertad.log' + LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + CORS_ORIGINS = [ ${concatMapStringsSep ", " (s: "\"" + s + "\"") cfg.corsOrigins} ]; + AUTH_REQUIRED = ${if cfg.authenticationRequired then "True" else "False"} + SIGNUP_ENABLED = ${if cfg.signupEnabled then "True" else "False"} + ${cfg.extraConfig} + ''; + }; +in +{ + options.services.alerta = { + enable = mkEnableOption "alerta"; + + port = mkOption { + type = types.port; + default = 5000; + description = "Port of Alerta"; + }; + + bind = mkOption { + type = types.str; + default = "0.0.0.0"; + description = "Address to bind to. The default is to bind to all addresses"; + }; + + logDir = mkOption { + type = types.path; + description = "Location where the logfiles are stored"; + default = "/var/log/alerta"; + }; + + databaseUrl = mkOption { + type = types.str; + description = "URL of the MongoDB or PostgreSQL database to connect to"; + default = "mongodb://localhost"; + }; + + databaseName = mkOption { + type = types.str; + description = "Name of the database instance to connect to"; + default = "monitoring"; + }; + + corsOrigins = mkOption { + type = types.listOf types.str; + description = "List of URLs that can access the API for Cross-Origin Resource Sharing (CORS)"; + default = [ "http://localhost" "http://localhost:5000" ]; + }; + + authenticationRequired = mkOption { + type = types.bool; + description = "Whether users must authenticate when using the web UI or command-line tool"; + default = false; + }; + + signupEnabled = mkOption { + type = types.bool; + description = "Whether to prevent sign-up of new users via the web UI"; + default = true; + }; + + extraConfig = mkOption { + description = "These lines go into alertad.conf verbatim."; + default = ""; + type = types.lines; + }; + }; + + config = mkIf cfg.enable { + systemd.tmpfiles.settings."10-alerta".${cfg.logDir}.d = { + user = "alerta"; + group = "alerta"; + }; + + systemd.services.alerta = { + description = "Alerta Monitoring System"; + wantedBy = [ "multi-user.target" ]; + after = [ "networking.target" ]; + environment = { + ALERTA_SVR_CONF_FILE = alertaConf; + }; + serviceConfig = { + ExecStart = "${pkgs.alerta-server}/bin/alertad run --port ${toString cfg.port} --host ${cfg.bind}"; + User = "alerta"; + Group = "alerta"; + }; + }; + + environment.systemPackages = [ pkgs.alerta ]; + + users.users.alerta = { + uid = config.ids.uids.alerta; + description = "Alerta user"; + }; + + users.groups.alerta = { + gid = config.ids.gids.alerta; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/apcupsd.nix b/nixpkgs/nixos/modules/services/monitoring/apcupsd.nix new file mode 100644 index 000000000000..09cf593f5d5e --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/apcupsd.nix @@ -0,0 +1,206 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.apcupsd; + + configFile = pkgs.writeText "apcupsd.conf" '' + ## apcupsd.conf v1.1 ## + # apcupsd complains if the first line is not like above. + ${cfg.configText} + SCRIPTDIR ${toString scriptDir} + ''; + + # List of events from "man apccontrol" + eventList = [ + "annoyme" + "battattach" + "battdetach" + "changeme" + "commfailure" + "commok" + "doreboot" + "doshutdown" + "emergency" + "failing" + "killpower" + "loadlimit" + "mainsback" + "onbattery" + "offbattery" + "powerout" + "remotedown" + "runlimit" + "timeout" + "startselftest" + "endselftest" + ]; + + shellCmdsForEventScript = eventname: commands: '' + echo "#!${pkgs.runtimeShell}" > "$out/${eventname}" + echo '${commands}' >> "$out/${eventname}" + chmod a+x "$out/${eventname}" + ''; + + eventToShellCmds = event: if builtins.hasAttr event cfg.hooks then (shellCmdsForEventScript event (builtins.getAttr event cfg.hooks)) else ""; + + scriptDir = pkgs.runCommand "apcupsd-scriptdir" { preferLocalBuild = true; } ('' + mkdir "$out" + # Copy SCRIPTDIR from apcupsd package + cp -r ${pkgs.apcupsd}/etc/apcupsd/* "$out"/ + # Make the files writeable (nix will unset the write bits afterwards) + chmod u+w "$out"/* + # Remove the sample event notification scripts, because they don't work + # anyways (they try to send mail to "root" with the "mail" command) + (cd "$out" && rm changeme commok commfailure onbattery offbattery) + # Remove the sample apcupsd.conf file (we're generating our own) + rm "$out/apcupsd.conf" + # Set the SCRIPTDIR= line in apccontrol to the dir we're creating now + sed -i -e "s|^SCRIPTDIR=.*|SCRIPTDIR=$out|" "$out/apccontrol" + '' + concatStringsSep "\n" (map eventToShellCmds eventList) + + ); + + # Ensure the CLI uses our generated configFile + wrappedBinaries = pkgs.runCommandLocal "apcupsd-wrapped-binaries" + { nativeBuildInputs = [ pkgs.makeWrapper ]; } + '' + for p in "${lib.getBin pkgs.apcupsd}/bin/"*; do + bname=$(basename "$p") + makeWrapper "$p" "$out/bin/$bname" --add-flags "-f ${configFile}" + done + ''; + + apcupsdWrapped = pkgs.symlinkJoin { + name = "apcupsd-wrapped"; + # Put wrappers first so they "win" + paths = [ wrappedBinaries pkgs.apcupsd ]; + }; +in + +{ + + ###### interface + + options = { + + services.apcupsd = { + + enable = mkOption { + default = false; + type = types.bool; + description = '' + Whether to enable the APC UPS daemon. apcupsd monitors your UPS and + permits orderly shutdown of your computer in the event of a power + failure. User manual: http://www.apcupsd.com/manual/manual.html. + Note that apcupsd runs as root (to allow shutdown of computer). + You can check the status of your UPS with the "apcaccess" command. + ''; + }; + + configText = mkOption { + default = '' + UPSTYPE usb + NISIP 127.0.0.1 + BATTERYLEVEL 50 + MINUTES 5 + ''; + type = types.lines; + description = '' + Contents of the runtime configuration file, apcupsd.conf. The default + settings makes apcupsd autodetect USB UPSes, limit network access to + localhost and shutdown the system when the battery level is below 50 + percent, or when the UPS has calculated that it has 5 minutes or less + of remaining power-on time. See man apcupsd.conf for details. + ''; + }; + + hooks = mkOption { + default = {}; + example = { + doshutdown = "# shell commands to notify that the computer is shutting down"; + }; + type = types.attrsOf types.lines; + description = '' + Each attribute in this option names an apcupsd event and the string + value it contains will be executed in a shell, in response to that + event (prior to the default action). See "man apccontrol" for the + list of events and what they represent. + + A hook script can stop apccontrol from doing its default action by + exiting with value 99. Do not do this unless you know what you're + doing. + ''; + }; + + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + + assertions = [ { + assertion = let hooknames = builtins.attrNames cfg.hooks; in all (x: elem x eventList) hooknames; + message = '' + One (or more) attribute names in services.apcupsd.hooks are invalid. + Current attribute names: ${toString (builtins.attrNames cfg.hooks)} + Valid attribute names : ${toString eventList} + ''; + } ]; + + # Give users access to the "apcaccess" tool + environment.systemPackages = [ apcupsdWrapped ]; + + # NOTE 1: apcupsd runs as root because it needs permission to run + # "shutdown" + # + # NOTE 2: When apcupsd calls "wall", it prints an error because stdout is + # not connected to a tty (it is connected to the journal): + # wall: cannot get tty name: Inappropriate ioctl for device + # The message still gets through. + systemd.services.apcupsd = { + description = "APC UPS Daemon"; + wantedBy = [ "multi-user.target" ]; + preStart = "mkdir -p /run/apcupsd/"; + serviceConfig = { + ExecStart = "${pkgs.apcupsd}/bin/apcupsd -b -f ${configFile} -d1"; + # TODO: When apcupsd has initiated a shutdown, systemd always ends up + # waiting for it to stop ("A stop job is running for UPS daemon"). This + # is weird, because in the journal one can clearly see that apcupsd has + # received the SIGTERM signal and has already quit (or so it seems). + # This reduces the wait time from 90 seconds (default) to just 5. Then + # systemd kills it with SIGKILL. + TimeoutStopSec = 5; + }; + unitConfig.Documentation = "man:apcupsd(8)"; + }; + + # A special service to tell the UPS to power down/hibernate just before the + # computer shuts down. (The UPS has a built in delay before it actually + # shuts off power.) Copied from here: + # http://forums.opensuse.org/english/get-technical-help-here/applications/479499-apcupsd-systemd-killpower-issues.html + systemd.services.apcupsd-killpower = { + description = "APC UPS Kill Power"; + after = [ "shutdown.target" ]; # append umount.target? + before = [ "final.target" ]; + wantedBy = [ "shutdown.target" ]; + unitConfig = { + ConditionPathExists = "/run/apcupsd/powerfail"; + DefaultDependencies = "no"; + }; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.apcupsd}/bin/apcupsd --killpower -f ${configFile}"; + TimeoutSec = "infinity"; + StandardOutput = "tty"; + RemainAfterExit = "yes"; + }; + }; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/arbtt.nix b/nixpkgs/nixos/modules/services/monitoring/arbtt.nix new file mode 100644 index 000000000000..6dad6bdec328 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/arbtt.nix @@ -0,0 +1,49 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.arbtt; +in { + options = { + services.arbtt = { + enable = mkEnableOption "Arbtt statistics capture service"; + + package = mkPackageOption pkgs [ "haskellPackages" "arbtt" ] { }; + + logFile = mkOption { + type = types.str; + default = "%h/.arbtt/capture.log"; + example = "/home/username/.arbtt-capture.log"; + description = '' + The log file for captured samples. + ''; + }; + + sampleRate = mkOption { + type = types.int; + default = 60; + example = 120; + description = '' + The sampling interval in seconds. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.user.services.arbtt = { + description = "arbtt statistics capture service"; + wantedBy = [ "graphical-session.target" ]; + partOf = [ "graphical-session.target" ]; + + serviceConfig = { + Type = "simple"; + ExecStart = "${cfg.package}/bin/arbtt-capture --logfile=${cfg.logFile} --sample-rate=${toString cfg.sampleRate}"; + Restart = "always"; + }; + }; + }; + + meta.maintainers = [ maintainers.michaelpj ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/below.nix b/nixpkgs/nixos/modules/services/monitoring/below.nix new file mode 100644 index 000000000000..729734828142 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/below.nix @@ -0,0 +1,108 @@ +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.services.below; + cfgContents = concatStringsSep "\n" ( + mapAttrsToList (n: v: ''${n} = "${v}"'') (filterAttrs (_k: v: v != null) { + log_dir = cfg.dirs.log; + store_dir = cfg.dirs.store; + cgroup_filter_out = cfg.cgroupFilterOut; + }) + ); + + mkDisableOption = n: mkOption { + type = types.bool; + default = true; + description = "Whether to enable ${n}."; + }; + optionalType = ty: x: mkOption (x // { + description = x.description; + type = (types.nullOr ty); + default = null; + }); + optionalPath = optionalType types.path; + optionalStr = optionalType types.str; + optionalInt = optionalType types.int; +in { + options = { + services.below = { + enable = mkEnableOption "'below' resource monitor"; + + cgroupFilterOut = optionalStr { + description = "A regexp matching the full paths of cgroups whose data shouldn't be collected"; + example = "user.slice.*"; + }; + collect = { + diskStats = mkDisableOption "dist_stat collection"; + ioStats = mkEnableOption "io.stat collection for cgroups"; + exitStats = mkDisableOption "eBPF-based exitstats"; + }; + compression.enable = mkEnableOption "data compression"; + retention = { + size = optionalInt { + description = '' + Size limit for below's data, in bytes. Data is deleted oldest-first, in 24h 'shards'. + + ::: {.note} + The size limit may be exceeded by at most the size of the active shard, as: + - the active shard cannot be deleted; + - the size limit is only enforced when a new shard is created. + ::: + ''; + }; + time = optionalInt { + description = '' + Retention time, in seconds. + + ::: {.note} + As data is stored in 24 hour shards which are discarded as a whole, + only data expired by 24h (or more) is guaranteed to be discarded. + ::: + + ::: {.note} + If `retention.size` is set, data may be discarded earlier than the specified time. + ::: + ''; + }; + }; + dirs = { + log = optionalPath { description = "Where to store below's logs"; }; + store = optionalPath { + description = "Where to store below's data"; + example = "/var/lib/below"; + }; + }; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [ pkgs.below ]; + # /etc/below.conf is also refered to by the `below` CLI tool, + # so this can't be a store-only file whose path is passed to the service + environment.etc."below/below.conf".text = cfgContents; + + systemd = { + packages = [ pkgs.below ]; + services.below = { + # Workaround for https://github.com/NixOS/nixpkgs/issues/81138 + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ cfgContents ]; + + serviceConfig.ExecStart = [ + "" + ("${lib.getExe pkgs.below} record " + (concatStringsSep " " ( + optional (!cfg.collect.diskStats) "--disable-disk-stat" ++ + optional cfg.collect.ioStats "--collect-io-stat" ++ + optional (!cfg.collect.exitStats) "--disable-exitstats" ++ + optional cfg.compression.enable "--compress" ++ + + optional (cfg.retention.size != null) "--store-size-limit ${toString cfg.retention.size}" ++ + optional (cfg.retention.time != null) "--retain-for-s ${toString cfg.retention.time}" + ))) + ]; + }; + }; + }; + + meta.maintainers = with lib.maintainers; [ nicoo ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/bosun.nix b/nixpkgs/nixos/modules/services/monitoring/bosun.nix new file mode 100644 index 000000000000..4b855b96e949 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/bosun.nix @@ -0,0 +1,152 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.bosun; + + configFile = pkgs.writeText "bosun.conf" '' + ${optionalString (cfg.opentsdbHost !=null) "tsdbHost = ${cfg.opentsdbHost}"} + ${optionalString (cfg.influxHost !=null) "influxHost = ${cfg.influxHost}"} + httpListen = ${cfg.listenAddress} + stateFile = ${cfg.stateFile} + ledisDir = ${cfg.ledisDir} + checkFrequency = ${cfg.checkFrequency} + + ${cfg.extraConfig} + ''; + +in { + + options = { + + services.bosun = { + + enable = mkEnableOption "bosun"; + + package = mkPackageOption pkgs "bosun" { }; + + user = mkOption { + type = types.str; + default = "bosun"; + description = '' + User account under which bosun runs. + ''; + }; + + group = mkOption { + type = types.str; + default = "bosun"; + description = '' + Group account under which bosun runs. + ''; + }; + + opentsdbHost = mkOption { + type = types.nullOr types.str; + default = "localhost:4242"; + description = '' + Host and port of the OpenTSDB database that stores bosun data. + To disable opentsdb you can pass null as parameter. + ''; + }; + + influxHost = mkOption { + type = types.nullOr types.str; + default = null; + example = "localhost:8086"; + description = '' + Host and port of the influxdb database. + ''; + }; + + listenAddress = mkOption { + type = types.str; + default = ":8070"; + description = '' + The host address and port that bosun's web interface will listen on. + ''; + }; + + stateFile = mkOption { + type = types.path; + default = "/var/lib/bosun/bosun.state"; + description = '' + Path to bosun's state file. + ''; + }; + + ledisDir = mkOption { + type = types.path; + default = "/var/lib/bosun/ledis_data"; + description = '' + Path to bosun's ledis data dir + ''; + }; + + checkFrequency = mkOption { + type = types.str; + default = "5m"; + description = '' + Bosun's check frequency + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration options for Bosun. You should describe your + desired templates, alerts, macros, etc through this configuration + option. + + A detailed description of the supported syntax can be found at-spi2-atk + https://bosun.org/configuration.html + ''; + }; + + }; + + }; + + config = mkIf cfg.enable { + + systemd.services.bosun = { + description = "bosun metrics collector (part of Bosun)"; + wantedBy = [ "multi-user.target" ]; + + preStart = '' + mkdir -p "$(dirname "${cfg.stateFile}")"; + touch "${cfg.stateFile}" + touch "${cfg.stateFile}.tmp" + + mkdir -p "${cfg.ledisDir}"; + + if [ "$(id -u)" = 0 ]; then + chown ${cfg.user}:${cfg.group} "${cfg.stateFile}" + chown ${cfg.user}:${cfg.group} "${cfg.stateFile}.tmp" + chown ${cfg.user}:${cfg.group} "${cfg.ledisDir}" + fi + ''; + + serviceConfig = { + PermissionsStartOnly = true; + User = cfg.user; + Group = cfg.group; + ExecStart = '' + ${cfg.package}/bin/bosun -c ${configFile} + ''; + }; + }; + + users.users.bosun = { + description = "bosun user"; + group = "bosun"; + uid = config.ids.uids.bosun; + }; + + users.groups.bosun.gid = config.ids.gids.bosun; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/cadvisor.nix b/nixpkgs/nixos/modules/services/monitoring/cadvisor.nix new file mode 100644 index 000000000000..6b0852cfe3ef --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/cadvisor.nix @@ -0,0 +1,138 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.cadvisor; + +in { + options = { + services.cadvisor = { + enable = mkEnableOption "Cadvisor service"; + + listenAddress = mkOption { + default = "127.0.0.1"; + type = types.str; + description = "Cadvisor listening host"; + }; + + port = mkOption { + default = 8080; + type = types.port; + description = "Cadvisor listening port"; + }; + + storageDriver = mkOption { + default = null; + type = types.nullOr types.str; + example = "influxdb"; + description = "Cadvisor storage driver."; + }; + + storageDriverHost = mkOption { + default = "localhost:8086"; + type = types.str; + description = "Cadvisor storage driver host."; + }; + + storageDriverDb = mkOption { + default = "root"; + type = types.str; + description = "Cadvisord storage driver database name."; + }; + + storageDriverUser = mkOption { + default = "root"; + type = types.str; + description = "Cadvisor storage driver username."; + }; + + storageDriverPassword = mkOption { + default = "root"; + type = types.str; + description = '' + Cadvisor storage driver password. + + Warning: this password is stored in the world-readable Nix store. It's + recommended to use the {option}`storageDriverPasswordFile` option + since that gives you control over the security of the password. + {option}`storageDriverPasswordFile` also takes precedence over {option}`storageDriverPassword`. + ''; + }; + + storageDriverPasswordFile = mkOption { + type = types.str; + description = '' + File that contains the cadvisor storage driver password. + + {option}`storageDriverPasswordFile` takes precedence over {option}`storageDriverPassword` + + Warning: when {option}`storageDriverPassword` is non-empty this defaults to a file in the + world-readable Nix store that contains the value of {option}`storageDriverPassword`. + + It's recommended to override this with a path not in the Nix store. + Tip: use [nixops key management](https://nixos.org/nixops/manual/#idm140737318306400) + ''; + }; + + storageDriverSecure = mkOption { + default = false; + type = types.bool; + description = "Cadvisor storage driver, enable secure communication."; + }; + + extraOptions = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Additional cadvisor options. + + See <https://github.com/google/cadvisor/blob/master/docs/runtime_options.md> for available options. + ''; + }; + }; + }; + + config = mkMerge [ + { services.cadvisor.storageDriverPasswordFile = mkIf (cfg.storageDriverPassword != "") ( + mkDefault (toString (pkgs.writeTextFile { + name = "cadvisor-storage-driver-password"; + text = cfg.storageDriverPassword; + })) + ); + } + + (mkIf cfg.enable { + systemd.services.cadvisor = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "docker.service" "influxdb.service" ]; + + path = optionals config.boot.zfs.enabled [ pkgs.zfs ]; + + postStart = mkBefore '' + until ${pkgs.curl.bin}/bin/curl -s -o /dev/null 'http://${cfg.listenAddress}:${toString cfg.port}/containers/'; do + sleep 1; + done + ''; + + script = '' + exec ${pkgs.cadvisor}/bin/cadvisor \ + -logtostderr=true \ + -listen_ip="${cfg.listenAddress}" \ + -port="${toString cfg.port}" \ + ${escapeShellArgs cfg.extraOptions} \ + ${optionalString (cfg.storageDriver != null) '' + -storage_driver "${cfg.storageDriver}" \ + -storage_driver_host "${cfg.storageDriverHost}" \ + -storage_driver_db "${cfg.storageDriverDb}" \ + -storage_driver_user "${cfg.storageDriverUser}" \ + -storage_driver_password "$(cat "${cfg.storageDriverPasswordFile}")" \ + ${optionalString cfg.storageDriverSecure "-storage_driver_secure"} + ''} + ''; + + serviceConfig.TimeoutStartSec=300; + }; + }) + ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/certspotter.md b/nixpkgs/nixos/modules/services/monitoring/certspotter.md new file mode 100644 index 000000000000..e999bfe65ec3 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/certspotter.md @@ -0,0 +1,78 @@ +# 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/nixpkgs/nixos/modules/services/monitoring/certspotter.nix b/nixpkgs/nixos/modules/services/monitoring/certspotter.nix new file mode 100644 index 000000000000..5551f0e37c51 --- /dev/null +++ b/nixpkgs/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.mkPackageOption 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/nixpkgs/nixos/modules/services/monitoring/cockpit.nix b/nixpkgs/nixos/modules/services/monitoring/cockpit.nix new file mode 100644 index 000000000000..64e26ce4e127 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/cockpit.nix @@ -0,0 +1,231 @@ +{ pkgs, config, lib, ... }: + +let + cfg = config.services.cockpit; + inherit (lib) types mkEnableOption mkOption mkIf literalMD mkPackageOption; + settingsFormat = pkgs.formats.ini {}; +in { + options = { + services.cockpit = { + enable = mkEnableOption "Cockpit"; + + package = mkPackageOption pkgs "Cockpit" { + default = [ "cockpit" ]; + }; + + settings = lib.mkOption { + type = settingsFormat.type; + + default = {}; + + description = '' + Settings for cockpit that will be saved in /etc/cockpit/cockpit.conf. + + See the [documentation](https://cockpit-project.org/guide/latest/cockpit.conf.5.html), that is also available with `man cockpit.conf.5` for details. + ''; + }; + + port = mkOption { + description = "Port where cockpit will listen."; + type = types.port; + default = 9090; + }; + + openFirewall = mkOption { + description = "Open port for cockpit."; + type = types.bool; + default = false; + }; + }; + }; + config = mkIf cfg.enable { + + # expose cockpit-bridge system-wide + environment.systemPackages = [ cfg.package ]; + + # allow cockpit to find its plugins + environment.pathsToLink = [ "/share/cockpit" ]; + + # generate cockpit settings + environment.etc."cockpit/cockpit.conf".source = settingsFormat.generate "cockpit.conf" cfg.settings; + + security.pam.services.cockpit = {}; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; + + # units are in reverse sort order if you ls $out/lib/systemd/system + # all these units are basically verbatim translated from upstream + + # Translation from $out/lib/systemd/system/systemd-cockpithttps.slice + systemd.slices.system-cockpithttps = { + description = "Resource limits for all cockpit-ws-https@.service instances"; + sliceConfig = { + TasksMax = 200; + MemoryHigh = "75%"; + MemoryMax = "90%"; + }; + }; + + # Translation from $out/lib/systemd/system/cockpit-wsinstance-https@.socket + systemd.sockets."cockpit-wsinstance-https@" = { + unitConfig = { + Description = "Socket for Cockpit Web Service https instance %I"; + BindsTo = [ "cockpit.service" "cockpit-wsinstance-https@%i.service" ]; + # clean up the socket after the service exits, to prevent fd leak + # this also effectively prevents a DoS by starting arbitrarily many sockets, as + # the services are resource-limited by system-cockpithttps.slice + Documentation = "man:cockpit-ws(8)"; + }; + socketConfig = { + ListenStream = "/run/cockpit/wsinstance/https@%i.sock"; + SocketUser = "root"; + SocketMode = "0600"; + }; + }; + + # Translation from $out/lib/systemd/system/cockpit-wsinstance-https@.service + systemd.services."cockpit-wsinstance-https@" = { + description = "Cockpit Web Service https instance %I"; + bindsTo = [ "cockpit.service"]; + path = [ cfg.package ]; + documentation = [ "man:cockpit-ws(8)" ]; + serviceConfig = { + Slice = "system-cockpithttps.slice"; + ExecStart = "${cfg.package}/libexec/cockpit-ws --for-tls-proxy --port=0"; + User = "root"; + Group = ""; + }; + }; + + # Translation from $out/lib/systemd/system/cockpit-wsinstance-http.socket + systemd.sockets.cockpit-wsinstance-http = { + unitConfig = { + Description = "Socket for Cockpit Web Service http instance"; + BindsTo = "cockpit.service"; + Documentation = "man:cockpit-ws(8)"; + }; + socketConfig = { + ListenStream = "/run/cockpit/wsinstance/http.sock"; + SocketUser = "root"; + SocketMode = "0600"; + }; + }; + + # Translation from $out/lib/systemd/system/cockpit-wsinstance-https-factory.socket + systemd.sockets.cockpit-wsinstance-https-factory = { + unitConfig = { + Description = "Socket for Cockpit Web Service https instance factory"; + BindsTo = "cockpit.service"; + Documentation = "man:cockpit-ws(8)"; + }; + socketConfig = { + ListenStream = "/run/cockpit/wsinstance/https-factory.sock"; + Accept = true; + SocketUser = "root"; + SocketMode = "0600"; + }; + }; + + # Translation from $out/lib/systemd/system/cockpit-wsinstance-https-factory@.service + systemd.services."cockpit-wsinstance-https-factory@" = { + description = "Cockpit Web Service https instance factory"; + documentation = [ "man:cockpit-ws(8)" ]; + path = [ cfg.package ]; + serviceConfig = { + ExecStart = "${cfg.package}/libexec/cockpit-wsinstance-factory"; + User = "root"; + }; + }; + + # Translation from $out/lib/systemd/system/cockpit-wsinstance-http.service + systemd.services."cockpit-wsinstance-http" = { + description = "Cockpit Web Service http instance"; + bindsTo = [ "cockpit.service" ]; + path = [ cfg.package ]; + documentation = [ "man:cockpit-ws(8)" ]; + serviceConfig = { + ExecStart = "${cfg.package}/libexec/cockpit-ws --no-tls --port=0"; + User = "root"; + Group = ""; + }; + }; + + # Translation from $out/lib/systemd/system/cockpit.socket + systemd.sockets."cockpit" = { + unitConfig = { + Description = "Cockpit Web Service Socket"; + Documentation = "man:cockpit-ws(8)"; + Wants = "cockpit-motd.service"; + }; + socketConfig = { + ListenStream = cfg.port; + ExecStartPost = [ + "-${cfg.package}/share/cockpit/motd/update-motd \"\" localhost" + "-${pkgs.coreutils}/bin/ln -snf active.motd /run/cockpit/motd" + ]; + ExecStopPost = "-${pkgs.coreutils}/bin/ln -snf inactive.motd /run/cockpit/motd"; + }; + wantedBy = [ "sockets.target" ]; + }; + + # Translation from $out/lib/systemd/system/cockpit.service + systemd.services."cockpit" = { + description = "Cockpit Web Service"; + documentation = [ "man:cockpit-ws(8)" ]; + restartIfChanged = true; + path = with pkgs; [ coreutils cfg.package ]; + requires = [ "cockpit.socket" "cockpit-wsinstance-http.socket" "cockpit-wsinstance-https-factory.socket" ]; + after = [ "cockpit-wsinstance-http.socket" "cockpit-wsinstance-https-factory.socket" ]; + environment = { + G_MESSAGES_DEBUG = "cockpit-ws,cockpit-bridge"; + }; + serviceConfig = { + RuntimeDirectory="cockpit/tls"; + ExecStartPre = [ + # cockpit-tls runs in a more constrained environment, these + means that these commands + # will run with full privilege instead of inside that constrained environment + # See https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart= for details + "+${cfg.package}/libexec/cockpit-certificate-ensure --for-cockpit-tls" + ]; + ExecStart = "${cfg.package}/libexec/cockpit-tls"; + User = "root"; + Group = ""; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectKernelTunables = true; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + MemoryDenyWriteExecute = true; + }; + }; + + # Translation from $out/lib/systemd/system/cockpit-motd.service + # This part basically implements a motd state machine: + # - If cockpit.socket is enabled then /run/cockpit/motd points to /run/cockpit/active.motd + # - If cockpit.socket is disabled then /run/cockpit/motd points to /run/cockpit/inactive.motd + # - As cockpit.socket is disabled by default, /run/cockpit/motd points to /run/cockpit/inactive.motd + # /run/cockpit/active.motd is generated dynamically by cockpit-motd.service + systemd.services."cockpit-motd" = { + path = with pkgs; [ nettools ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${cfg.package}/share/cockpit/motd/update-motd"; + }; + description = "Cockpit motd updater service"; + documentation = [ "man:cockpit-ws(8)" ]; + wants = [ "network.target" ]; + after = [ "network.target" "cockpit.socket" ]; + }; + + systemd.tmpfiles.rules = [ # From $out/lib/tmpfiles.d/cockpit-tmpfiles.conf + "C /run/cockpit/inactive.motd 0640 root root - ${cfg.package}/share/cockpit/motd/inactive.motd" + "f /run/cockpit/active.motd 0640 root root -" + "L+ /run/cockpit/motd - - - - inactive.motd" + "d /etc/cockpit/ws-certs.d 0600 root root 0" + ]; + }; + + meta.maintainers = pkgs.cockpit.meta.maintainers; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/collectd.nix b/nixpkgs/nixos/modules/services/monitoring/collectd.nix new file mode 100644 index 000000000000..fe9b1214e5c1 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/collectd.nix @@ -0,0 +1,159 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.collectd; + + baseDirLine = ''BaseDir "${cfg.dataDir}"''; + unvalidated_conf = pkgs.writeText "collectd-unvalidated.conf" cfg.extraConfig; + + conf = if cfg.validateConfig then + pkgs.runCommand "collectd.conf" {} '' + echo testing ${unvalidated_conf} + cp ${unvalidated_conf} collectd.conf + # collectd -t fails if BaseDir does not exist. + substituteInPlace collectd.conf --replace ${lib.escapeShellArgs [ baseDirLine ]} 'BaseDir "."' + ${package}/bin/collectd -t -C collectd.conf + cp ${unvalidated_conf} $out + '' else unvalidated_conf; + + package = + if cfg.buildMinimalPackage + then minimalPackage + else cfg.package; + + minimalPackage = cfg.package.override { + enabledPlugins = [ "syslog" ] ++ builtins.attrNames cfg.plugins; + }; + +in { + options.services.collectd = with types; { + enable = mkEnableOption "collectd agent"; + + validateConfig = mkOption { + default = true; + description = '' + Validate the syntax of collectd configuration file at build time. + Disable this if you use the Include directive on files unavailable in + the build sandbox, or when cross-compiling. + ''; + type = types.bool; + }; + + package = mkPackageOption pkgs "collectd" { }; + + buildMinimalPackage = mkOption { + default = false; + description = '' + Build a minimal collectd package with only the configured `services.collectd.plugins` + ''; + type = bool; + }; + + user = mkOption { + default = "collectd"; + description = '' + User under which to run collectd. + ''; + type = nullOr str; + }; + + dataDir = mkOption { + default = "/var/lib/collectd"; + description = '' + Data directory for collectd agent. + ''; + type = path; + }; + + autoLoadPlugin = mkOption { + default = false; + description = '' + Enable plugin autoloading. + ''; + type = bool; + }; + + include = mkOption { + default = []; + description = '' + Additional paths to load config from. + ''; + type = listOf str; + }; + + plugins = mkOption { + default = {}; + example = { cpu = ""; memory = ""; network = "Server 192.168.1.1 25826"; }; + description = '' + Attribute set of plugin names to plugin config segments + ''; + type = attrsOf lines; + }; + + extraConfig = mkOption { + default = ""; + description = '' + Extra configuration for collectd. Use mkBefore to add lines before the + default config, and mkAfter to add them below. + ''; + type = lines; + }; + + }; + + config = mkIf cfg.enable { + # 1200 is after the default (1000) but before mkAfter (1500). + services.collectd.extraConfig = lib.mkOrder 1200 '' + ${baseDirLine} + AutoLoadPlugin ${boolToString cfg.autoLoadPlugin} + Hostname "${config.networking.hostName}" + + LoadPlugin syslog + <Plugin "syslog"> + LogLevel "info" + NotifyLevel "OKAY" + </Plugin> + + ${concatStrings (mapAttrsToList (plugin: pluginConfig: '' + LoadPlugin ${plugin} + <Plugin "${plugin}"> + ${pluginConfig} + </Plugin> + '') cfg.plugins)} + + ${concatMapStrings (f: '' + Include "${f}" + '') cfg.include} + ''; + + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' - ${cfg.user} - - -" + ]; + + systemd.services.collectd = { + description = "Collectd Monitoring Agent"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + ExecStart = "${package}/sbin/collectd -C ${conf} -f"; + User = cfg.user; + Restart = "on-failure"; + RestartSec = 3; + }; + }; + + users.users = optionalAttrs (cfg.user == "collectd") { + collectd = { + isSystemUser = true; + group = "collectd"; + }; + }; + + users.groups = optionalAttrs (cfg.user == "collectd") { + collectd = {}; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/das_watchdog.nix b/nixpkgs/nixos/modules/services/monitoring/das_watchdog.nix new file mode 100644 index 000000000000..88ca3a9227d2 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/das_watchdog.nix @@ -0,0 +1,34 @@ +# A general watchdog for the linux operating system that should run in the +# background at all times to ensure a realtime process won't hang the machine +{ config, lib, pkgs, ... }: + +with lib; + +let + + inherit (pkgs) das_watchdog; + +in { + ###### interface + + options = { + services.das_watchdog.enable = mkEnableOption "realtime watchdog"; + }; + + ###### implementation + + config = mkIf config.services.das_watchdog.enable { + environment.systemPackages = [ das_watchdog ]; + systemd.services.das_watchdog = { + description = "Watchdog to ensure a realtime process won't hang the machine"; + after = [ "multi-user.target" "sound.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = "root"; + Type = "simple"; + ExecStart = "${das_watchdog}/bin/das_watchdog"; + RemainAfterExit = true; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/datadog-agent.nix b/nixpkgs/nixos/modules/services/monitoring/datadog-agent.nix new file mode 100644 index 000000000000..5ac98bdf0382 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/datadog-agent.nix @@ -0,0 +1,299 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.datadog-agent; + + ddConf = { + skip_ssl_validation = false; + confd_path = "/etc/datadog-agent/conf.d"; + additional_checksd = "/etc/datadog-agent/checks.d"; + use_dogstatsd = true; + } + // optionalAttrs (cfg.logLevel != null) { log_level = cfg.logLevel; } + // optionalAttrs (cfg.hostname != null) { inherit (cfg) hostname; } + // optionalAttrs (cfg.ddUrl != null) { dd_url = cfg.ddUrl; } + // optionalAttrs (cfg.site != null) { site = cfg.site; } + // optionalAttrs (cfg.tags != null ) { tags = concatStringsSep ", " cfg.tags; } + // optionalAttrs (cfg.enableLiveProcessCollection) { process_config = { enabled = "true"; }; } + // optionalAttrs (cfg.enableTraceAgent) { apm_config = { enabled = true; }; } + // cfg.extraConfig; + + # Generate Datadog configuration files for each configured checks. + # This works because check configurations have predictable paths, + # and because JSON is a valid subset of YAML. + makeCheckConfigs = entries: mapAttrs' (name: conf: { + name = "datadog-agent/conf.d/${name}.d/conf.yaml"; + value.source = pkgs.writeText "${name}-check-conf.yaml" (builtins.toJSON conf); + }) entries; + + defaultChecks = { + disk = cfg.diskCheck; + network = cfg.networkCheck; + }; + + # Assemble all check configurations and the top-level agent + # configuration. + etcfiles = with pkgs; with builtins; + { "datadog-agent/datadog.yaml" = { + source = writeText "datadog.yaml" (toJSON ddConf); + }; + } // makeCheckConfigs (cfg.checks // defaultChecks); + + # Apply the configured extraIntegrations to the provided agent + # package. See the documentation of `dd-agent/integrations-core.nix` + # for detailed information on this. + datadogPkg = cfg.package.override { + pythonPackages = pkgs.datadog-integrations-core cfg.extraIntegrations; + }; +in { + options.services.datadog-agent = { + enable = mkEnableOption "Datadog-agent v7 monitoring service"; + + package = mkPackageOption pkgs "datadog-agent" { + extraDescription = '' + ::: {.note} + The provided package is expected to have an overridable `pythonPackages`-attribute + which configures the Python environment with the Datadog checks. + ::: + ''; + }; + + apiKeyFile = mkOption { + description = '' + Path to a file containing the Datadog API key to associate the + agent with your account. + ''; + example = "/run/keys/datadog_api_key"; + type = types.path; + }; + + ddUrl = mkOption { + description = '' + Custom dd_url to configure the agent with. Useful if traffic to datadog + needs to go through a proxy. + Don't use this to point to another datadog site (EU) - use site instead. + ''; + default = null; + example = "http://haproxy.example.com:3834"; + type = types.nullOr types.str; + }; + + site = mkOption { + description = '' + The datadog site to point the agent towards. + Set to datadoghq.eu to point it to their EU site. + ''; + default = null; + example = "datadoghq.eu"; + type = types.nullOr types.str; + }; + + tags = mkOption { + description = "The tags to mark this Datadog agent"; + example = [ "test" "service" ]; + default = null; + type = types.nullOr (types.listOf types.str); + }; + + hostname = mkOption { + description = "The hostname to show in the Datadog dashboard (optional)"; + default = null; + example = "mymachine.mydomain"; + type = types.nullOr types.str; + }; + + logLevel = mkOption { + description = "Logging verbosity."; + default = null; + type = types.nullOr (types.enum ["DEBUG" "INFO" "WARN" "ERROR"]); + }; + + extraIntegrations = mkOption { + default = {}; + type = types.attrs; + + description = '' + Extra integrations from the Datadog core-integrations + repository that should be built and included. + + By default the included integrations are disk, mongo, network, + nginx and postgres. + + To include additional integrations the name of the derivation + and a function to filter its dependencies from the Python + package set must be provided. + ''; + + example = literalExpression '' + { + ntp = pythonPackages: [ pythonPackages.ntplib ]; + } + ''; + }; + + extraConfig = mkOption { + default = {}; + type = types.attrs; + description = '' + Extra configuration options that will be merged into the + main config file {file}`datadog.yaml`. + ''; + }; + + enableLiveProcessCollection = mkOption { + description = '' + Whether to enable the live process collection agent. + ''; + default = false; + type = types.bool; + }; + + processAgentPackage = mkOption { + default = pkgs.datadog-process-agent; + defaultText = literalExpression "pkgs.datadog-process-agent"; + description = '' + Which DataDog v7 agent package to use. Note that the provided + package is expected to have an overridable `pythonPackages`-attribute + which configures the Python environment with the Datadog + checks. + ''; + type = types.package; + }; + + enableTraceAgent = mkOption { + description = '' + Whether to enable the trace agent. + ''; + default = false; + type = types.bool; + }; + + checks = mkOption { + description = '' + Configuration for all Datadog checks. Keys of this attribute + set will be used as the name of the check to create the + appropriate configuration in `conf.d/$check.d/conf.yaml`. + + The configuration is converted into JSON from the plain Nix + language configuration, meaning that you should write + configuration adhering to Datadog's documentation - but in Nix + language. + + Refer to the implementation of this module (specifically the + definition of `defaultChecks`) for an example. + + Note: The 'disk' and 'network' check are configured in + separate options because they exist by default. Attempting to + override their configuration here will have no effect. + ''; + + example = { + http_check = { + init_config = null; # sic! + instances = [ + { + name = "some-service"; + url = "http://localhost:1337/healthz"; + tags = [ "some-service" ]; + } + ]; + }; + }; + + default = {}; + + # sic! The structure of the values is up to the check, so we can + # not usefully constrain the type further. + type = with types; attrsOf attrs; + }; + + diskCheck = mkOption { + description = "Disk check config"; + type = types.attrs; + default = { + init_config = {}; + instances = [ { use_mount = "false"; } ]; + }; + }; + + networkCheck = mkOption { + description = "Network check config"; + type = types.attrs; + default = { + init_config = {}; + # Network check only supports one configured instance + instances = [ { collect_connection_state = false; + excluded_interfaces = [ "lo" "lo0" ]; } ]; + }; + }; + }; + config = mkIf cfg.enable { + environment.systemPackages = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute2 ]; + + users.users.datadog = { + description = "Datadog Agent User"; + uid = config.ids.uids.datadog; + group = "datadog"; + home = "/var/log/datadog/"; + createHome = true; + }; + + users.groups.datadog.gid = config.ids.gids.datadog; + + systemd.services = let + makeService = attrs: recursiveUpdate { + path = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute2 ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + User = "datadog"; + Group = "datadog"; + Restart = "always"; + RestartSec = 2; + }; + restartTriggers = [ datadogPkg ] ++ map (x: x.source) (attrValues etcfiles); + } attrs; + in { + datadog-agent = makeService { + description = "Datadog agent monitor"; + preStart = '' + chown -R datadog: /etc/datadog-agent + rm -f /etc/datadog-agent/auth_token + ''; + script = '' + export DD_API_KEY=$(head -n 1 ${cfg.apiKeyFile}) + exec ${datadogPkg}/bin/agent run -c /etc/datadog-agent/datadog.yaml + ''; + serviceConfig.PermissionsStartOnly = true; + }; + + dd-jmxfetch = lib.mkIf (lib.hasAttr "jmx" cfg.checks) (makeService { + description = "Datadog JMX Fetcher"; + path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps pkgs.jdk ]; + serviceConfig.ExecStart = "${datadogPkg}/bin/dd-jmxfetch"; + }); + + datadog-process-agent = lib.mkIf cfg.enableLiveProcessCollection (makeService { + description = "Datadog Live Process Agent"; + path = [ ]; + script = '' + export DD_API_KEY=$(head -n 1 ${cfg.apiKeyFile}) + ${cfg.processAgentPackage}/bin/process-agent --config /etc/datadog-agent/datadog.yaml + ''; + }); + + datadog-trace-agent = lib.mkIf cfg.enableTraceAgent (makeService { + description = "Datadog Trace Agent"; + path = [ ]; + script = '' + export DD_API_KEY=$(head -n 1 ${cfg.apiKeyFile}) + ${datadogPkg}/bin/trace-agent -config /etc/datadog-agent/datadog.yaml + ''; + }); + + }; + + environment.etc = etcfiles; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/do-agent.nix b/nixpkgs/nixos/modules/services/monitoring/do-agent.nix new file mode 100644 index 000000000000..4dfb6236727b --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/do-agent.nix @@ -0,0 +1,25 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.do-agent; + +in +{ + options.services.do-agent = { + enable = mkEnableOption "do-agent, the DigitalOcean droplet metrics agent"; + }; + + config = mkIf cfg.enable { + systemd.packages = [ pkgs.do-agent ]; + + systemd.services.do-agent = { + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = [ "" "${pkgs.do-agent}/bin/do-agent --syslog" ]; + DynamicUser = true; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/fusion-inventory.nix b/nixpkgs/nixos/modules/services/monitoring/fusion-inventory.nix new file mode 100644 index 000000000000..9b65c76ce02e --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/fusion-inventory.nix @@ -0,0 +1,63 @@ +# Fusion Inventory daemon. +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.fusionInventory; + + configFile = pkgs.writeText "fusion_inventory.conf" '' + server = ${concatStringsSep ", " cfg.servers} + + logger = stderr + + ${cfg.extraConfig} + ''; + +in { + + ###### interface + + options = { + + services.fusionInventory = { + + enable = mkEnableOption "Fusion Inventory Agent"; + + servers = mkOption { + type = types.listOf types.str; + description = '' + The urls of the OCS/GLPI servers to connect to. + ''; + }; + + extraConfig = mkOption { + default = ""; + type = types.lines; + description = '' + Configuration that is injected verbatim into the configuration file. + ''; + }; + }; + }; + + + ###### implementation + + config = mkIf cfg.enable { + + users.users.fusion-inventory = { + description = "FusionInventory user"; + isSystemUser = true; + }; + + systemd.services.fusion-inventory = { + description = "Fusion Inventory Agent"; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + ExecStart = "${pkgs.fusionInventory}/bin/fusioninventory-agent --conf-file=${configFile} --daemon --no-fork"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/goss.md b/nixpkgs/nixos/modules/services/monitoring/goss.md new file mode 100644 index 000000000000..bf91d42011fa --- /dev/null +++ b/nixpkgs/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: + +```nix +{ + 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/nixpkgs/nixos/modules/services/monitoring/goss.nix b/nixpkgs/nixos/modules/services/monitoring/goss.nix new file mode 100644 index 000000000000..00246752a7cd --- /dev/null +++ b/nixpkgs/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 "Goss daemon"; + + package = lib.mkPackageOption pkgs "goss" { }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + example = { + GOSS_FMT = "json"; + GOSS_LOGLEVEL = "FATAL"; + GOSS_LISTEN = ":8080"; + }; + description = '' + 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 = '' + 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/nixpkgs/nixos/modules/services/monitoring/grafana-agent.nix b/nixpkgs/nixos/modules/services/monitoring/grafana-agent.nix new file mode 100644 index 000000000000..655ec8ded1e0 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/grafana-agent.nix @@ -0,0 +1,163 @@ +{ lib, pkgs, config, generators, ... }: +with lib; +let + cfg = config.services.grafana-agent; + settingsFormat = pkgs.formats.yaml { }; + configFile = settingsFormat.generate "grafana-agent.yaml" cfg.settings; +in +{ + meta = { + maintainers = with maintainers; [ flokli zimbatm ]; + }; + + options.services.grafana-agent = { + enable = mkEnableOption "grafana-agent"; + + package = mkPackageOption pkgs "grafana-agent" { }; + + credentials = mkOption { + description = '' + Credentials to load at service startup. Keys that are UPPER_SNAKE will be loaded as env vars. Values are absolute paths to the credentials. + ''; + type = types.attrsOf types.str; + default = { }; + + example = { + logs_remote_write_password = "/run/keys/grafana_agent_logs_remote_write_password"; + LOGS_REMOTE_WRITE_URL = "/run/keys/grafana_agent_logs_remote_write_url"; + LOGS_REMOTE_WRITE_USERNAME = "/run/keys/grafana_agent_logs_remote_write_username"; + metrics_remote_write_password = "/run/keys/grafana_agent_metrics_remote_write_password"; + METRICS_REMOTE_WRITE_URL = "/run/keys/grafana_agent_metrics_remote_write_url"; + METRICS_REMOTE_WRITE_USERNAME = "/run/keys/grafana_agent_metrics_remote_write_username"; + }; + }; + + extraFlags = mkOption { + type = with types; listOf str; + default = [ ]; + example = [ "-enable-features=integrations-next" "-disable-reporting" ]; + description = '' + Extra command-line flags passed to {command}`grafana-agent`. + + See <https://grafana.com/docs/agent/latest/static/configuration/flags/> + ''; + }; + + settings = mkOption { + description = '' + Configuration for {command}`grafana-agent`. + + See <https://grafana.com/docs/agent/latest/configuration/> + ''; + + type = types.submodule { + freeformType = settingsFormat.type; + }; + + default = { }; + defaultText = lib.literalExpression '' + { + metrics = { + wal_directory = "\''${STATE_DIRECTORY}"; + global.scrape_interval = "5s"; + }; + integrations = { + agent.enabled = true; + agent.scrape_integration = true; + node_exporter.enabled = true; + }; + } + ''; + example = { + metrics.global.remote_write = [{ + url = "\${METRICS_REMOTE_WRITE_URL}"; + basic_auth.username = "\${METRICS_REMOTE_WRITE_USERNAME}"; + basic_auth.password_file = "\${CREDENTIALS_DIRECTORY}/metrics_remote_write_password"; + }]; + logs.configs = [{ + name = "default"; + scrape_configs = [ + { + job_name = "journal"; + journal = { + max_age = "12h"; + labels.job = "systemd-journal"; + }; + relabel_configs = [ + { + source_labels = [ "__journal__systemd_unit" ]; + target_label = "systemd_unit"; + } + { + source_labels = [ "__journal__hostname" ]; + target_label = "nodename"; + } + { + source_labels = [ "__journal_syslog_identifier" ]; + target_label = "syslog_identifier"; + } + ]; + } + ]; + positions.filename = "\${STATE_DIRECTORY}/loki_positions.yaml"; + clients = [{ + url = "\${LOGS_REMOTE_WRITE_URL}"; + basic_auth.username = "\${LOGS_REMOTE_WRITE_USERNAME}"; + basic_auth.password_file = "\${CREDENTIALS_DIRECTORY}/logs_remote_write_password"; + }]; + }]; + }; + }; + }; + + config = mkIf cfg.enable { + services.grafana-agent.settings = { + # keep this in sync with config.services.grafana-agent.settings.defaultText. + metrics = { + wal_directory = mkDefault "\${STATE_DIRECTORY}"; + global.scrape_interval = mkDefault "5s"; + }; + integrations = { + agent.enabled = mkDefault true; + agent.scrape_integration = mkDefault true; + node_exporter.enabled = mkDefault true; + }; + }; + + systemd.services.grafana-agent = { + wantedBy = [ "multi-user.target" ]; + script = '' + set -euo pipefail + shopt -u nullglob + + # Load all credentials into env if they are in UPPER_SNAKE form. + if [[ -n "''${CREDENTIALS_DIRECTORY:-}" ]]; then + for file in "$CREDENTIALS_DIRECTORY"/*; do + key=$(basename "$file") + if [[ $key =~ ^[A-Z0-9_]+$ ]]; then + echo "Environ $key" + export "$key=$(< "$file")" + fi + done + fi + + # We can't use Environment=HOSTNAME=%H, as it doesn't include the domain part. + export HOSTNAME=$(< /proc/sys/kernel/hostname) + + exec ${lib.getExe cfg.package} -config.expand-env -config.file ${configFile} ${escapeShellArgs cfg.extraFlags} + ''; + serviceConfig = { + Restart = "always"; + DynamicUser = true; + RestartSec = 2; + SupplementaryGroups = [ + # allow to read the systemd journal for loki log forwarding + "systemd-journal" + ]; + StateDirectory = "grafana-agent"; + LoadCredential = lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials; + Type = "simple"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixpkgs/nixos/modules/services/monitoring/grafana-image-renderer.nix new file mode 100644 index 000000000000..e06720b15302 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/grafana-image-renderer.nix @@ -0,0 +1,148 @@ +{ lib, pkgs, config, ... }: + +with lib; + +let + cfg = config.services.grafana-image-renderer; + + format = pkgs.formats.json { }; + + configFile = format.generate "grafana-image-renderer-config.json" cfg.settings; +in { + options.services.grafana-image-renderer = { + enable = mkEnableOption "grafana-image-renderer"; + + chromium = mkOption { + type = types.package; + description = '' + The chromium to use for image rendering. + ''; + }; + + verbose = mkEnableOption "verbosity for the service"; + + provisionGrafana = mkEnableOption "Grafana configuration for grafana-image-renderer"; + + settings = mkOption { + type = types.submodule { + freeformType = format.type; + + options = { + service = { + port = mkOption { + type = types.port; + default = 8081; + description = '' + The TCP port to use for the rendering server. + ''; + }; + logging.level = mkOption { + type = types.enum [ "error" "warning" "info" "debug" ]; + default = "info"; + description = '' + The log-level of the {file}`grafana-image-renderer.service`-unit. + ''; + }; + }; + rendering = { + width = mkOption { + default = 1000; + type = types.ints.positive; + description = '' + Width of the PNG used to display the alerting graph. + ''; + }; + height = mkOption { + default = 500; + type = types.ints.positive; + description = '' + Height of the PNG used to display the alerting graph. + ''; + }; + mode = mkOption { + default = "default"; + type = types.enum [ "default" "reusable" "clustered" ]; + description = '' + Rendering mode of `grafana-image-renderer`: + + - `default:` Creates on browser-instance + per rendering request. + - `reusable:` One browser instance + will be started and reused for each rendering request. + - `clustered:` allows to precisely + configure how many browser-instances are supposed to be used. The values + for that mode can be declared in `rendering.clustering`. + ''; + }; + args = mkOption { + type = types.listOf types.str; + default = [ "--no-sandbox" ]; + description = '' + List of CLI flags passed to `chromium`. + ''; + }; + }; + }; + }; + + default = {}; + + description = '' + Configuration attributes for `grafana-image-renderer`. + + See <https://github.com/grafana/grafana-image-renderer/blob/ce1f81438e5f69c7fd7c73ce08bab624c4c92e25/default.json> + for supported values. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { assertion = cfg.provisionGrafana -> config.services.grafana.enable; + message = '' + To provision a Grafana instance to use grafana-image-renderer, + `services.grafana.enable` must be set to `true`! + ''; + } + ]; + + services.grafana.settings.rendering = mkIf cfg.provisionGrafana { + server_url = "http://localhost:${toString cfg.settings.service.port}/render"; + 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; + + services.grafana-image-renderer.settings = { + rendering = mapAttrs (const mkDefault) { + chromeBin = "${cfg.chromium}/bin/chromium"; + verboseLogging = cfg.verbose; + timezone = config.time.timeZone; + }; + + service = { + logging.level = mkIf cfg.verbose (mkDefault "debug"); + metrics.enabled = mkDefault false; + }; + }; + + systemd.services.grafana-image-renderer = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + description = " A Grafana backend plugin that handles rendering of panels & dashboards to PNGs using headless browser (Chromium/Chrome)"; + + environment = { + PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = "true"; + }; + + serviceConfig = { + DynamicUser = true; + PrivateTmp = true; + ExecStart = "${pkgs.grafana-image-renderer}/bin/grafana-image-renderer server --config=${configFile}"; + Restart = "always"; + }; + }; + }; + + meta.maintainers = with maintainers; [ ma27 ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/grafana-reporter.nix b/nixpkgs/nixos/modules/services/monitoring/grafana-reporter.nix new file mode 100644 index 000000000000..340ab7abd19b --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/grafana-reporter.nix @@ -0,0 +1,67 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.grafana_reporter; + +in { + options.services.grafana_reporter = { + enable = mkEnableOption "grafana_reporter"; + + grafana = { + protocol = mkOption { + description = "Grafana protocol."; + default = "http"; + type = types.enum ["http" "https"]; + }; + addr = mkOption { + description = "Grafana address."; + default = "127.0.0.1"; + type = types.str; + }; + port = mkOption { + description = "Grafana port."; + default = 3000; + type = types.port; + }; + + }; + addr = mkOption { + description = "Listening address."; + default = "127.0.0.1"; + type = types.str; + }; + + port = mkOption { + description = "Listening port."; + default = 8686; + type = types.port; + }; + + templateDir = mkOption { + description = "Optional template directory to use custom tex templates"; + default = pkgs.grafana_reporter; + defaultText = literalExpression "pkgs.grafana_reporter"; + type = types.either types.str types.path; + }; + }; + + config = mkIf cfg.enable { + systemd.services.grafana_reporter = { + description = "Grafana Reporter Service Daemon"; + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + serviceConfig = let + args = lib.concatStringsSep " " [ + "-proto ${cfg.grafana.protocol}://" + "-ip ${cfg.grafana.addr}:${toString cfg.grafana.port}" + "-port :${toString cfg.port}" + "-templates ${cfg.templateDir}" + ]; + in { + ExecStart = "${pkgs.grafana_reporter}/bin/grafana-reporter ${args}"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/grafana.nix b/nixpkgs/nixos/modules/services/monitoring/grafana.nix new file mode 100644 index 000000000000..9d453c539482 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/grafana.nix @@ -0,0 +1,1888 @@ +{ options, config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.grafana; + opt = options.services.grafana; + provisioningSettingsFormat = pkgs.formats.yaml { }; + declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins); + useMysql = cfg.settings.database.type == "mysql"; + usePostgresql = cfg.settings.database.type == "postgres"; + + # Prefer using the values from the default config file[0] directly. This way, + # people reading the NixOS manual can see them without cross-referencing the + # official documentation. + # + # However, if there is no default entry or if the setting is optional, use + # `null` as the default value. It will be turned into the empty string. + # + # If a setting is a list, always allow setting it as a plain string as well. + # + # [0]: https://github.com/grafana/grafana/blob/main/conf/defaults.ini + settingsFormatIni = pkgs.formats.ini { + listToValue = concatMapStringsSep " " (generators.mkValueStringDefault { }); + mkKeyValue = generators.mkKeyValueDefault + { + mkValueString = v: + if v == null then "" + else generators.mkValueStringDefault { } v; + } + "="; + }; + configFile = settingsFormatIni.generate "config.ini" cfg.settings; + + mkProvisionCfg = name: attr: provisionCfg: + if provisionCfg.path != null + then provisionCfg.path + else + provisioningSettingsFormat.generate "${name}.yaml" + (if provisionCfg.settings != null + then provisionCfg.settings + else { + apiVersion = 1; + ${attr} = [ ]; + }); + + datasourceFileOrDir = mkProvisionCfg "datasource" "datasources" cfg.provision.datasources; + dashboardFileOrDir = mkProvisionCfg "dashboard" "providers" cfg.provision.dashboards; + + notifierConfiguration = { + apiVersion = 1; + notifiers = cfg.provision.notifiers; + }; + + notifierFileOrDir = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration); + + generateAlertingProvisioningYaml = x: + if (cfg.provision.alerting."${x}".path == null) + then provisioningSettingsFormat.generate "${x}.yaml" cfg.provision.alerting."${x}".settings + else cfg.provision.alerting."${x}".path; + rulesFileOrDir = generateAlertingProvisioningYaml "rules"; + contactPointsFileOrDir = generateAlertingProvisioningYaml "contactPoints"; + policiesFileOrDir = generateAlertingProvisioningYaml "policies"; + templatesFileOrDir = generateAlertingProvisioningYaml "templates"; + muteTimingsFileOrDir = generateAlertingProvisioningYaml "muteTimings"; + + ln = { src, dir, filename }: '' + if [[ -d "${src}" ]]; then + pushd $out/${dir} &>/dev/null + lndir "${src}" + popd &>/dev/null + else + ln -sf ${src} $out/${dir}/${filename}.yaml + fi + ''; + provisionConfDir = pkgs.runCommand "grafana-provisioning" { nativeBuildInputs = [ pkgs.xorg.lndir ]; } '' + mkdir -p $out/{alerting,datasources,dashboards,notifiers,plugins} + ${ln { src = datasourceFileOrDir; dir = "datasources"; filename = "datasource"; }} + ${ln { src = dashboardFileOrDir; dir = "dashboards"; filename = "dashboard"; }} + ${ln { src = notifierFileOrDir; dir = "notifiers"; filename = "notifier"; }} + ${ln { src = rulesFileOrDir; dir = "alerting"; filename = "rules"; }} + ${ln { src = contactPointsFileOrDir; dir = "alerting"; filename = "contactPoints"; }} + ${ln { src = policiesFileOrDir; dir = "alerting"; filename = "policies"; }} + ${ln { src = templatesFileOrDir; dir = "alerting"; filename = "templates"; }} + ${ln { src = muteTimingsFileOrDir; dir = "alerting"; filename = "muteTimings"; }} + ''; + + # Get a submodule without any embedded metadata: + _filter = x: filterAttrs (k: v: k != "_module") x; + + # https://grafana.com/docs/grafana/latest/administration/provisioning/#datasources + grafanaTypes.datasourceConfig = types.submodule { + freeformType = provisioningSettingsFormat.type; + + options = { + name = mkOption { + type = types.str; + description = "Name of the datasource. Required."; + }; + type = mkOption { + type = types.str; + description = "Datasource type. Required."; + }; + access = mkOption { + type = types.enum [ "proxy" "direct" ]; + default = "proxy"; + description = "Access mode. proxy or direct (Server or Browser in the UI). Required."; + }; + uid = mkOption { + type = types.nullOr types.str; + default = null; + description = "Custom UID which can be used to reference this datasource in other parts of the configuration, if not specified will be generated automatically."; + }; + url = mkOption { + type = types.str; + default = "localhost"; + description = "Url of the datasource."; + }; + editable = mkOption { + type = types.bool; + default = false; + description = "Allow users to edit datasources from the UI."; + }; + jsonData = mkOption { + type = types.nullOr types.attrs; + default = null; + description = "Extra data for datasource plugins."; + }; + secureJsonData = mkOption { + type = types.nullOr types.attrs; + default = null; + description = '' + Datasource specific secure configuration. Please note that the contents of this option + will end up in a world-readable Nix store. Use the file provider + pointing at a reasonably secured file in the local filesystem + to work around that. Look at the documentation for details: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; + }; + }; + }; + + # https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards + grafanaTypes.dashboardConfig = types.submodule { + freeformType = provisioningSettingsFormat.type; + + options = { + name = mkOption { + type = types.str; + default = "default"; + description = "A unique provider name."; + }; + type = mkOption { + type = types.str; + default = "file"; + description = "Dashboard provider type."; + }; + options.path = mkOption { + type = types.path; + description = "Path grafana will watch for dashboards. Required when using the 'file' type."; + }; + }; + }; + + grafanaTypes.notifierConfig = types.submodule { + options = { + name = mkOption { + type = types.str; + default = "default"; + description = "Notifier name."; + }; + type = mkOption { + type = types.enum [ "dingding" "discord" "email" "googlechat" "hipchat" "kafka" "line" "teams" "opsgenie" "pagerduty" "prometheus-alertmanager" "pushover" "sensu" "sensugo" "slack" "telegram" "threema" "victorops" "webhook" ]; + description = "Notifier type."; + }; + uid = mkOption { + type = types.str; + description = "Unique notifier identifier."; + }; + org_id = mkOption { + type = types.int; + default = 1; + description = "Organization ID."; + }; + org_name = mkOption { + type = types.str; + default = "Main Org."; + description = "Organization name."; + }; + is_default = mkOption { + type = types.bool; + description = "Is the default notifier."; + default = false; + }; + send_reminder = mkOption { + type = types.bool; + default = true; + description = "Should the notifier be sent reminder notifications while alerts continue to fire."; + }; + frequency = mkOption { + type = types.str; + default = "5m"; + description = "How frequently should the notifier be sent reminders."; + }; + disable_resolve_message = mkOption { + type = types.bool; + default = false; + description = "Turn off the message that sends when an alert returns to OK."; + }; + settings = mkOption { + type = types.nullOr types.attrs; + default = null; + description = "Settings for the notifier type."; + }; + secure_settings = mkOption { + type = types.nullOr types.attrs; + default = null; + description = '' + Secure settings for the notifier type. Please note that the contents of this option + will end up in a world-readable Nix store. Use the file provider + pointing at a reasonably secured file in the local filesystem + to work around that. Look at the documentation for details: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; + }; + }; + }; +in +{ + imports = [ + (mkRenamedOptionModule [ "services" "grafana" "protocol" ] [ "services" "grafana" "settings" "server" "protocol" ]) + (mkRenamedOptionModule [ "services" "grafana" "addr" ] [ "services" "grafana" "settings" "server" "http_addr" ]) + (mkRenamedOptionModule [ "services" "grafana" "port" ] [ "services" "grafana" "settings" "server" "http_port" ]) + (mkRenamedOptionModule [ "services" "grafana" "domain" ] [ "services" "grafana" "settings" "server" "domain" ]) + (mkRenamedOptionModule [ "services" "grafana" "rootUrl" ] [ "services" "grafana" "settings" "server" "root_url" ]) + (mkRenamedOptionModule [ "services" "grafana" "staticRootPath" ] [ "services" "grafana" "settings" "server" "static_root_path" ]) + (mkRenamedOptionModule [ "services" "grafana" "certFile" ] [ "services" "grafana" "settings" "server" "cert_file" ]) + (mkRenamedOptionModule [ "services" "grafana" "certKey" ] [ "services" "grafana" "settings" "server" "cert_key" ]) + (mkRenamedOptionModule [ "services" "grafana" "socket" ] [ "services" "grafana" "settings" "server" "socket" ]) + (mkRenamedOptionModule [ "services" "grafana" "database" "type" ] [ "services" "grafana" "settings" "database" "type" ]) + (mkRenamedOptionModule [ "services" "grafana" "database" "host" ] [ "services" "grafana" "settings" "database" "host" ]) + (mkRenamedOptionModule [ "services" "grafana" "database" "name" ] [ "services" "grafana" "settings" "database" "name" ]) + (mkRenamedOptionModule [ "services" "grafana" "database" "user" ] [ "services" "grafana" "settings" "database" "user" ]) + (mkRenamedOptionModule [ "services" "grafana" "database" "password" ] [ "services" "grafana" "settings" "database" "password" ]) + (mkRenamedOptionModule [ "services" "grafana" "database" "path" ] [ "services" "grafana" "settings" "database" "path" ]) + (mkRenamedOptionModule [ "services" "grafana" "database" "connMaxLifetime" ] [ "services" "grafana" "settings" "database" "conn_max_lifetime" ]) + (mkRenamedOptionModule [ "services" "grafana" "security" "adminUser" ] [ "services" "grafana" "settings" "security" "admin_user" ]) + (mkRenamedOptionModule [ "services" "grafana" "security" "adminPassword" ] [ "services" "grafana" "settings" "security" "admin_password" ]) + (mkRenamedOptionModule [ "services" "grafana" "security" "secretKey" ] [ "services" "grafana" "settings" "security" "secret_key" ]) + (mkRenamedOptionModule [ "services" "grafana" "server" "serveFromSubPath" ] [ "services" "grafana" "settings" "server" "serve_from_sub_path" ]) + (mkRenamedOptionModule [ "services" "grafana" "smtp" "enable" ] [ "services" "grafana" "settings" "smtp" "enabled" ]) + (mkRenamedOptionModule [ "services" "grafana" "smtp" "user" ] [ "services" "grafana" "settings" "smtp" "user" ]) + (mkRenamedOptionModule [ "services" "grafana" "smtp" "password" ] [ "services" "grafana" "settings" "smtp" "password" ]) + (mkRenamedOptionModule [ "services" "grafana" "smtp" "fromAddress" ] [ "services" "grafana" "settings" "smtp" "from_address" ]) + (mkRenamedOptionModule [ "services" "grafana" "users" "allowSignUp" ] [ "services" "grafana" "settings" "users" "allow_sign_up" ]) + (mkRenamedOptionModule [ "services" "grafana" "users" "allowOrgCreate" ] [ "services" "grafana" "settings" "users" "allow_org_create" ]) + (mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrg" ] [ "services" "grafana" "settings" "users" "auto_assign_org" ]) + (mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrgRole" ] [ "services" "grafana" "settings" "users" "auto_assign_org_role" ]) + (mkRenamedOptionModule [ "services" "grafana" "auth" "disableLoginForm" ] [ "services" "grafana" "settings" "auth" "disable_login_form" ]) + (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "enable" ] [ "services" "grafana" "settings" "auth.anonymous" "enabled" ]) + (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_name" ] [ "services" "grafana" "settings" "auth.anonymous" "org_name" ]) + (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_role" ] [ "services" "grafana" "settings" "auth.anonymous" "org_role" ]) + (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "enable" ] [ "services" "grafana" "settings" "auth.azuread" "enabled" ]) + (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowSignUp" ] [ "services" "grafana" "settings" "auth.azuread" "allow_sign_up" ]) + (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "clientId" ] [ "services" "grafana" "settings" "auth.azuread" "client_id" ]) + (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedDomains" ] [ "services" "grafana" "settings" "auth.azuread" "allowed_domains" ]) + (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedGroups" ] [ "services" "grafana" "settings" "auth.azuread" "allowed_groups" ]) + (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "enable" ] [ "services" "grafana" "settings" "auth.google" "enabled" ]) + (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "allowSignUp" ] [ "services" "grafana" "settings" "auth.google" "allow_sign_up" ]) + (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "clientId" ] [ "services" "grafana" "settings" "auth.google" "client_id" ]) + (mkRenamedOptionModule [ "services" "grafana" "analytics" "reporting" "enable" ] [ "services" "grafana" "settings" "analytics" "reporting_enabled" ]) + + (mkRemovedOptionModule [ "services" "grafana" "database" "passwordFile" ] '' + This option has been removed. Use 'services.grafana.settings.database.password' with file provider instead. + '') + (mkRemovedOptionModule [ "services" "grafana" "security" "adminPasswordFile" ] '' + This option has been removed. Use 'services.grafana.settings.security.admin_password' with file provider instead. + '') + (mkRemovedOptionModule [ "services" "grafana" "security" "secretKeyFile" ] '' + This option has been removed. Use 'services.grafana.settings.security.secret_key' with file provider instead. + '') + (mkRemovedOptionModule [ "services" "grafana" "smtp" "passwordFile" ] '' + This option has been removed. Use 'services.grafana.settings.smtp.password' with file provider instead. + '') + (mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "clientSecretFile" ] '' + This option has been removed. Use 'services.grafana.settings.azuread.client_secret' with file provider instead. + '') + (mkRemovedOptionModule [ "services" "grafana" "auth" "google" "clientSecretFile" ] '' + This option has been removed. Use 'services.grafana.settings.google.client_secret' with file provider instead. + '') + (mkRemovedOptionModule [ "services" "grafana" "extraOptions" ] '' + This option has been removed. Use 'services.grafana.settings' instead. For a detailed migration guide, please + review the release notes of NixOS 22.11. + '') + + (mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "tenantId" ] "This option has been deprecated upstream.") + ]; + + options.services.grafana = { + enable = mkEnableOption "grafana"; + + declarativePlugins = mkOption { + type = with types; nullOr (listOf path); + default = null; + description = "If non-null, then a list of packages containing Grafana plugins to install. If set, plugins cannot be manually installed."; + example = literalExpression "with pkgs.grafanaPlugins; [ grafana-piechart-panel ]"; + # Make sure each plugin is added only once; otherwise building + # the link farm fails, since the same path is added multiple + # times. + apply = x: if isList x then lib.unique x else x; + }; + + package = mkPackageOption pkgs "grafana" { }; + + dataDir = mkOption { + description = "Data directory."; + default = "/var/lib/grafana"; + type = types.path; + }; + + settings = mkOption { + description = '' + Grafana settings. See <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/> + for available options. INI format is used. + ''; + type = types.submodule { + freeformType = settingsFormatIni.type; + + options = { + paths = { + plugins = mkOption { + description = "Directory where grafana will automatically scan and look for plugins"; + default = if (cfg.declarativePlugins == null) then "${cfg.dataDir}/plugins" else declarativePlugins; + defaultText = literalExpression "if (cfg.declarativePlugins == null) then \"\${cfg.dataDir}/plugins\" else declarativePlugins"; + type = types.path; + }; + + provisioning = mkOption { + description = '' + Folder that contains provisioning config files that grafana will apply on startup and while running. + Don't change the value of this option if you are planning to use `services.grafana.provision` options. + ''; + default = provisionConfDir; + defaultText = "directory with links to files generated from services.grafana.provision"; + type = types.path; + }; + }; + + server = { + protocol = mkOption { + description = "Which protocol to listen."; + default = "http"; + type = types.enum [ "http" "https" "h2" "socket" ]; + }; + + http_addr = mkOption { + type = types.str; + default = "127.0.0.1"; + description = '' + Listening address. + + ::: {.note} + This setting intentionally varies from upstream's default to be a bit more secure by default. + ::: + ''; + }; + + http_port = mkOption { + description = "Listening port."; + default = 3000; + type = types.port; + }; + + domain = mkOption { + description = '' + The public facing domain name used to access grafana from a browser. + + This setting is only used in the default value of the `root_url` setting. + If you set the latter manually, this option does not have to be specified. + ''; + default = "localhost"; + type = types.str; + }; + + enforce_domain = mkOption { + description = '' + Redirect to correct domain if the host header does not match the domain. + Prevents DNS rebinding attacks. + ''; + default = false; + type = types.bool; + }; + + root_url = mkOption { + description = '' + This is the full URL used to access Grafana from a web browser. + This is important if you use Google or GitHub OAuth authentication (for the callback URL to be correct). + + This setting is also important if you have a reverse proxy in front of Grafana that exposes it through a subpath. + In that case add the subpath to the end of this URL setting. + ''; + default = "%(protocol)s://%(domain)s:%(http_port)s/"; + type = types.str; + }; + + serve_from_sub_path = mkOption { + description = '' + Serve Grafana from subpath specified in the `root_url` setting. + By default it is set to `false` for compatibility reasons. + + By enabling this setting and using a subpath in `root_url` above, + e.g. `root_url = "http://localhost:3000/grafana"`, + Grafana is accessible on `http://localhost:3000/grafana`. + If accessed without subpath, Grafana will redirect to an URL with the subpath. + ''; + default = false; + type = types.bool; + }; + + router_logging = mkOption { + description = '' + Set to `true` for Grafana to log all HTTP requests (not just errors). + These are logged as Info level events to the Grafana log. + ''; + default = false; + type = types.bool; + }; + + static_root_path = mkOption { + description = "Root path for static assets."; + default = "${cfg.package}/share/grafana/public"; + defaultText = literalExpression ''"''${package}/share/grafana/public"''; + type = types.str; + }; + + enable_gzip = mkOption { + description = '' + Set this option to `true` to enable HTTP compression, this can improve transfer speed and bandwidth utilization. + It is recommended that most users set it to `true`. By default it is set to `false` for compatibility reasons. + ''; + default = false; + type = types.bool; + }; + + cert_file = mkOption { + description = '' + Path to the certificate file (if `protocol` is set to `https` or `h2`). + ''; + default = null; + type = types.nullOr types.str; + }; + + cert_key = mkOption { + description = '' + Path to the certificate key file (if `protocol` is set to `https` or `h2`). + ''; + default = null; + type = types.nullOr types.str; + }; + + socket_gid = mkOption { + description = '' + GID where the socket should be set when `protocol=socket`. + Make sure that the target group is in the group of Grafana process and that Grafana process is the file owner before you change this setting. + It is recommended to set the gid as http server user gid. + Not set when the value is -1. + ''; + default = -1; + type = types.int; + }; + + socket_mode = mkOption { + description = '' + Mode where the socket should be set when `protocol=socket`. + Make sure that Grafana process is the file owner before you change this setting. + ''; + # I assume this value is interpreted as octal literal by grafana. + # If this was an int, people following tutorials or porting their + # old config could stumble across nix not having octal literals. + default = "0660"; + type = types.str; + }; + + socket = mkOption { + description = '' + Path where the socket should be created when `protocol=socket`. + Make sure that Grafana has appropriate permissions before you change this setting. + ''; + default = "/run/grafana/grafana.sock"; + type = types.str; + }; + + cdn_url = mkOption { + description = '' + Specify a full HTTP URL address to the root of your Grafana CDN assets. + Grafana will add edition and version paths. + + For example, given a cdn url like `https://cdn.myserver.com` + grafana will try to load a javascript file from `http://cdn.myserver.com/grafana-oss/7.4.0/public/build/app.<hash>.js`. + ''; + default = null; + type = types.nullOr types.str; + }; + + read_timeout = mkOption { + description = '' + Sets the maximum time using a duration format (5s/5m/5ms) + before timing out read of an incoming request and closing idle connections. + 0 means there is no timeout for reading the request. + ''; + default = "0"; + type = types.str; + }; + }; + + database = { + type = mkOption { + description = "Database type."; + default = "sqlite3"; + type = types.enum [ "mysql" "sqlite3" "postgres" ]; + }; + + host = mkOption { + description = '' + Only applicable to MySQL or Postgres. + Includes IP or hostname and port or in case of Unix sockets the path to it. + For example, for MySQL running on the same host as Grafana: `host = "127.0.0.1:3306"` + or with Unix sockets: `host = "/var/run/mysqld/mysqld.sock"` + ''; + default = "127.0.0.1:3306"; + type = types.str; + }; + + name = mkOption { + description = "The name of the Grafana database."; + default = "grafana"; + type = types.str; + }; + + user = mkOption { + description = "The database user (not applicable for `sqlite3`)."; + default = "root"; + type = types.str; + }; + + password = mkOption { + description = '' + The database user's password (not applicable for `sqlite3`). + + Please note that the contents of this option + will end up in a world-readable Nix store. Use the file provider + pointing at a reasonably secured file in the local filesystem + to work around that. Look at the documentation for details: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; + default = ""; + type = types.str; + }; + + max_idle_conn = mkOption { + description = "The maximum number of connections in the idle connection pool."; + default = 2; + type = types.int; + }; + + max_open_conn = mkOption { + description = "The maximum number of open connections to the database."; + default = 0; + type = types.int; + }; + + conn_max_lifetime = mkOption { + description = '' + Sets the maximum amount of time a connection may be reused. + The default is 14400 (which means 14400 seconds or 4 hours). + For MySQL, this setting should be shorter than the `wait_timeout` variable. + ''; + default = 14400; + type = types.int; + }; + + locking_attempt_timeout_sec = mkOption { + description = '' + For `mysql`, if the `migrationLocking` feature toggle is set, + specify the time (in seconds) to wait before failing to lock the database for the migrations. + ''; + default = 0; + type = types.int; + }; + + log_queries = mkOption { + description = "Set to `true` to log the sql calls and execution times"; + default = false; + type = types.bool; + }; + + ssl_mode = mkOption { + description = '' + For Postgres, use either `disable`, `require` or `verify-full`. + For MySQL, use either `true`, `false`, or `skip-verify`. + ''; + default = "disable"; + type = types.enum [ "disable" "require" "verify-full" "true" "false" "skip-verify" ]; + }; + + isolation_level = mkOption { + description = '' + Only the MySQL driver supports isolation levels in Grafana. + In case the value is empty, the driver's default isolation level is applied. + ''; + default = null; + type = types.nullOr (types.enum [ "READ-UNCOMMITTED" "READ-COMMITTED" "REPEATABLE-READ" "SERIALIZABLE" ]); + }; + + ca_cert_path = mkOption { + description = "The path to the CA certificate to use."; + default = null; + type = types.nullOr types.str; + }; + + client_key_path = mkOption { + description = "The path to the client key. Only if server requires client authentication."; + default = null; + type = types.nullOr types.str; + }; + + client_cert_path = mkOption { + description = "The path to the client cert. Only if server requires client authentication."; + default = null; + type = types.nullOr types.str; + }; + + server_cert_name = mkOption { + description = '' + The common name field of the certificate used by the `mysql` or `postgres` server. + Not necessary if `ssl_mode` is set to `skip-verify`. + ''; + default = null; + type = types.nullOr types.str; + }; + + path = mkOption { + description = "Only applicable to `sqlite3` database. The file path where the database will be stored."; + default = "${cfg.dataDir}/data/grafana.db"; + defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"''; + type = types.path; + }; + + cache_mode = mkOption { + description = '' + For `sqlite3` only. + [Shared cache](https://www.sqlite.org/sharedcache.html) setting used for connecting to the database. + ''; + default = "private"; + type = types.enum [ "private" "shared" ]; + }; + + wal = mkOption { + description = '' + For `sqlite3` only. + Setting to enable/disable [Write-Ahead Logging](https://sqlite.org/wal.html). + ''; + default = false; + type = types.bool; + }; + + query_retries = mkOption { + description = '' + This setting applies to `sqlite3` only and controls the number of times the system retries a query when the database is locked. + ''; + default = 0; + type = types.int; + }; + + transaction_retries = mkOption { + description = '' + This setting applies to `sqlite3` only and controls the number of times the system retries a transaction when the database is locked. + ''; + default = 5; + type = types.int; + }; + + # TODO Add "instrument_queries" option when upgrading to grafana 10.0 + # instrument_queries = mkOption { + # description = "Set to `true` to add metrics and tracing for database queries."; + # default = false; + # type = types.bool; + # }; + }; + + security = { + disable_initial_admin_creation = mkOption { + description = "Disable creation of admin user on first start of Grafana."; + default = false; + type = types.bool; + }; + + admin_user = mkOption { + description = "Default admin username."; + default = "admin"; + type = types.str; + }; + + admin_password = mkOption { + description = '' + Default admin password. Please note that the contents of this option + will end up in a world-readable Nix store. Use the file provider + pointing at a reasonably secured file in the local filesystem + to work around that. Look at the documentation for details: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; + default = "admin"; + type = types.str; + }; + + admin_email = mkOption { + description = "The email of the default Grafana Admin, created on startup."; + default = "admin@localhost"; + type = types.str; + }; + + secret_key = mkOption { + description = '' + Secret key used for signing. Please note that the contents of this option + will end up in a world-readable Nix store. Use the file provider + pointing at a reasonably secured file in the local filesystem + to work around that. Look at the documentation for details: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; + default = "SW2YcwTIb9zpOOhoPsMm"; + type = types.str; + }; + + disable_gravatar = mkOption { + description = "Set to `true` to disable the use of Gravatar for user profile images."; + default = false; + type = types.bool; + }; + + data_source_proxy_whitelist = mkOption { + description = '' + Define a whitelist of allowed IP addresses or domains, with ports, + to be used in data source URLs with the Grafana data source proxy. + Format: `ip_or_domain:port` separated by spaces. + PostgreSQL, MySQL, and MSSQL data sources do not use the proxy and are therefore unaffected by this setting. + ''; + default = [ ]; + type = types.oneOf [ types.str (types.listOf types.str) ]; + }; + + disable_brute_force_login_protection = mkOption { + description = "Set to `true` to disable [brute force login protection](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#account-lockout)."; + default = false; + type = types.bool; + }; + + cookie_secure = mkOption { + description = "Set to `true` if you host Grafana behind HTTPS."; + default = false; + type = types.bool; + }; + + cookie_samesite = mkOption { + description = '' + Sets the `SameSite` cookie attribute and prevents the browser from sending this cookie along with cross-site requests. + The main goal is to mitigate the risk of cross-origin information leakage. + This setting also provides some protection against cross-site request forgery attacks (CSRF), + [read more about SameSite here](https://owasp.org/www-community/SameSite). + Using value `disabled` does not add any `SameSite` attribute to cookies. + ''; + default = "lax"; + type = types.enum [ "lax" "strict" "none" "disabled" ]; + }; + + allow_embedding = mkOption { + description = '' + When `false`, the HTTP header `X-Frame-Options: deny` will be set in Grafana HTTP responses + which will instruct browsers to not allow rendering Grafana in a `<frame>`, `<iframe>`, `<embed>` or `<object>`. + The main goal is to mitigate the risk of [Clickjacking](https://owasp.org/www-community/attacks/Clickjacking). + ''; + default = false; + type = types.bool; + }; + + strict_transport_security = mkOption { + description = '' + Set to `true` if you want to enable HTTP `Strict-Transport-Security` (HSTS) response header. + Only use this when HTTPS is enabled in your configuration, + or when there is another upstream system that ensures your application does HTTPS (like a frontend load balancer). + HSTS tells browsers that the site should only be accessed using HTTPS. + ''; + default = false; + type = types.bool; + }; + + strict_transport_security_max_age_seconds = mkOption { + description = '' + Sets how long a browser should cache HSTS in seconds. + Only applied if `strict_transport_security` is enabled. + ''; + default = 86400; + type = types.int; + }; + + strict_transport_security_preload = mkOption { + description = '' + Set to `true` to enable HSTS `preloading` option. + Only applied if `strict_transport_security` is enabled. + ''; + default = false; + type = types.bool; + }; + + strict_transport_security_subdomains = mkOption { + description = '' + Set to `true` to enable HSTS `includeSubDomains` option. + Only applied if `strict_transport_security` is enabled. + ''; + default = false; + type = types.bool; + }; + + x_content_type_options = mkOption { + description = '' + Set to `false` to disable the `X-Content-Type-Options` response header. + The `X-Content-Type-Options` response HTTP header is a marker used by the server + to indicate that the MIME types advertised in the `Content-Type` headers should not be changed and be followed. + ''; + default = true; + type = types.bool; + }; + + x_xss_protection = mkOption { + description = '' + Set to `false` to disable the `X-XSS-Protection` header, + which tells browsers to stop pages from loading when they detect reflected cross-site scripting (XSS) attacks. + ''; + default = true; + type = types.bool; + }; + + content_security_policy = mkOption { + description = '' + Set to `true` to add the `Content-Security-Policy` header to your requests. + CSP allows to control resources that the user agent can load and helps prevent XSS attacks. + ''; + default = false; + type = types.bool; + }; + + content_security_policy_report_only = mkOption { + description = '' + Set to `true` to add the `Content-Security-Policy-Report-Only` header to your requests. + CSP in Report Only mode enables you to experiment with policies by monitoring their effects without enforcing them. + You can enable both policies simultaneously. + ''; + default = false; + type = types.bool; + }; + + # The options content_security_policy_template and + # content_security_policy_template are missing because I'm not sure + # how exactly the quoting of the default value works. See also + # https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L364 + # https://github.com/grafana/grafana/blob/cb7e18938b8eb6860a64b91aaba13a7eb31bc95b/conf/defaults.ini#L373 + + # These two options are lists joined with spaces: + # https://github.com/grafana/grafana/blob/916d9793aa81c2990640b55a15dee0db6b525e41/pkg/middleware/csrf/csrf.go#L37-L38 + + csrf_trusted_origins = mkOption { + description = '' + List of additional allowed URLs to pass by the CSRF check. + Suggested when authentication comes from an IdP. + ''; + default = [ ]; + type = types.oneOf [ types.str (types.listOf types.str) ]; + }; + + csrf_additional_headers = mkOption { + description = '' + List of allowed headers to be set by the user. + Suggested to use for if authentication lives behind reverse proxies. + ''; + default = [ ]; + type = types.oneOf [ types.str (types.listOf types.str) ]; + }; + }; + + smtp = { + enabled = mkOption { + description = "Whether to enable SMTP."; + default = false; + type = types.bool; + }; + + host = mkOption { + description = "Host to connect to."; + default = "localhost:25"; + type = types.str; + }; + + user = mkOption { + description = "User used for authentication."; + default = null; + type = types.nullOr types.str; + }; + + password = mkOption { + description = '' + Password used for authentication. Please note that the contents of this option + will end up in a world-readable Nix store. Use the file provider + pointing at a reasonably secured file in the local filesystem + to work around that. Look at the documentation for details: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; + default = ""; + type = types.str; + }; + + cert_file = mkOption { + description = "File path to a cert file."; + default = null; + type = types.nullOr types.str; + }; + + key_file = mkOption { + description = "File path to a key file."; + default = null; + type = types.nullOr types.str; + }; + + skip_verify = mkOption { + description = "Verify SSL for SMTP server."; + default = false; + type = types.bool; + }; + + from_address = mkOption { + description = "Address used when sending out emails."; + default = "admin@grafana.localhost"; + type = types.str; + }; + + from_name = mkOption { + description = "Name to be used as client identity for EHLO in SMTP dialog."; + default = "Grafana"; + type = types.str; + }; + + ehlo_identity = mkOption { + description = "Name to be used as client identity for EHLO in SMTP dialog."; + default = null; + type = types.nullOr types.str; + }; + + startTLS_policy = mkOption { + description = "StartTLS policy when connecting to server."; + default = null; + type = types.nullOr (types.enum [ "OpportunisticStartTLS" "MandatoryStartTLS" "NoStartTLS" ]); + }; + }; + + users = { + allow_sign_up = mkOption { + description = '' + Set to false to prohibit users from being able to sign up / create user accounts. + The admin user can still create users. + ''; + default = false; + type = types.bool; + }; + + allow_org_create = mkOption { + description = "Set to `false` to prohibit users from creating new organizations."; + default = false; + type = types.bool; + }; + + auto_assign_org = mkOption { + description = '' + Set to `true` to automatically add new users to the main organization (id 1). + When set to `false,` new users automatically cause a new organization to be created for that new user. + The organization will be created even if the `allow_org_create` setting is set to `false`. + ''; + default = true; + type = types.bool; + }; + + auto_assign_org_id = mkOption { + description = '' + Set this value to automatically add new users to the provided org. + This requires `auto_assign_org` to be set to `true`. + Please make sure that this organization already exists. + ''; + default = 1; + type = types.int; + }; + + auto_assign_org_role = mkOption { + description = '' + The role new users will be assigned for the main organization (if the `auto_assign_org` setting is set to `true`). + ''; + default = "Viewer"; + type = types.enum [ "Viewer" "Editor" "Admin" ]; + }; + + verify_email_enabled = mkOption { + description = "Require email validation before sign up completes."; + default = false; + type = types.bool; + }; + + login_hint = mkOption { + description = "Text used as placeholder text on login page for login/username input."; + default = "email or username"; + type = types.str; + }; + + password_hint = mkOption { + description = "Text used as placeholder text on login page for password input."; + default = "password"; + type = types.str; + }; + + default_theme = mkOption { + description = "Sets the default UI theme. `system` matches the user's system theme."; + default = "dark"; + type = types.enum [ "dark" "light" "system" ]; + }; + + default_language = mkOption { + description = "This setting configures the default UI language, which must be a supported IETF language tag, such as `en-US`."; + default = "en-US"; + type = types.str; + }; + + home_page = mkOption { + description = '' + Path to a custom home page. + Users are only redirected to this if the default home dashboard is used. + It should match a frontend route and contain a leading slash. + ''; + default = ""; + type = types.str; + }; + + viewers_can_edit = mkOption { + description = '' + Viewers can access and use Explore and perform temporary edits on panels in dashboards they have access to. + They cannot save their changes. + ''; + default = false; + type = types.bool; + }; + + editors_can_admin = mkOption { + description = "Editors can administrate dashboards, folders and teams they create."; + default = false; + type = types.bool; + }; + + user_invite_max_lifetime_duration = mkOption { + description = '' + The duration in time a user invitation remains valid before expiring. + This setting should be expressed as a duration. + Examples: `6h` (hours), `2d` (days), `1w` (week). + The minimum supported duration is `15m` (15 minutes). + ''; + default = "24h"; + type = types.str; + }; + + # Lists are joined via space, so this option can't be a list. + # Users have to manually join their values. + hidden_users = mkOption { + description = '' + This is a comma-separated list of usernames. + Users specified here are hidden in the Grafana UI. + They are still visible to Grafana administrators and to themselves. + ''; + default = ""; + type = types.str; + }; + }; + + analytics = { + reporting_enabled = mkOption { + description = '' + When enabled Grafana will send anonymous usage statistics to `stats.grafana.org`. + No IP addresses are being tracked, only simple counters to track running instances, versions, dashboard and error counts. + Counters are sent every 24 hours. + ''; + default = true; + type = types.bool; + }; + + check_for_updates = mkOption { + description = '' + When set to `false`, disables checking for new versions of Grafana from Grafana's GitHub repository. + When enabled, the check for a new version runs every 10 minutes. + It will notify, via the UI, when a new version is available. + The check itself will not prompt any auto-updates of the Grafana software, nor will it send any sensitive information. + ''; + default = false; + type = types.bool; + }; + + check_for_plugin_updates = mkOption { + description = '' + When set to `false`, disables checking for new versions of installed plugins from https://grafana.com. + When enabled, the check for a new plugin runs every 10 minutes. + It will notify, via the UI, when a new plugin update exists. + The check itself will not prompt any auto-updates of the plugin, nor will it send any sensitive information. + ''; + default = cfg.declarativePlugins == null; + defaultText = literalExpression "cfg.declarativePlugins == null"; + type = types.bool; + }; + + feedback_links_enabled = mkOption { + description = "Set to `false` to remove all feedback links from the UI."; + default = true; + type = types.bool; + }; + }; + }; + }; + }; + + provision = { + enable = mkEnableOption "provision"; + + datasources = mkOption { + description = '' + Declaratively provision Grafana's datasources. + ''; + default = { }; + type = types.submodule { + options.settings = mkOption { + description = '' + Grafana datasource configuration in Nix. Can't be used with + [](#opt-services.grafana.provision.datasources.path) simultaneously. See + <https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources> + for supported options. + ''; + default = null; + type = types.nullOr (types.submodule { + options = { + apiVersion = mkOption { + description = "Config file version."; + default = 1; + type = types.int; + }; + + datasources = mkOption { + description = "List of datasources to insert/update."; + default = [ ]; + type = types.listOf grafanaTypes.datasourceConfig; + }; + + deleteDatasources = mkOption { + description = "List of datasources that should be deleted from the database."; + default = [ ]; + type = types.listOf (types.submodule { + options.name = mkOption { + description = "Name of the datasource to delete."; + type = types.str; + }; + + options.orgId = mkOption { + description = "Organization ID of the datasource to delete."; + type = types.int; + }; + }); + }; + }; + }); + example = literalExpression '' + { + apiVersion = 1; + + datasources = [{ + name = "Graphite"; + type = "graphite"; + }]; + + deleteDatasources = [{ + name = "Graphite"; + orgId = 1; + }]; + } + ''; + }; + + options.path = mkOption { + description = '' + Path to YAML datasource configuration. Can't be used with + [](#opt-services.grafana.provision.datasources.settings) simultaneously. + Can be either a directory or a single YAML file. Will end up in the store. + ''; + default = null; + type = types.nullOr types.path; + }; + }; + }; + + + dashboards = mkOption { + description = '' + Declaratively provision Grafana's dashboards. + ''; + default = { }; + type = types.submodule { + options.settings = mkOption { + description = '' + Grafana dashboard configuration in Nix. Can't be used with + [](#opt-services.grafana.provision.dashboards.path) simultaneously. See + <https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards> + for supported options. + ''; + default = null; + type = types.nullOr (types.submodule { + options.apiVersion = mkOption { + description = "Config file version."; + default = 1; + type = types.int; + }; + + options.providers = mkOption { + description = "List of dashboards to insert/update."; + default = [ ]; + type = types.listOf grafanaTypes.dashboardConfig; + }; + }); + example = literalExpression '' + { + apiVersion = 1; + + providers = [{ + name = "default"; + options.path = "/var/lib/grafana/dashboards"; + }]; + } + ''; + }; + + options.path = mkOption { + description = '' + Path to YAML dashboard configuration. Can't be used with + [](#opt-services.grafana.provision.dashboards.settings) simultaneously. + Can be either a directory or a single YAML file. Will end up in the store. + ''; + default = null; + type = types.nullOr types.path; + }; + }; + }; + + + notifiers = mkOption { + description = "Grafana notifier configuration."; + default = [ ]; + type = types.listOf grafanaTypes.notifierConfig; + apply = x: map _filter x; + }; + + + alerting = { + rules = { + path = mkOption { + description = '' + Path to YAML rules configuration. Can't be used with + [](#opt-services.grafana.provision.alerting.rules.settings) simultaneously. + Can be either a directory or a single YAML file. Will end up in the store. + ''; + default = null; + type = types.nullOr types.path; + }; + + settings = mkOption { + description = '' + Grafana rules configuration in Nix. Can't be used with + [](#opt-services.grafana.provision.alerting.rules.path) simultaneously. See + <https://grafana.com/docs/grafana/latest/administration/provisioning/#rules> + for supported options. + ''; + default = null; + type = types.nullOr (types.submodule { + options = { + apiVersion = mkOption { + description = "Config file version."; + default = 1; + type = types.int; + }; + + groups = mkOption { + description = "List of rule groups to import or update."; + default = [ ]; + type = types.listOf (types.submodule { + freeformType = provisioningSettingsFormat.type; + + options.name = mkOption { + description = "Name of the rule group. Required."; + type = types.str; + }; + + options.folder = mkOption { + description = "Name of the folder the rule group will be stored in. Required."; + type = types.str; + }; + + options.interval = mkOption { + description = "Interval that the rule group should be evaluated at. Required."; + type = types.str; + }; + }); + }; + + deleteRules = mkOption { + description = "List of alert rule UIDs that should be deleted."; + default = [ ]; + type = types.listOf (types.submodule { + options.orgId = mkOption { + description = "Organization ID, default = 1"; + default = 1; + type = types.int; + }; + + options.uid = mkOption { + description = "Unique identifier for the rule. Required."; + type = types.str; + }; + }); + }; + }; + }); + example = literalExpression '' + { + apiVersion = 1; + + groups = [{ + orgId = 1; + name = "my_rule_group"; + folder = "my_first_folder"; + interval = "60s"; + rules = [{ + uid = "my_id_1"; + title = "my_first_rule"; + condition = "A"; + data = [{ + refId = "A"; + datasourceUid = "-100"; + model = { + conditions = [{ + evaluator = { + params = [ 3 ]; + type = "git"; + }; + operator.type = "and"; + query.params = [ "A" ]; + reducer.type = "last"; + type = "query"; + }]; + datasource = { + type = "__expr__"; + uid = "-100"; + }; + expression = "1==0"; + intervalMs = 1000; + maxDataPoints = 43200; + refId = "A"; + type = "math"; + }; + }]; + dashboardUid = "my_dashboard"; + panelId = 123; + noDataState = "Alerting"; + for = "60s"; + annotations.some_key = "some_value"; + labels.team = "sre_team1"; + }]; + }]; + + deleteRules = [{ + orgId = 1; + uid = "my_id_1"; + }]; + } + ''; + }; + }; + + contactPoints = { + path = mkOption { + description = '' + Path to YAML contact points configuration. Can't be used with + [](#opt-services.grafana.provision.alerting.contactPoints.settings) simultaneously. + Can be either a directory or a single YAML file. Will end up in the store. + ''; + default = null; + type = types.nullOr types.path; + }; + + settings = mkOption { + description = '' + Grafana contact points configuration in Nix. Can't be used with + [](#opt-services.grafana.provision.alerting.contactPoints.path) simultaneously. See + <https://grafana.com/docs/grafana/latest/administration/provisioning/#contact-points> + for supported options. + ''; + default = null; + type = types.nullOr (types.submodule { + options = { + apiVersion = mkOption { + description = "Config file version."; + default = 1; + type = types.int; + }; + + contactPoints = mkOption { + description = "List of contact points to import or update."; + default = [ ]; + type = types.listOf (types.submodule { + freeformType = provisioningSettingsFormat.type; + + options.name = mkOption { + description = "Name of the contact point. Required."; + type = types.str; + }; + }); + }; + + deleteContactPoints = mkOption { + description = "List of receivers that should be deleted."; + default = [ ]; + type = types.listOf (types.submodule { + options.orgId = mkOption { + description = "Organization ID, default = 1."; + default = 1; + type = types.int; + }; + + options.uid = mkOption { + description = "Unique identifier for the receiver. Required."; + type = types.str; + }; + }); + }; + }; + }); + example = literalExpression '' + { + apiVersion = 1; + + contactPoints = [{ + orgId = 1; + name = "cp_1"; + receivers = [{ + uid = "first_uid"; + type = "prometheus-alertmanager"; + settings.url = "http://test:9000"; + }]; + }]; + + deleteContactPoints = [{ + orgId = 1; + uid = "first_uid"; + }]; + } + ''; + }; + }; + + policies = { + path = mkOption { + description = '' + Path to YAML notification policies configuration. Can't be used with + [](#opt-services.grafana.provision.alerting.policies.settings) simultaneously. + Can be either a directory or a single YAML file. Will end up in the store. + ''; + default = null; + type = types.nullOr types.path; + }; + + settings = mkOption { + description = '' + Grafana notification policies configuration in Nix. Can't be used with + [](#opt-services.grafana.provision.alerting.policies.path) simultaneously. See + <https://grafana.com/docs/grafana/latest/administration/provisioning/#notification-policies> + for supported options. + ''; + default = null; + type = types.nullOr (types.submodule { + options = { + apiVersion = mkOption { + description = "Config file version."; + default = 1; + type = types.int; + }; + + policies = mkOption { + description = "List of contact points to import or update."; + default = [ ]; + type = types.listOf (types.submodule { + freeformType = provisioningSettingsFormat.type; + }); + }; + + resetPolicies = mkOption { + description = "List of orgIds that should be reset to the default policy."; + default = [ ]; + type = types.listOf types.int; + }; + }; + }); + example = literalExpression '' + { + apiVersion = 1; + + policies = [{ + orgId = 1; + receiver = "grafana-default-email"; + group_by = [ "..." ]; + matchers = [ + "alertname = Watchdog" + "severity =~ \"warning|critical\"" + ]; + mute_time_intervals = [ + "abc" + ]; + group_wait = "30s"; + group_interval = "5m"; + repeat_interval = "4h"; + }]; + + resetPolicies = [ + 1 + ]; + } + ''; + }; + }; + + templates = { + path = mkOption { + description = '' + Path to YAML templates configuration. Can't be used with + [](#opt-services.grafana.provision.alerting.templates.settings) simultaneously. + Can be either a directory or a single YAML file. Will end up in the store. + ''; + default = null; + type = types.nullOr types.path; + }; + + settings = mkOption { + description = '' + Grafana templates configuration in Nix. Can't be used with + [](#opt-services.grafana.provision.alerting.templates.path) simultaneously. See + <https://grafana.com/docs/grafana/latest/administration/provisioning/#templates> + for supported options. + ''; + default = null; + type = types.nullOr (types.submodule { + options = { + apiVersion = mkOption { + description = "Config file version."; + default = 1; + type = types.int; + }; + + templates = mkOption { + description = "List of templates to import or update."; + default = [ ]; + type = types.listOf (types.submodule { + freeformType = provisioningSettingsFormat.type; + + options.name = mkOption { + description = "Name of the template, must be unique. Required."; + type = types.str; + }; + + options.template = mkOption { + description = "Alerting with a custom text template"; + type = types.str; + }; + }); + }; + + deleteTemplates = mkOption { + description = "List of alert rule UIDs that should be deleted."; + default = [ ]; + type = types.listOf (types.submodule { + options.orgId = mkOption { + description = "Organization ID, default = 1."; + default = 1; + type = types.int; + }; + + options.name = mkOption { + description = "Name of the template, must be unique. Required."; + type = types.str; + }; + }); + }; + }; + }); + example = literalExpression '' + { + apiVersion = 1; + + templates = [{ + orgId = 1; + name = "my_first_template"; + template = "Alerting with a custom text template"; + }]; + + deleteTemplates = [{ + orgId = 1; + name = "my_first_template"; + }]; + } + ''; + }; + }; + + muteTimings = { + path = mkOption { + description = '' + Path to YAML mute timings configuration. Can't be used with + [](#opt-services.grafana.provision.alerting.muteTimings.settings) simultaneously. + Can be either a directory or a single YAML file. Will end up in the store. + ''; + default = null; + type = types.nullOr types.path; + }; + + settings = mkOption { + description = '' + Grafana mute timings configuration in Nix. Can't be used with + [](#opt-services.grafana.provision.alerting.muteTimings.path) simultaneously. See + <https://grafana.com/docs/grafana/latest/administration/provisioning/#mute-timings> + for supported options. + ''; + default = null; + type = types.nullOr (types.submodule { + options = { + apiVersion = mkOption { + description = "Config file version."; + default = 1; + type = types.int; + }; + + muteTimes = mkOption { + description = "List of mute time intervals to import or update."; + default = [ ]; + type = types.listOf (types.submodule { + freeformType = provisioningSettingsFormat.type; + + options.name = mkOption { + description = "Name of the mute time interval, must be unique. Required."; + type = types.str; + }; + }); + }; + + deleteMuteTimes = mkOption { + description = "List of mute time intervals that should be deleted."; + default = [ ]; + type = types.listOf (types.submodule { + options.orgId = mkOption { + description = "Organization ID, default = 1."; + default = 1; + type = types.int; + }; + + options.name = mkOption { + description = "Name of the mute time interval, must be unique. Required."; + type = types.str; + }; + }); + }; + }; + }); + example = literalExpression '' + { + apiVersion = 1; + + muteTimes = [{ + orgId = 1; + name = "mti_1"; + time_intervals = [{ + times = [{ + start_time = "06:00"; + end_time = "23:59"; + }]; + weekdays = [ + "monday:wednesday" + "saturday" + "sunday" + ]; + months = [ + "1:3" + "may:august" + "december" + ]; + years = [ + "2020:2022" + "2030" + ]; + days_of_month = [ + "1:5" + "-3:-1" + ]; + }]; + }]; + + deleteMuteTimes = [{ + orgId = 1; + name = "mti_1"; + }]; + } + ''; + }; + }; + }; + }; + }; + + config = mkIf cfg.enable { + warnings = + let + doesntUseFileProvider = opt: defaultValue: + let regex = "${optionalString (defaultValue != null) "^${defaultValue}$|"}^\\$__(file|env)\\{.*}$|^\\$[^_\\$][^ ]+$"; + in builtins.match regex opt == null; + + # Ensure that no custom credentials are leaked into the Nix store. Unless the default value + # is specified, this can be achieved by using the file/env provider: + # https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#variable-expansion + passwordWithoutFileProvider = optional + ( + doesntUseFileProvider cfg.settings.database.password "" || + doesntUseFileProvider cfg.settings.security.admin_password "admin" + ) + '' + Grafana passwords will be stored as plaintext in the Nix store! + Use file provider or an env-var instead. + ''; + + # Warn about deprecated notifiers. + deprecatedNotifiers = optional (cfg.provision.notifiers != [ ]) '' + Notifiers are deprecated upstream and will be removed in Grafana 11. + Use `services.grafana.provision.alerting.contactPoints` instead. + ''; + + # Ensure that `secureJsonData` of datasources provisioned via `datasources.settings` + # only uses file/env providers. + secureJsonDataWithoutFileProvider = optional + ( + let + datasourcesToCheck = optionals + (cfg.provision.datasources.settings != null) + cfg.provision.datasources.settings.datasources; + declarationUnsafe = { secureJsonData, ... }: + secureJsonData != null + && any (flip doesntUseFileProvider null) (attrValues secureJsonData); + in + any declarationUnsafe datasourcesToCheck + ) + '' + Declarations in the `secureJsonData`-block of a datasource will be leaked to the + Nix store unless a file-provider or an env-var is used! + ''; + + notifierSecureSettingsWithoutFileProvider = optional + (any (x: x.secure_settings != null) cfg.provision.notifiers) + "Notifier secure settings will be stored as plaintext in the Nix store! Use file provider instead."; + in + passwordWithoutFileProvider + ++ deprecatedNotifiers + ++ secureJsonDataWithoutFileProvider + ++ notifierSecureSettingsWithoutFileProvider; + + environment.systemPackages = [ cfg.package ]; + + assertions = [ + { + assertion = cfg.provision.datasources.settings == null || cfg.provision.datasources.path == null; + message = "Cannot set both datasources settings and datasources path"; + } + { + assertion = + let + prometheusIsNotDirect = opt: all + ({ type, access, ... }: type == "prometheus" -> access != "direct") + opt; + in + cfg.provision.datasources.settings == null || prometheusIsNotDirect cfg.provision.datasources.settings.datasources; + message = "For datasources of type `prometheus`, the `direct` access mode is not supported anymore (since Grafana 9.2.0)"; + } + { + assertion = cfg.provision.dashboards.settings == null || cfg.provision.dashboards.path == null; + message = "Cannot set both dashboards settings and dashboards path"; + } + { + assertion = cfg.provision.alerting.rules.settings == null || cfg.provision.alerting.rules.path == null; + message = "Cannot set both rules settings and rules path"; + } + { + assertion = cfg.provision.alerting.contactPoints.settings == null || cfg.provision.alerting.contactPoints.path == null; + message = "Cannot set both contact points settings and contact points path"; + } + { + assertion = cfg.provision.alerting.policies.settings == null || cfg.provision.alerting.policies.path == null; + message = "Cannot set both policies settings and policies path"; + } + { + assertion = cfg.provision.alerting.templates.settings == null || cfg.provision.alerting.templates.path == null; + message = "Cannot set both templates settings and templates path"; + } + { + assertion = cfg.provision.alerting.muteTimings.settings == null || cfg.provision.alerting.muteTimings.path == null; + message = "Cannot set both mute timings settings and mute timings path"; + } + ]; + + systemd.services.grafana = { + description = "Grafana Service Daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "networking.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service"; + script = '' + set -o errexit -o pipefail -o nounset -o errtrace + shopt -s inherit_errexit + + exec ${cfg.package}/bin/grafana server -homepath ${cfg.dataDir} -config ${configFile} + ''; + serviceConfig = { + WorkingDirectory = cfg.dataDir; + User = "grafana"; + Restart = "on-failure"; + RuntimeDirectory = "grafana"; + RuntimeDirectoryMode = "0755"; + # Hardening + AmbientCapabilities = lib.mkIf (cfg.settings.server.http_port < 1024) [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = if (cfg.settings.server.http_port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ]; + DeviceAllow = [ "" ]; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "full"; + RemoveIPC = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + # Upstream grafana is not setting SystemCallFilter for compatibility + # reasons, see https://github.com/grafana/grafana/pull/40176 + SystemCallFilter = [ + "@system-service" + "~@privileged" + ] ++ lib.optionals (cfg.settings.server.protocol == "socket") [ "@chown" ]; + UMask = "0027"; + }; + preStart = '' + ln -fs ${cfg.package}/share/grafana/conf ${cfg.dataDir} + ln -fs ${cfg.package}/share/grafana/tools ${cfg.dataDir} + ''; + }; + + users.users.grafana = { + uid = config.ids.uids.grafana; + description = "Grafana user"; + home = cfg.dataDir; + createHome = true; + group = "grafana"; + }; + users.groups.grafana = { }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/graphite.nix b/nixpkgs/nixos/modules/services/monitoring/graphite.nix new file mode 100644 index 000000000000..3a8f41f32078 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/graphite.nix @@ -0,0 +1,428 @@ +{ config, lib, options, pkgs, ... }: + +with lib; + +let + cfg = config.services.graphite; + opt = options.services.graphite; + writeTextOrNull = f: t: mapNullable (pkgs.writeTextDir f) t; + + dataDir = cfg.dataDir; + staticDir = cfg.dataDir + "/static"; + + graphiteLocalSettingsDir = pkgs.runCommand "graphite_local_settings" { + inherit graphiteLocalSettings; + preferLocalBuild = true; + } '' + mkdir -p $out + ln -s $graphiteLocalSettings $out/graphite_local_settings.py + ''; + + graphiteLocalSettings = pkgs.writeText "graphite_local_settings.py" ( + "STATIC_ROOT = '${staticDir}'\n" + + optionalString (config.time.timeZone != null) "TIME_ZONE = '${config.time.timeZone}'\n" + + cfg.web.extraConfig + ); + + seyrenConfig = { + SEYREN_URL = cfg.seyren.seyrenUrl; + MONGO_URL = cfg.seyren.mongoUrl; + GRAPHITE_URL = cfg.seyren.graphiteUrl; + } // cfg.seyren.extraConfig; + + configDir = pkgs.buildEnv { + name = "graphite-config"; + paths = lists.filter (el: el != null) [ + (writeTextOrNull "carbon.conf" cfg.carbon.config) + (writeTextOrNull "storage-aggregation.conf" cfg.carbon.storageAggregation) + (writeTextOrNull "storage-schemas.conf" cfg.carbon.storageSchemas) + (writeTextOrNull "blacklist.conf" cfg.carbon.blacklist) + (writeTextOrNull "whitelist.conf" cfg.carbon.whitelist) + (writeTextOrNull "rewrite-rules.conf" cfg.carbon.rewriteRules) + (writeTextOrNull "relay-rules.conf" cfg.carbon.relayRules) + (writeTextOrNull "aggregation-rules.conf" cfg.carbon.aggregationRules) + ]; + }; + + carbonOpts = name: with config.ids; '' + --nodaemon --syslog --prefix=${name} --pidfile /run/${name}/${name}.pid ${name} + ''; + + carbonEnv = { + PYTHONPATH = let + cenv = pkgs.python3.buildEnv.override { + extraLibs = [ pkgs.python3Packages.carbon ]; + }; + in "${cenv}/${pkgs.python3.sitePackages}"; + GRAPHITE_ROOT = dataDir; + GRAPHITE_CONF_DIR = configDir; + GRAPHITE_STORAGE_DIR = dataDir; + }; + +in { + + imports = [ + (mkRemovedOptionModule ["services" "graphite" "api"] "") + (mkRemovedOptionModule ["services" "graphite" "beacon"] "") + (mkRemovedOptionModule ["services" "graphite" "pager"] "") + ]; + + ###### interface + + options.services.graphite = { + dataDir = mkOption { + type = types.path; + default = "/var/db/graphite"; + description = '' + Data directory for graphite. + ''; + }; + + web = { + enable = mkOption { + description = "Whether to enable graphite web frontend."; + default = false; + type = types.bool; + }; + + listenAddress = mkOption { + description = "Graphite web frontend listen address."; + default = "127.0.0.1"; + type = types.str; + }; + + port = mkOption { + description = "Graphite web frontend port."; + default = 8080; + type = types.port; + }; + + extraConfig = mkOption { + type = types.str; + default = ""; + description = '' + Graphite webapp settings. See: + <https://graphite.readthedocs.io/en/latest/config-local-settings.html> + ''; + }; + }; + + carbon = { + config = mkOption { + description = "Content of carbon configuration file."; + default = '' + [cache] + # Listen on localhost by default for security reasons + UDP_RECEIVER_INTERFACE = 127.0.0.1 + PICKLE_RECEIVER_INTERFACE = 127.0.0.1 + LINE_RECEIVER_INTERFACE = 127.0.0.1 + CACHE_QUERY_INTERFACE = 127.0.0.1 + # Do not log every update + LOG_UPDATES = False + LOG_CACHE_HITS = False + ''; + type = types.str; + }; + + enableCache = mkOption { + description = "Whether to enable carbon cache, the graphite storage daemon."; + default = false; + type = types.bool; + }; + + storageAggregation = mkOption { + description = "Defines how to aggregate data to lower-precision retentions."; + default = null; + type = types.nullOr types.str; + example = '' + [all_min] + pattern = \.min$ + xFilesFactor = 0.1 + aggregationMethod = min + ''; + }; + + storageSchemas = mkOption { + description = "Defines retention rates for storing metrics."; + default = ""; + type = types.nullOr types.str; + example = '' + [apache_busyWorkers] + pattern = ^servers\.www.*\.workers\.busyWorkers$ + retentions = 15s:7d,1m:21d,15m:5y + ''; + }; + + blacklist = mkOption { + description = "Any metrics received which match one of the expressions will be dropped."; + default = null; + type = types.nullOr types.str; + example = "^some\\.noisy\\.metric\\.prefix\\..*"; + }; + + whitelist = mkOption { + description = "Only metrics received which match one of the expressions will be persisted."; + default = null; + type = types.nullOr types.str; + example = ".*"; + }; + + rewriteRules = mkOption { + description = '' + Regular expression patterns that can be used to rewrite metric names + in a search and replace fashion. + ''; + default = null; + type = types.nullOr types.str; + example = '' + [post] + _sum$ = + _avg$ = + ''; + }; + + enableRelay = mkOption { + description = "Whether to enable carbon relay, the carbon replication and sharding service."; + default = false; + type = types.bool; + }; + + relayRules = mkOption { + description = "Relay rules are used to send certain metrics to a certain backend."; + default = null; + type = types.nullOr types.str; + example = '' + [example] + pattern = ^mydata\.foo\..+ + servers = 10.1.2.3, 10.1.2.4:2004, myserver.mydomain.com + ''; + }; + + enableAggregator = mkOption { + description = "Whether to enable carbon aggregator, the carbon buffering service."; + default = false; + type = types.bool; + }; + + aggregationRules = mkOption { + description = "Defines if and how received metrics will be aggregated."; + default = null; + type = types.nullOr types.str; + example = '' + <env>.applications.<app>.all.requests (60) = sum <env>.applications.<app>.*.requests + <env>.applications.<app>.all.latency (60) = avg <env>.applications.<app>.*.latency + ''; + }; + }; + + seyren = { + enable = mkOption { + description = "Whether to enable seyren service."; + default = false; + type = types.bool; + }; + + port = mkOption { + description = "Seyren listening port."; + default = 8081; + type = types.port; + }; + + seyrenUrl = mkOption { + default = "http://localhost:${toString cfg.seyren.port}/"; + defaultText = literalExpression ''"http://localhost:''${toString config.${opt.seyren.port}}/"''; + description = "Host where seyren is accessible."; + type = types.str; + }; + + graphiteUrl = mkOption { + default = "http://${cfg.web.listenAddress}:${toString cfg.web.port}"; + defaultText = literalExpression ''"http://''${config.${opt.web.listenAddress}}:''${toString config.${opt.web.port}}"''; + description = "Host where graphite service runs."; + type = types.str; + }; + + mongoUrl = mkOption { + default = "mongodb://${config.services.mongodb.bind_ip}:27017/seyren"; + defaultText = literalExpression ''"mongodb://''${config.services.mongodb.bind_ip}:27017/seyren"''; + description = "Mongodb connection string."; + type = types.str; + }; + + extraConfig = mkOption { + default = {}; + description = '' + Extra seyren configuration. See + <https://github.com/scobal/seyren#config> + ''; + type = types.attrsOf types.str; + example = literalExpression '' + { + GRAPHITE_USERNAME = "user"; + GRAPHITE_PASSWORD = "pass"; + } + ''; + }; + }; + }; + + ###### implementation + + config = mkMerge [ + (mkIf cfg.carbon.enableCache { + systemd.services.carbonCache = let name = "carbon-cache"; in { + description = "Graphite Data Storage Backend"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + environment = carbonEnv; + serviceConfig = { + RuntimeDirectory = name; + ExecStart = "${pkgs.python3Packages.twisted}/bin/twistd ${carbonOpts name}"; + User = "graphite"; + Group = "graphite"; + PermissionsStartOnly = true; + PIDFile="/run/${name}/${name}.pid"; + }; + preStart = '' + install -dm0700 -o graphite -g graphite ${cfg.dataDir} + install -dm0700 -o graphite -g graphite ${cfg.dataDir}/whisper + ''; + }; + }) + + (mkIf cfg.carbon.enableAggregator { + systemd.services.carbonAggregator = let name = "carbon-aggregator"; in { + enable = cfg.carbon.enableAggregator; + description = "Carbon Data Aggregator"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + environment = carbonEnv; + serviceConfig = { + RuntimeDirectory = name; + ExecStart = "${pkgs.python3Packages.twisted}/bin/twistd ${carbonOpts name}"; + User = "graphite"; + Group = "graphite"; + PIDFile="/run/${name}/${name}.pid"; + }; + }; + }) + + (mkIf cfg.carbon.enableRelay { + systemd.services.carbonRelay = let name = "carbon-relay"; in { + description = "Carbon Data Relay"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + environment = carbonEnv; + serviceConfig = { + RuntimeDirectory = name; + ExecStart = "${pkgs.python3Packages.twisted}/bin/twistd ${carbonOpts name}"; + User = "graphite"; + Group = "graphite"; + PIDFile="/run/${name}/${name}.pid"; + }; + }; + }) + + (mkIf (cfg.carbon.enableCache || cfg.carbon.enableAggregator || cfg.carbon.enableRelay) { + environment.systemPackages = [ + pkgs.python3Packages.carbon + ]; + }) + + (mkIf cfg.web.enable ({ + systemd.services.graphiteWeb = { + description = "Graphite Web Interface"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + path = [ pkgs.perl ]; + environment = { + PYTHONPATH = let + penv = pkgs.python3.buildEnv.override { + extraLibs = [ + pkgs.python3Packages.graphite-web + ]; + }; + penvPack = "${penv}/${pkgs.python3.sitePackages}"; + in concatStringsSep ":" [ + "${graphiteLocalSettingsDir}" + "${penvPack}" + # explicitly adding pycairo in path because it cannot be imported via buildEnv + "${pkgs.python3Packages.pycairo}/${pkgs.python3.sitePackages}" + ]; + DJANGO_SETTINGS_MODULE = "graphite.settings"; + GRAPHITE_SETTINGS_MODULE = "graphite_local_settings"; + GRAPHITE_CONF_DIR = configDir; + GRAPHITE_STORAGE_DIR = dataDir; + LD_LIBRARY_PATH = "${pkgs.cairo.out}/lib"; + }; + serviceConfig = { + ExecStart = '' + ${pkgs.python3Packages.waitress-django}/bin/waitress-serve-django \ + --host=${cfg.web.listenAddress} --port=${toString cfg.web.port} + ''; + User = "graphite"; + Group = "graphite"; + PermissionsStartOnly = true; + }; + preStart = '' + if ! test -e ${dataDir}/db-created; then + mkdir -p ${dataDir}/{whisper/,log/webapp/} + chmod 0700 ${dataDir}/{whisper/,log/webapp/} + + ${pkgs.python3Packages.django}/bin/django-admin.py migrate --noinput + + chown -R graphite:graphite ${dataDir} + + touch ${dataDir}/db-created + fi + + # Only collect static files when graphite_web changes. + if ! [ "${dataDir}/current_graphite_web" -ef "${pkgs.python3Packages.graphite-web}" ]; then + mkdir -p ${staticDir} + ${pkgs.python3Packages.django}/bin/django-admin.py collectstatic --noinput --clear + chown -R graphite:graphite ${staticDir} + ln -sfT "${pkgs.python3Packages.graphite-web}" "${dataDir}/current_graphite_web" + fi + ''; + }; + + environment.systemPackages = [ pkgs.python3Packages.graphite-web ]; + })) + + (mkIf cfg.seyren.enable { + systemd.services.seyren = { + description = "Graphite Alerting Dashboard"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "mongodb.service" ]; + environment = seyrenConfig; + serviceConfig = { + ExecStart = "${pkgs.seyren}/bin/seyren -httpPort ${toString cfg.seyren.port}"; + WorkingDirectory = dataDir; + User = "graphite"; + Group = "graphite"; + }; + preStart = '' + if ! test -e ${dataDir}/db-created; then + mkdir -p ${dataDir} + chown graphite:graphite ${dataDir} + fi + ''; + }; + + services.mongodb.enable = mkDefault true; + }) + + (mkIf ( + cfg.carbon.enableCache || cfg.carbon.enableAggregator || cfg.carbon.enableRelay || + cfg.web.enable || cfg.seyren.enable + ) { + users.users.graphite = { + uid = config.ids.uids.graphite; + group = "graphite"; + description = "Graphite daemon user"; + home = dataDir; + }; + users.groups.graphite.gid = config.ids.gids.graphite; + }) + ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/hdaps.nix b/nixpkgs/nixos/modules/services/monitoring/hdaps.nix new file mode 100644 index 000000000000..366367ef835c --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/hdaps.nix @@ -0,0 +1,22 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.hdapsd; + hdapsd = [ pkgs.hdapsd ]; +in +{ + options = { + services.hdapsd.enable = mkEnableOption '' + Hard Drive Active Protection System Daemon, + devices are detected and managed automatically by udev and systemd + ''; + }; + + config = mkIf cfg.enable { + boot.kernelModules = [ "hdapsd" ]; + services.udev.packages = hdapsd; + systemd.packages = hdapsd; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/heapster.nix b/nixpkgs/nixos/modules/services/monitoring/heapster.nix new file mode 100644 index 000000000000..b8ba3632caca --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/heapster.nix @@ -0,0 +1,50 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.heapster; +in { + options.services.heapster = { + enable = mkEnableOption "Heapster monitoring"; + + source = mkOption { + description = "Heapster metric source"; + example = "kubernetes:https://kubernetes.default"; + type = types.str; + }; + + sink = mkOption { + description = "Heapster metic sink"; + example = "influxdb:http://localhost:8086"; + type = types.str; + }; + + extraOpts = mkOption { + description = "Heapster extra options"; + default = ""; + type = types.separatedString " "; + }; + + package = mkPackageOption pkgs "heapster" { }; + }; + + config = mkIf cfg.enable { + systemd.services.heapster = { + wantedBy = ["multi-user.target"]; + after = ["cadvisor.service" "kube-apiserver.service"]; + + serviceConfig = { + ExecStart = "${cfg.package}/bin/heapster --source=${cfg.source} --sink=${cfg.sink} ${cfg.extraOpts}"; + User = "heapster"; + }; + }; + + users.users.heapster = { + isSystemUser = true; + group = "heapster"; + description = "Heapster user"; + }; + users.groups.heapster = {}; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/incron.nix b/nixpkgs/nixos/modules/services/monitoring/incron.nix new file mode 100644 index 000000000000..58b07bf97f1d --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/incron.nix @@ -0,0 +1,103 @@ + +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.incron; + +in + +{ + options = { + + services.incron = { + + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable the incron daemon. + + Note that commands run under incrontab only support common Nix profiles for the {env}`PATH` provided variable. + ''; + }; + + allow = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = '' + Users allowed to use incrontab. + + If empty then no user will be allowed to have their own incrontab. + If `null` then will defer to {option}`deny`. + If both {option}`allow` and {option}`deny` are null + then all users will be allowed to have their own incrontab. + ''; + }; + + deny = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = "Users forbidden from using incrontab."; + }; + + systab = mkOption { + type = types.lines; + default = ""; + description = "The system incrontab contents."; + example = '' + /var/mail IN_CLOSE_WRITE abc $@/$# + /tmp IN_ALL_EVENTS efg $@/$# $& + ''; + }; + + extraPackages = mkOption { + type = types.listOf types.package; + default = []; + example = literalExpression "[ pkgs.rsync ]"; + description = "Extra packages available to the system incrontab."; + }; + + }; + + }; + + config = mkIf cfg.enable { + + warnings = optional (cfg.allow != null && cfg.deny != null) + "If `services.incron.allow` is set then `services.incron.deny` will be ignored."; + + environment.systemPackages = [ pkgs.incron ]; + + security.wrappers.incrontab = + { setuid = true; + owner = "root"; + group = "root"; + source = "${pkgs.incron}/bin/incrontab"; + }; + + # incron won't read symlinks + environment.etc."incron.d/system" = { + mode = "0444"; + text = cfg.systab; + }; + environment.etc."incron.allow" = mkIf (cfg.allow != null) { + text = concatStringsSep "\n" cfg.allow; + }; + environment.etc."incron.deny" = mkIf (cfg.deny != null) { + text = concatStringsSep "\n" cfg.deny; + }; + + systemd.services.incron = { + description = "File System Events Scheduler"; + wantedBy = [ "multi-user.target" ]; + path = cfg.extraPackages; + serviceConfig.PIDFile = "/run/incrond.pid"; + serviceConfig.ExecStartPre = "${pkgs.coreutils}/bin/mkdir -m 710 -p /var/spool/incron"; + serviceConfig.ExecStart = "${pkgs.incron}/bin/incrond --foreground"; + }; + }; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/kapacitor.nix b/nixpkgs/nixos/modules/services/monitoring/kapacitor.nix new file mode 100644 index 000000000000..01919e73f734 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/kapacitor.nix @@ -0,0 +1,188 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.kapacitor; + + kapacitorConf = pkgs.writeTextFile { + name = "kapacitord.conf"; + text = '' + hostname="${config.networking.hostName}" + data_dir="${cfg.dataDir}" + + [http] + bind-address = "${cfg.bind}:${toString cfg.port}" + log-enabled = false + auth-enabled = false + + [task] + dir = "${cfg.dataDir}/tasks" + snapshot-interval = "${cfg.taskSnapshotInterval}" + + [replay] + dir = "${cfg.dataDir}/replay" + + [storage] + boltdb = "${cfg.dataDir}/kapacitor.db" + + ${optionalString (cfg.loadDirectory != null) '' + [load] + enabled = true + dir = "${cfg.loadDirectory}" + ''} + + ${optionalString (cfg.defaultDatabase.enable) '' + [[influxdb]] + name = "default" + enabled = true + default = true + urls = [ "${cfg.defaultDatabase.url}" ] + username = "${cfg.defaultDatabase.username}" + password = "${cfg.defaultDatabase.password}" + ''} + + ${optionalString (cfg.alerta.enable) '' + [alerta] + enabled = true + url = "${cfg.alerta.url}" + token = "${cfg.alerta.token}" + environment = "${cfg.alerta.environment}" + origin = "${cfg.alerta.origin}" + ''} + + ${cfg.extraConfig} + ''; + }; +in +{ + options.services.kapacitor = { + enable = mkEnableOption "kapacitor"; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/kapacitor"; + description = "Location where Kapacitor stores its state"; + }; + + port = mkOption { + type = types.port; + default = 9092; + description = "Port of Kapacitor"; + }; + + bind = mkOption { + type = types.str; + default = ""; + example = "0.0.0.0"; + description = "Address to bind to. The default is to bind to all addresses"; + }; + + extraConfig = mkOption { + description = "These lines go into kapacitord.conf verbatim."; + default = ""; + type = types.lines; + }; + + user = mkOption { + type = types.str; + default = "kapacitor"; + description = "User account under which Kapacitor runs"; + }; + + group = mkOption { + type = types.str; + default = "kapacitor"; + description = "Group under which Kapacitor runs"; + }; + + taskSnapshotInterval = mkOption { + type = types.str; + description = "Specifies how often to snapshot the task state (in InfluxDB time units)"; + default = "1m0s"; + }; + + loadDirectory = mkOption { + type = types.nullOr types.path; + description = "Directory where to load services from, such as tasks, templates and handlers (or null to disable service loading on startup)"; + default = null; + }; + + defaultDatabase = { + enable = mkEnableOption "kapacitor.defaultDatabase"; + + url = mkOption { + description = "The URL to an InfluxDB server that serves as the default database"; + example = "http://localhost:8086"; + type = types.str; + }; + + username = mkOption { + description = "The username to connect to the remote InfluxDB server"; + type = types.str; + }; + + password = mkOption { + description = "The password to connect to the remote InfluxDB server"; + type = types.str; + }; + }; + + alerta = { + enable = mkEnableOption "kapacitor alerta integration"; + + url = mkOption { + description = "The URL to the Alerta REST API"; + default = "http://localhost:5000"; + type = types.str; + }; + + token = mkOption { + description = "Default Alerta authentication token"; + type = types.str; + default = ""; + }; + + environment = mkOption { + description = "Default Alerta environment"; + type = types.str; + default = "Production"; + }; + + origin = mkOption { + description = "Default origin of alert"; + type = types.str; + default = "kapacitor"; + }; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [ pkgs.kapacitor ]; + + systemd.tmpfiles.settings."10-kapacitor".${cfg.dataDir}.d = { + inherit (cfg) user group; + }; + + systemd.services.kapacitor = { + description = "Kapacitor Real-Time Stream Processing Engine"; + wantedBy = [ "multi-user.target" ]; + after = [ "networking.target" ]; + serviceConfig = { + ExecStart = "${pkgs.kapacitor}/bin/kapacitord -config ${kapacitorConf}"; + User = "kapacitor"; + Group = "kapacitor"; + }; + }; + + users.users.kapacitor = { + uid = config.ids.uids.kapacitor; + description = "Kapacitor user"; + home = cfg.dataDir; + }; + + users.groups.kapacitor = { + gid = config.ids.gids.kapacitor; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/karma.nix b/nixpkgs/nixos/modules/services/monitoring/karma.nix new file mode 100644 index 000000000000..b7ec5e4ae6fb --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/karma.nix @@ -0,0 +1,121 @@ +{ config, pkgs, lib, ... }: +with lib; +let + cfg = config.services.karma; + yaml = pkgs.formats.yaml { }; +in +{ + options.services.karma = { + enable = mkEnableOption "the Karma dashboard service"; + + package = mkPackageOption pkgs "karma" { }; + + configFile = mkOption { + type = types.path; + default = yaml.generate "karma.yaml" cfg.settings; + defaultText = "A configuration file generated from the provided nix attributes settings option."; + description = '' + A YAML config file which can be used to configure karma instead of the nix-generated file. + ''; + example = "/etc/karma/karma.conf"; + }; + + environment = mkOption { + type = with types; attrsOf str; + default = {}; + description = '' + Additional environment variables to provide to karma. + ''; + example = { + ALERTMANAGER_URI = "https://alertmanager.example.com"; + ALERTMANAGER_NAME= "single"; + }; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = '' + Whether to open ports in the firewall needed for karma to function. + ''; + }; + + extraOptions = mkOption { + type = with types; listOf str; + default = []; + description = '' + Extra command line options. + ''; + example = [ + "--alertmanager.timeout 10s" + ]; + }; + + settings = mkOption { + type = types.submodule { + freeformType = yaml.type; + + options.listen = { + address = mkOption { + type = types.str; + default = "127.0.0.1"; + description = '' + Hostname or IP to listen on. + ''; + example = "[::]"; + }; + + port = mkOption { + type = types.port; + default = 8080; + description = '' + HTTP port to listen on. + ''; + example = 8182; + }; + }; + }; + default = { + listen = { + address = "127.0.0.1"; + }; + }; + description = '' + Karma dashboard configuration as nix attributes. + + Reference: <https://github.com/prymitive/karma/blob/main/docs/CONFIGURATION.md> + ''; + example = { + listen = { + address = "192.168.1.4"; + port = "8000"; + prefix = "/dashboard"; + }; + alertmanager = { + interval = "15s"; + servers = [ + { + name = "prod"; + uri = "http://alertmanager.example.com"; + } + ]; + }; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.karma = { + description = "Alert dashboard for Prometheus Alertmanager"; + wantedBy = [ "multi-user.target" ]; + environment = cfg.environment; + serviceConfig = { + Type = "simple"; + DynamicUser = true; + Restart = "on-failure"; + ExecStart = "${pkgs.karma}/bin/karma --config.file ${cfg.configFile} ${concatStringsSep " " cfg.extraOptions}"; + }; + }; + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.listen.port ]; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/kthxbye.nix b/nixpkgs/nixos/modules/services/monitoring/kthxbye.nix new file mode 100644 index 000000000000..81f47fba6679 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/kthxbye.nix @@ -0,0 +1,159 @@ +{ config, pkgs, lib, ... }: +with lib; + +let + cfg = config.services.kthxbye; +in + +{ + options.services.kthxbye = { + enable = mkEnableOption "kthxbye alert acknowledgement management daemon"; + + package = mkPackageOption pkgs "kthxbye" { }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = '' + Whether to open ports in the firewall needed for the daemon to function. + ''; + }; + + extraOptions = mkOption { + type = with types; listOf str; + default = []; + description = '' + Extra command line options. + + Documentation can be found [here](https://github.com/prymitive/kthxbye/blob/main/README.md). + ''; + example = literalExpression '' + [ + "-extend-with-prefix 'ACK!'" + ]; + ''; + }; + + alertmanager = { + timeout = mkOption { + type = types.str; + default = "1m0s"; + description = '' + Alertmanager request timeout duration in the [time.Duration](https://pkg.go.dev/time#ParseDuration) format. + ''; + example = "30s"; + }; + uri = mkOption { + type = types.str; + default = "http://localhost:9093"; + description = '' + Alertmanager URI to use. + ''; + example = "https://alertmanager.example.com"; + }; + }; + + extendBy = mkOption { + type = types.str; + default = "15m0s"; + description = '' + Extend silences by adding DURATION seconds. + + DURATION should be provided in the [time.Duration](https://pkg.go.dev/time#ParseDuration) format. + ''; + example = "6h0m0s"; + }; + + extendIfExpiringIn = mkOption { + type = types.str; + default = "5m0s"; + description = '' + Extend silences that are about to expire in the next DURATION seconds. + + DURATION should be provided in the [time.Duration](https://pkg.go.dev/time#ParseDuration) format. + ''; + example = "1m0s"; + }; + + extendWithPrefix = mkOption { + type = types.str; + default = "ACK!"; + description = '' + Extend silences with comment starting with PREFIX string. + ''; + example = "!perma-silence"; + }; + + interval = mkOption { + type = types.str; + default = "45s"; + description = '' + Silence check interval duration in the [time.Duration](https://pkg.go.dev/time#ParseDuration) format. + ''; + example = "30s"; + }; + + listenAddress = mkOption { + type = types.str; + default = "0.0.0.0"; + description = '' + The address to listen on for HTTP requests. + ''; + example = "127.0.0.1"; + }; + + port = mkOption { + type = types.port; + default = 8080; + description = '' + The port to listen on for HTTP requests. + ''; + }; + + logJSON = mkOption { + type = types.bool; + default = false; + description = '' + Format logged messages as JSON. + ''; + }; + + maxDuration = mkOption { + type = with types; nullOr str; + default = null; + description = '' + Maximum duration of a silence, it won't be extended anymore after reaching it. + + Duration should be provided in the [time.Duration](https://pkg.go.dev/time#ParseDuration) format. + ''; + example = "30d"; + }; + }; + + config = mkIf cfg.enable { + systemd.services.kthxbye = { + description = "kthxbye Alertmanager ack management daemon"; + wantedBy = [ "multi-user.target" ]; + script = '' + ${cfg.package}/bin/kthxbye \ + -alertmanager.timeout ${cfg.alertmanager.timeout} \ + -alertmanager.uri ${cfg.alertmanager.uri} \ + -extend-by ${cfg.extendBy} \ + -extend-if-expiring-in ${cfg.extendIfExpiringIn} \ + -extend-with-prefix ${cfg.extendWithPrefix} \ + -interval ${cfg.interval} \ + -listen ${cfg.listenAddress}:${toString cfg.port} \ + ${optionalString cfg.logJSON "-log-json"} \ + ${optionalString (cfg.maxDuration != null) "-max-duration ${cfg.maxDuration}"} \ + ${concatStringsSep " " cfg.extraOptions} + ''; + serviceConfig = { + Type = "simple"; + DynamicUser = true; + Restart = "on-failure"; + }; + }; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/librenms.nix b/nixpkgs/nixos/modules/services/monitoring/librenms.nix new file mode 100644 index 000000000000..08a46754e0e8 --- /dev/null +++ b/nixpkgs/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/nixpkgs/nixos/modules/services/monitoring/loki.nix b/nixpkgs/nixos/modules/services/monitoring/loki.nix new file mode 100644 index 000000000000..ba63f95e7f1a --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/loki.nix @@ -0,0 +1,116 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) escapeShellArgs mkEnableOption mkIf mkOption types; + + cfg = config.services.loki; + + prettyJSON = conf: + pkgs.runCommand "loki-config.json" { } '' + echo '${builtins.toJSON conf}' | ${pkgs.jq}/bin/jq 'del(._module)' > $out + ''; + +in { + options.services.loki = { + enable = mkEnableOption "loki"; + + user = mkOption { + type = types.str; + default = "loki"; + description = '' + User under which the Loki service runs. + ''; + }; + + package = lib.mkPackageOption pkgs "grafana-loki" { }; + + group = mkOption { + type = types.str; + default = "loki"; + description = '' + Group under which the Loki service runs. + ''; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/loki"; + description = '' + Specify the directory for Loki. + ''; + }; + + configuration = mkOption { + type = (pkgs.formats.json {}).type; + default = {}; + description = '' + Specify the configuration for Loki in Nix. + ''; + }; + + configFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Specify a configuration file that Loki should use. + ''; + }; + + extraFlags = mkOption { + type = types.listOf types.str; + default = []; + example = [ "--server.http-listen-port=3101" ]; + description = '' + Specify a list of additional command line flags, + which get escaped and are then passed to Loki. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [{ + assertion = ( + (cfg.configuration == {} -> cfg.configFile != null) && + (cfg.configFile != null -> cfg.configuration == {}) + ); + message = '' + Please specify either + 'services.loki.configuration' or + 'services.loki.configFile'. + ''; + }]; + + environment.systemPackages = [ cfg.package ]; # logcli + + users.groups.${cfg.group} = { }; + users.users.${cfg.user} = { + description = "Loki Service User"; + group = cfg.group; + home = cfg.dataDir; + createHome = true; + isSystemUser = true; + }; + + systemd.services.loki = { + description = "Loki Service Daemon"; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = let + conf = if cfg.configFile == null + then prettyJSON cfg.configuration + else cfg.configFile; + in + { + ExecStart = "${cfg.package}/bin/loki --config.file=${conf} ${escapeShellArgs cfg.extraFlags}"; + User = cfg.user; + Restart = "always"; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "full"; + DevicePolicy = "closed"; + NoNewPrivileges = true; + WorkingDirectory = cfg.dataDir; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/longview.nix b/nixpkgs/nixos/modules/services/monitoring/longview.nix new file mode 100644 index 000000000000..aafe54b994ab --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/longview.nix @@ -0,0 +1,160 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.longview; + + runDir = "/run/longview"; + configsDir = "${runDir}/longview.d"; + +in { + options = { + + services.longview = { + + enable = mkOption { + type = types.bool; + default = false; + description = '' + If enabled, system metrics will be sent to Linode LongView. + ''; + }; + + apiKey = mkOption { + type = types.str; + default = ""; + example = "01234567-89AB-CDEF-0123456789ABCDEF"; + description = '' + Longview API key. To get this, look in Longview settings which + are found at https://manager.linode.com/longview/. + + Warning: this secret is stored in the world-readable Nix store! + Use {option}`apiKeyFile` instead. + ''; + }; + + apiKeyFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/keys/longview-api-key"; + description = '' + A file containing the Longview API key. + To get this, look in Longview settings which + are found at https://manager.linode.com/longview/. + + {option}`apiKeyFile` takes precedence over {option}`apiKey`. + ''; + }; + + apacheStatusUrl = mkOption { + type = types.str; + default = ""; + example = "http://127.0.0.1/server-status"; + description = '' + The Apache status page URL. If provided, Longview will + gather statistics from this location. This requires Apache + mod_status to be loaded and enabled. + ''; + }; + + nginxStatusUrl = mkOption { + type = types.str; + default = ""; + example = "http://127.0.0.1/nginx_status"; + description = '' + The Nginx status page URL. Longview will gather statistics + from this URL. This requires the Nginx stub_status module to + be enabled and configured at the given location. + ''; + }; + + mysqlUser = mkOption { + type = types.str; + default = ""; + description = '' + The user for connecting to the MySQL database. If provided, + Longview will connect to MySQL and collect statistics about + queries, etc. This user does not need to have been granted + any extra privileges. + ''; + }; + + mysqlPassword = mkOption { + type = types.str; + default = ""; + description = '' + The password corresponding to {option}`mysqlUser`. + Warning: this is stored in cleartext in the Nix store! + Use {option}`mysqlPasswordFile` instead. + ''; + }; + + mysqlPasswordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/keys/dbpassword"; + description = '' + A file containing the password corresponding to {option}`mysqlUser`. + ''; + }; + + }; + + }; + + config = mkIf cfg.enable { + systemd.services.longview = + { description = "Longview Metrics Collection"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig.Type = "forking"; + serviceConfig.ExecStop = "-${pkgs.coreutils}/bin/kill -TERM $MAINPID"; + serviceConfig.ExecReload = "-${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + serviceConfig.PIDFile = "${runDir}/longview.pid"; + serviceConfig.ExecStart = "${pkgs.longview}/bin/longview"; + preStart = '' + umask 077 + mkdir -p ${configsDir} + '' + (optionalString (cfg.apiKeyFile != null) '' + cp --no-preserve=all "${cfg.apiKeyFile}" ${runDir}/longview.key + '') + (optionalString (cfg.apacheStatusUrl != "") '' + cat > ${configsDir}/Apache.conf <<EOF + location ${cfg.apacheStatusUrl}?auto + EOF + '') + (optionalString (cfg.mysqlUser != "" && cfg.mysqlPasswordFile != null) '' + cat > ${configsDir}/MySQL.conf <<EOF + username ${cfg.mysqlUser} + password `head -n1 "${cfg.mysqlPasswordFile}"` + EOF + '') + (optionalString (cfg.nginxStatusUrl != "") '' + cat > ${configsDir}/Nginx.conf <<EOF + location ${cfg.nginxStatusUrl} + EOF + ''); + }; + + warnings = let warn = k: optional (cfg.${k} != "") + "config.services.longview.${k} is insecure. Use ${k}File instead."; + in concatMap warn [ "apiKey" "mysqlPassword" ]; + + assertions = [ + { assertion = cfg.apiKeyFile != null; + message = "Longview needs an API key configured"; + } + ]; + + # Create API key file if not configured. + services.longview.apiKeyFile = mkIf (cfg.apiKey != "") + (mkDefault (toString (pkgs.writeTextFile { + name = "longview.key"; + text = cfg.apiKey; + }))); + + # Create MySQL password file if not configured. + services.longview.mysqlPasswordFile = mkDefault (toString (pkgs.writeTextFile { + name = "mysql-password-file"; + text = cfg.mysqlPassword; + })); + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/mackerel-agent.nix b/nixpkgs/nixos/modules/services/monitoring/mackerel-agent.nix new file mode 100644 index 000000000000..d218c18c4354 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/mackerel-agent.nix @@ -0,0 +1,111 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.mackerel-agent; + settingsFmt = pkgs.formats.toml {}; +in { + options.services.mackerel-agent = { + enable = mkEnableOption "mackerel.io agent"; + + # the upstream package runs as root, but doesn't seem to be strictly + # necessary for basic functionality + runAsRoot = mkEnableOption "running as root"; + + autoRetirement = mkEnableOption '' + retiring the host upon OS shutdown + ''; + + apiKeyFile = mkOption { + type = types.path; + example = "/run/keys/mackerel-api-key"; + description = '' + Path to file containing the Mackerel API key. The file should contain a + single line of the following form: + + `apikey = "EXAMPLE_API_KEY"` + ''; + }; + + settings = mkOption { + description = '' + Options for mackerel-agent.conf. + + Documentation: + <https://mackerel.io/docs/entry/spec/agent> + ''; + + default = {}; + example = { + verbose = false; + silent = false; + }; + + type = types.submodule { + freeformType = settingsFmt.type; + + options.host_status = { + on_start = mkOption { + type = types.enum [ "working" "standby" "maintenance" "poweroff" ]; + description = "Host status after agent startup."; + default = "working"; + }; + on_stop = mkOption { + type = types.enum [ "working" "standby" "maintenance" "poweroff" ]; + description = "Host status after agent shutdown."; + default = "poweroff"; + }; + }; + + options.diagnostic = + mkEnableOption "collecting memory usage for the agent itself"; + }; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = with pkgs; [ mackerel-agent ]; + + environment.etc = { + "mackerel-agent/mackerel-agent.conf".source = + settingsFmt.generate "mackerel-agent.conf" cfg.settings; + "mackerel-agent/conf.d/api-key.conf".source = cfg.apiKeyFile; + }; + + services.mackerel-agent.settings = { + root = mkDefault "/var/lib/mackerel-agent"; + pidfile = mkDefault "/run/mackerel-agent/mackerel-agent.pid"; + + # conf.d stores the symlink to cfg.apiKeyFile + include = mkDefault "/etc/mackerel-agent/conf.d/*.conf"; + }; + + # upstream service file in https://github.com/mackerelio/mackerel-agent/blob/master/packaging/rpm/src/mackerel-agent.service + systemd.services.mackerel-agent = { + description = "mackerel.io agent"; + wants = [ "network-online.target" ]; + after = [ "network-online.target" "nss-lookup.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = { + MACKEREL_PLUGIN_WORKDIR = mkDefault "%C/mackerel-agent"; + }; + serviceConfig = { + DynamicUser = !cfg.runAsRoot; + PrivateTmp = mkDefault true; + CacheDirectory = "mackerel-agent"; + ConfigurationDirectory = "mackerel-agent"; + RuntimeDirectory = "mackerel-agent"; + StateDirectory = "mackerel-agent"; + ExecStart = "${pkgs.mackerel-agent}/bin/mackerel-agent supervise"; + ExecStopPost = mkIf cfg.autoRetirement "${pkg.mackerel-agent}/bin/mackerel-agent retire -force"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + LimitNOFILE = mkDefault 65536; + LimitNPROC = mkDefault 65536; + }; + restartTriggers = [ + config.environment.etc."mackerel-agent/mackerel-agent.conf".source + ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/metricbeat.nix b/nixpkgs/nixos/modules/services/monitoring/metricbeat.nix new file mode 100644 index 000000000000..fe6f7f1cfcfa --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/metricbeat.nix @@ -0,0 +1,146 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) + attrValues + literalExpression + mkEnableOption + mkPackageOption + mkIf + mkOption + types + ; + cfg = config.services.metricbeat; + + settingsFormat = pkgs.formats.yaml {}; + +in +{ + options = { + + services.metricbeat = { + + enable = mkEnableOption "metricbeat"; + + package = mkPackageOption pkgs "metricbeat" { + example = "metricbeat7"; + }; + + modules = mkOption { + description = '' + Metricbeat modules are responsible for reading metrics from the various sources. + + This is like `services.metricbeat.settings.metricbeat.modules`, + but structured as an attribute set. This has the benefit that multiple + NixOS modules can contribute settings to a single metricbeat module. + + A module can be specified multiple times by choosing a different `<name>` + for each, but setting [](#opt-services.metricbeat.modules._name_.module) to the same value. + + See <https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-modules.html>. + ''; + default = {}; + type = types.attrsOf (types.submodule ({ name, ... }: { + freeformType = settingsFormat.type; + options = { + module = mkOption { + type = types.str; + default = name; + description = '' + The name of the module. + + Look for the value after `module:` on the individual + module pages linked from <https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-modules.html>. + ''; + }; + }; + })); + example = { + system = { + metricsets = ["cpu" "load" "memory" "network" "process" "process_summary" "uptime" "socket_summary"]; + enabled = true; + period = "10s"; + processes = [".*"]; + cpu.metrics = ["percentages" "normalized_percentages"]; + core.metrics = ["percentages"]; + }; + }; + }; + + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = { + + name = mkOption { + type = types.str; + default = ""; + description = '' + Name of the beat. Defaults to the hostname. + See <https://www.elastic.co/guide/en/beats/metricbeat/current/configuration-general-options.html#_name>. + ''; + }; + + tags = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Tags to place on the shipped metrics. + See <https://www.elastic.co/guide/en/beats/metricbeat/current/configuration-general-options.html#_tags_2>. + ''; + }; + + metricbeat.modules = mkOption { + type = types.listOf settingsFormat.type; + default = []; + internal = true; + description = '' + The metric collecting modules. Use [](#opt-services.metricbeat.modules) instead. + + See <https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-modules.html>. + ''; + }; + }; + }; + default = {}; + description = '' + Configuration for metricbeat. See <https://www.elastic.co/guide/en/beats/metricbeat/current/configuring-howto-metricbeat.html> for supported values. + ''; + }; + + }; + }; + + config = mkIf cfg.enable { + + assertions = [ + { + # empty modules would cause a failure at runtime + assertion = cfg.settings.metricbeat.modules != []; + message = "services.metricbeat: You must configure one or more modules."; + } + ]; + + services.metricbeat.settings.metricbeat.modules = attrValues cfg.modules; + + systemd.services.metricbeat = { + description = "metricbeat metrics shipper"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = '' + ${cfg.package}/bin/metricbeat \ + -c ${settingsFormat.generate "metricbeat.yml" cfg.settings} \ + --path.data $STATE_DIRECTORY \ + --path.logs $LOGS_DIRECTORY \ + ; + ''; + Restart = "always"; + DynamicUser = true; + ProtectSystem = "strict"; + ProtectHome = "tmpfs"; + StateDirectory = "metricbeat"; + LogsDirectory = "metricbeat"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/mimir.nix b/nixpkgs/nixos/modules/services/monitoring/mimir.nix new file mode 100644 index 000000000000..76fff95ae597 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/mimir.nix @@ -0,0 +1,79 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) escapeShellArgs mkEnableOption mkPackageOption mkIf mkOption types; + + cfg = config.services.mimir; + + settingsFormat = pkgs.formats.yaml {}; +in { + options.services.mimir = { + enable = mkEnableOption "mimir"; + + configuration = mkOption { + type = (pkgs.formats.json {}).type; + default = {}; + description = '' + Specify the configuration for Mimir in Nix. + ''; + }; + + configFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Specify a configuration file that Mimir should use. + ''; + }; + + package = mkPackageOption pkgs "mimir" { }; + + extraFlags = mkOption { + type = types.listOf types.str; + default = []; + example = [ "--config.expand-env=true" ]; + description = '' + Specify a list of additional command line flags, + which get escaped and are then passed to Mimir. + ''; + }; + }; + + config = mkIf cfg.enable { + # for mimirtool + environment.systemPackages = [ cfg.package ]; + + assertions = [{ + assertion = ( + (cfg.configuration == {} -> cfg.configFile != null) && + (cfg.configFile != null -> cfg.configuration == {}) + ); + message = '' + Please specify either + 'services.mimir.configuration' or + 'services.mimir.configFile'. + ''; + }]; + + systemd.services.mimir = { + description = "mimir Service Daemon"; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = let + conf = if cfg.configFile == null + then settingsFormat.generate "config.yaml" cfg.configuration + else cfg.configFile; + in + { + ExecStart = "${cfg.package}/bin/mimir --config.file=${conf} ${escapeShellArgs cfg.extraFlags}"; + DynamicUser = true; + Restart = "always"; + ProtectSystem = "full"; + DevicePolicy = "closed"; + NoNewPrivileges = true; + WorkingDirectory = "/var/lib/mimir"; + StateDirectory = "mimir"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/monit.nix b/nixpkgs/nixos/modules/services/monitoring/monit.nix new file mode 100644 index 000000000000..379ee967620e --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/monit.nix @@ -0,0 +1,48 @@ +{config, pkgs, lib, ...}: + +with lib; + +let + cfg = config.services.monit; +in + +{ + options.services.monit = { + + enable = mkEnableOption "Monit"; + + config = mkOption { + type = types.lines; + default = ""; + description = "monitrc content"; + }; + + }; + + config = mkIf cfg.enable { + + environment.systemPackages = [ pkgs.monit ]; + + environment.etc.monitrc = { + text = cfg.config; + mode = "0400"; + }; + + systemd.services.monit = { + description = "Pro-active monitoring utility for unix systems"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${pkgs.monit}/bin/monit -I -c /etc/monitrc"; + ExecStop = "${pkgs.monit}/bin/monit -c /etc/monitrc quit"; + ExecReload = "${pkgs.monit}/bin/monit -c /etc/monitrc reload"; + KillMode = "process"; + Restart = "always"; + }; + restartTriggers = [ config.environment.etc.monitrc.source ]; + }; + + }; + + meta.maintainers = with maintainers; [ ryantm ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/munin.nix b/nixpkgs/nixos/modules/services/monitoring/munin.nix new file mode 100644 index 000000000000..401f6383cb57 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/munin.nix @@ -0,0 +1,419 @@ +{ config, lib, pkgs, ... }: + +# TODO: support munin-async +# TODO: LWP/Pg perl libs aren't recognized + +# TODO: support fastcgi +# 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 +# nginx https://munin.readthedocs.org/en/latest/example/webserver/nginx.html + + +with lib; + +let + nodeCfg = config.services.munin-node; + cronCfg = config.services.munin-cron; + + muninConf = pkgs.writeText "munin.conf" + '' + dbdir /var/lib/munin + htmldir /var/www/munin + logdir /var/log/munin + rundir /run/munin + + ${lib.optionalString (cronCfg.extraCSS != "") "staticdir ${customStaticDir}"} + + ${cronCfg.extraGlobalConfig} + + ${cronCfg.hosts} + ''; + + nodeConf = pkgs.writeText "munin-node.conf" + '' + log_level 3 + log_file Sys::Syslog + port 4949 + host * + background 0 + user root + group root + host_name ${config.networking.hostName} + setsid 0 + + # wrapped plugins by makeWrapper being with dots + ignore_file ^\. + + allow ^::1$ + allow ^127\.0\.0\.1$ + + ${nodeCfg.extraConfig} + ''; + + pluginConf = pkgs.writeText "munin-plugin-conf" + '' + [hddtemp_smartctl] + user root + group root + + [meminfo] + user root + group root + + [ipmi*] + user root + group root + + [munin*] + env.UPDATE_STATSFILE /var/lib/munin/munin-update.stats + + ${nodeCfg.extraPluginConfig} + ''; + + pluginConfDir = pkgs.stdenv.mkDerivation { + name = "munin-plugin-conf.d"; + buildCommand = '' + mkdir $out + ln -s ${pluginConf} $out/nixos-config + ''; + }; + + # 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 }: + "cp -a '${path}' '${name}'"; + + # Copy an entire tree of Munin plugins into a single directory in the Nix + # 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" (map intern-fn paths)} + chmod -R u+w . + ${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 + (lib.attrsets.mapAttrsToList (k: v: { name = k; path = v; }) nodeCfg.extraPlugins); + + extraAutoPluginDir = internAndFixPlugins "munin-extra-auto-plugins.d" + internManyPlugins nodeCfg.extraAutoPlugins; + + customStaticDir = pkgs.runCommand "munin-custom-static-data" {} '' + cp -a "${pkgs.munin}/etc/opt/munin/static" "$out" + cd "$out" + chmod -R u+w . + echo "${cronCfg.extraCSS}" >> style.css + echo "${cronCfg.extraCSS}" >> style-new.css + ''; +in + +{ + + options = { + + services.munin-node = { + + enable = mkOption { + default = false; + type = types.bool; + description = '' + 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 <https://guide.munin-monitoring.org/en/latest/architecture/index.html>. + ''; + }; + + extraConfig = mkOption { + default = ""; + type = types.lines; + description = '' + {file}`munin-node.conf` extra configuration. See + <https://guide.munin-monitoring.org/en/latest/reference/munin-node.conf.html> + ''; + }; + + extraPluginConfig = mkOption { + default = ""; + type = types.lines; + description = '' + {file}`plugin-conf.d` extra plugin configuration. See + <https://guide.munin-monitoring.org/en/latest/plugin/use.html> + ''; + example = '' + [fail2ban_*] + user root + ''; + }; + + extraPlugins = mkOption { + default = {}; + type = with types; attrsOf path; + description = '' + Additional Munin plugins to activate. Keys are the name of the plugin + symlink, values are the path to the underlying plugin script. You + can use the same plugin script multiple times (e.g. for wildcard + plugins). + + Note that these plugins do not participate in autoconfiguration. If + you want to autoconfigure additional plugins, use + {option}`services.munin-node.extraAutoPlugins`. + + Plugins enabled in this manner take precedence over autoconfigured + plugins. + + Plugins will be copied into the Nix store, and it will attempt to + modify them to run properly by fixing hardcoded references to + `/bin`, `/usr/bin`, + `/sbin`, and `/usr/sbin`. + ''; + example = literalExpression '' + { + zfs_usage_bigpool = /src/munin-contrib/plugins/zfs/zfs_usage_; + zfs_usage_smallpool = /src/munin-contrib/plugins/zfs/zfs_usage_; + zfs_list = /src/munin-contrib/plugins/zfs/zfs_list; + }; + ''; + }; + + extraAutoPlugins = mkOption { + default = []; + type = with types; listOf path; + description = '' + Additional Munin plugins to autoconfigure, using + `munin-node-configure --suggest`. These should be + the actual paths to the plugin files (or directories containing them), + not just their names. + + If you want to manually enable individual plugins instead, use + {option}`services.munin-node.extraPlugins`. + + Note that only plugins that have the 'autoconfig' capability will do + anything if listed here, since plugins that cannot autoconfigure + won't be automatically enabled by + `munin-node-configure`. + + Plugins will be copied into the Nix store, and it will attempt to + modify them to run properly by fixing hardcoded references to + `/bin`, `/usr/bin`, + `/sbin`, and `/usr/sbin`. + ''; + example = literalExpression '' + [ + /src/munin-contrib/plugins/zfs + /src/munin-contrib/plugins/ssh + ]; + ''; + }; + + disabledPlugins = mkOption { + # TODO: figure out why Munin isn't writing the log file and fix it. + # In the meantime this at least suppresses a useless graph full of + # NaNs in the output. + default = [ "munin_stats" ]; + type = with types; listOf str; + description = '' + Munin plugins to disable, even if + `munin-node-configure --suggest` tries to enable + them. To disable a wildcard plugin, use an actual wildcard, as in + the example. + + munin_stats is disabled by default as it tries to read + `/var/log/munin/munin-update.log` for timing + information, and the NixOS build of Munin does not write this file. + ''; + example = [ "diskstats" "zfs_usage_*" ]; + }; + }; + + services.munin-cron = { + + enable = mkOption { + default = false; + type = types.bool; + description = '' + Enable munin-cron. Takes care of all heavy lifting to collect data from + nodes and draws graphs to html. Runs munin-update, munin-limits, + munin-graphs and munin-html in that order. + + HTML output is in {file}`/var/www/munin/`, configure your + favourite webserver to serve static files. + ''; + }; + + extraGlobalConfig = mkOption { + default = ""; + type = types.lines; + description = '' + {file}`munin.conf` extra global configuration. + See <https://guide.munin-monitoring.org/en/latest/reference/munin.conf.html>. + Useful to setup notifications, see + <https://guide.munin-monitoring.org/en/latest/tutorial/alert.html> + ''; + example = '' + contact.email.command mail -s "Munin notification for ''${var:host}" someone@example.com + ''; + }; + + hosts = mkOption { + default = ""; + type = types.lines; + description = '' + Definitions of hosts of nodes to collect data from. Needs at least one + host for cron to succeed. See + <https://guide.munin-monitoring.org/en/latest/reference/munin.conf.html> + ''; + example = literalExpression '' + ''' + [''${config.networking.hostName}] + address localhost + ''' + ''; + }; + + extraCSS = mkOption { + default = ""; + type = types.lines; + description = '' + Custom styling for the HTML that munin-cron generates. This will be + appended to the CSS files used by munin-cron and will thus take + precedence over the builtin styles. + ''; + example = '' + /* A simple dark theme. */ + html, body { background: #222222; } + #header, #footer { background: #333333; } + img.i, img.iwarn, img.icrit, img.iunkn { + filter: invert(100%) hue-rotate(-30deg); + } + ''; + }; + + }; + + }; + + config = mkMerge [ (mkIf (nodeCfg.enable || cronCfg.enable) { + + environment.systemPackages = [ pkgs.munin ]; + + users.users.munin = { + description = "Munin monitoring user"; + group = "munin"; + uid = config.ids.uids.munin; + home = "/var/lib/munin"; + }; + + users.groups.munin = { + gid = config.ids.gids.munin; + }; + + }) (mkIf nodeCfg.enable { + + systemd.services.munin-node = { + description = "Munin Node"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = with pkgs; [ munin smartmontools "/run/current-system/sw" "/run/wrappers" ]; + environment.MUNIN_LIBDIR = "${pkgs.munin}/lib"; + environment.MUNIN_PLUGSTATE = "/run/munin"; + environment.MUNIN_LOGDIR = "/var/log/munin"; + preStart = '' + echo "Updating munin plugins..." + + mkdir -p /etc/munin/plugins + rm -rf /etc/munin/plugins/* + + # Autoconfigure builtin plugins + ${pkgs.munin}/bin/munin-node-configure --suggest --shell --families contrib,auto,manual --config ${nodeConf} --libdir=${pkgs.munin}/lib/plugins --servicedir=/etc/munin/plugins --sconfdir=${pluginConfDir} 2>/dev/null | ${pkgs.bash}/bin/bash + + # Autoconfigure extra plugins + ${pkgs.munin}/bin/munin-node-configure --suggest --shell --families contrib,auto,manual --config ${nodeConf} --libdir=${extraAutoPluginDir} --servicedir=/etc/munin/plugins --sconfdir=${pluginConfDir} 2>/dev/null | ${pkgs.bash}/bin/bash + + ${lib.optionalString (nodeCfg.extraPlugins != {}) '' + # Link in manually enabled plugins + ln -f -s -t /etc/munin/plugins ${extraPluginDir}/* + ''} + + ${lib.optionalString (nodeCfg.disabledPlugins != []) '' + # Disable plugins + cd /etc/munin/plugins + rm -f ${toString nodeCfg.disabledPlugins} + ''} + ''; + serviceConfig = { + ExecStart = "${pkgs.munin}/sbin/munin-node --config ${nodeConf} --servicedir /etc/munin/plugins/ --sconfdir=${pluginConfDir}"; + }; + }; + + # munin_stats plugin breaks as of 2.0.33 when this doesn't exist + systemd.tmpfiles.settings."10-munin"."/run/munin".d = { + mode = "0755"; + user = "munin"; + group = "munin"; + }; + + }) (mkIf cronCfg.enable { + + # Munin is hardcoded to use DejaVu Mono and the graphs come out wrong if + # it's not available. + fonts.packages = [ pkgs.dejavu_fonts ]; + + systemd.timers.munin-cron = { + description = "batch Munin master programs"; + wantedBy = [ "timers.target" ]; + timerConfig.OnCalendar = "*:0/5"; + }; + + systemd.services.munin-cron = { + description = "batch Munin master programs"; + unitConfig.Documentation = "man:munin-cron(8)"; + + serviceConfig = { + Type = "oneshot"; + User = "munin"; + ExecStart = "${pkgs.munin}/bin/munin-cron --config ${muninConf}"; + }; + }; + + systemd.tmpfiles.settings."20-munin" = let + defaultConfig = { + mode = "0755"; + user = "munin"; + group = "munin"; + }; + in { + "/run/munin".d = defaultConfig; + "/var/log/munin".d = defaultConfig; + "/var/www/munin".d = defaultConfig; + "/var/lib/munin".d = defaultConfig; + }; + })]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/nagios.nix b/nixpkgs/nixos/modules/services/monitoring/nagios.nix new file mode 100644 index 000000000000..27fc0a1ff3b9 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/nagios.nix @@ -0,0 +1,213 @@ +# Nagios system/network monitoring daemon. +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.nagios; + + nagiosState = "/var/lib/nagios"; + nagiosLogDir = "/var/log/nagios"; + urlPath = "/nagios"; + + nagiosObjectDefs = cfg.objectDefs; + + nagiosObjectDefsDir = pkgs.runCommand "nagios-objects" { + inherit nagiosObjectDefs; + preferLocalBuild = true; + } "mkdir -p $out; ln -s $nagiosObjectDefs $out/"; + + nagiosCfgFile = let + default = { + log_file="${nagiosLogDir}/current"; + log_archive_path="${nagiosLogDir}/archive"; + status_file="${nagiosState}/status.dat"; + object_cache_file="${nagiosState}/objects.cache"; + temp_file="${nagiosState}/nagios.tmp"; + lock_file="/run/nagios.lock"; + state_retention_file="${nagiosState}/retention.dat"; + query_socket="${nagiosState}/nagios.qh"; + check_result_path="${nagiosState}"; + command_file="${nagiosState}/nagios.cmd"; + cfg_dir="${nagiosObjectDefsDir}"; + nagios_user="nagios"; + nagios_group="nagios"; + illegal_macro_output_chars="`~$&|'\"<>"; + retain_state_information="1"; + }; + lines = mapAttrsToList (key: value: "${key}=${value}") (default // cfg.extraConfig); + content = concatStringsSep "\n" lines; + file = pkgs.writeText "nagios.cfg" content; + validated = pkgs.runCommand "nagios-checked.cfg" {preferLocalBuild=true;} '' + cp ${file} nagios.cfg + # nagios checks the existence of /var/lib/nagios, but + # it does not exist in the build sandbox, so we fake it + mkdir lib + lib=$(readlink -f lib) + sed -i s@=${nagiosState}@=$lib@ nagios.cfg + ${pkgs.nagios}/bin/nagios -v nagios.cfg && cp ${file} $out + ''; + defaultCfgFile = if cfg.validateConfig then validated else file; + in + if cfg.mainConfigFile == null then defaultCfgFile else cfg.mainConfigFile; + + # Plain configuration for the Nagios web-interface with no + # authentication. + nagiosCGICfgFile = pkgs.writeText "nagios.cgi.conf" + '' + main_config_file=${cfg.mainConfigFile} + use_authentication=0 + url_html_path=${urlPath} + ''; + + extraHttpdConfig = + '' + ScriptAlias ${urlPath}/cgi-bin ${pkgs.nagios}/sbin + + <Directory "${pkgs.nagios}/sbin"> + Options ExecCGI + Require all granted + SetEnv NAGIOS_CGI_CONFIG ${cfg.cgiConfigFile} + </Directory> + + Alias ${urlPath} ${pkgs.nagios}/share + + <Directory "${pkgs.nagios}/share"> + Options None + Require all granted + </Directory> + ''; + +in +{ + imports = [ + (mkRemovedOptionModule [ "services" "nagios" "urlPath" ] "The urlPath option has been removed as it is hard coded to /nagios in the nagios package.") + ]; + + meta.maintainers = with lib.maintainers; [ symphorien ]; + + options = { + services.nagios = { + enable = mkEnableOption ''[Nagios](https://www.nagios.org/) to monitor your system or network.''; + + objectDefs = mkOption { + description = '' + A list of Nagios object configuration files that must define + the hosts, host groups, services and contacts for the + network that you want Nagios to monitor. + ''; + type = types.listOf types.path; + example = literalExpression "[ ./objects.cfg ]"; + }; + + plugins = mkOption { + type = types.listOf types.package; + default = with pkgs; [ monitoring-plugins msmtp mailutils ]; + defaultText = literalExpression "[pkgs.monitoring-plugins pkgs.msmtp pkgs.mailutils]"; + description = '' + Packages to be added to the Nagios {env}`PATH`. + Typically used to add plugins, but can be anything. + ''; + }; + + mainConfigFile = mkOption { + type = types.nullOr types.package; + default = null; + description = '' + If non-null, overrides the main configuration file of Nagios. + ''; + }; + + extraConfig = mkOption { + type = types.attrsOf types.str; + example = { + debug_level = "-1"; + debug_file = "/var/log/nagios/debug.log"; + }; + default = {}; + description = "Configuration to add to /etc/nagios.cfg"; + }; + + validateConfig = mkOption { + type = types.bool; + default = pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform; + defaultText = literalExpression "pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform"; + description = "if true, the syntax of the nagios configuration file is checked at build time"; + }; + + cgiConfigFile = mkOption { + type = types.package; + default = nagiosCGICfgFile; + defaultText = literalExpression "nagiosCGICfgFile"; + description = '' + Derivation for the configuration file of Nagios CGI scripts + that can be used in web servers for running the Nagios web interface. + ''; + }; + + enableWebInterface = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable the Nagios web interface. You should also + enable Apache ({option}`services.httpd.enable`). + ''; + }; + + virtualHost = mkOption { + type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix); + example = literalExpression '' + { hostName = "example.org"; + adminAddr = "webmaster@example.org"; + enableSSL = true; + sslServerCert = "/var/lib/acme/example.org/full.pem"; + sslServerKey = "/var/lib/acme/example.org/key.pem"; + } + ''; + description = '' + Apache configuration can be done by adapting {option}`services.httpd.virtualHosts`. + See [](#opt-services.httpd.virtualHosts) for further information. + ''; + }; + }; + }; + + + config = mkIf cfg.enable { + users.users.nagios = { + description = "Nagios user "; + uid = config.ids.uids.nagios; + home = nagiosState; + group = "nagios"; + }; + + users.groups.nagios = { }; + + # This isn't needed, it's just so that the user can type "nagiostats + # -c /etc/nagios.cfg". + environment.etc."nagios.cfg".source = nagiosCfgFile; + + environment.systemPackages = [ pkgs.nagios ]; + systemd.services.nagios = { + description = "Nagios monitoring daemon"; + path = [ pkgs.nagios ] ++ cfg.plugins; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + restartTriggers = [ nagiosCfgFile ]; + + serviceConfig = { + User = "nagios"; + Group = "nagios"; + Restart = "always"; + RestartSec = 2; + LogsDirectory = "nagios"; + StateDirectory = "nagios"; + ExecStart = "${pkgs.nagios}/bin/nagios /etc/nagios.cfg"; + }; + }; + + services.httpd.virtualHosts = optionalAttrs cfg.enableWebInterface { + ${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost { extraConfig = extraHttpdConfig; } ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/netdata.nix b/nixpkgs/nixos/modules/services/monitoring/netdata.nix new file mode 100644 index 000000000000..90e00e91deed --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/netdata.nix @@ -0,0 +1,370 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.netdata; + + wrappedPlugins = pkgs.runCommand "wrapped-plugins" { preferLocalBuild = true; } '' + mkdir -p $out/libexec/netdata/plugins.d + ln -s /run/wrappers/bin/apps.plugin $out/libexec/netdata/plugins.d/apps.plugin + ln -s /run/wrappers/bin/cgroup-network $out/libexec/netdata/plugins.d/cgroup-network + 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 = [ + "${cfg.package}/libexec/netdata/plugins.d" + "${wrappedPlugins}/libexec/netdata/plugins.d" + ] ++ cfg.extraPluginPaths; + + configDirectory = pkgs.runCommand "netdata-config-d" { } '' + mkdir $out + ${concatStringsSep "\n" (mapAttrsToList (path: file: '' + mkdir -p "$out/$(dirname ${path})" + ln -s "${file}" "$out/${path}" + '') cfg.configDir)} + ''; + + localConfig = { + global = { + "config directory" = "/etc/netdata/conf.d"; + "plugins directory" = concatStringsSep " " plugins; + }; + web = { + "web files owner" = "root"; + "web files group" = "root"; + }; + "plugin:cgroups" = { + "script to get cgroup network interfaces" = "${wrappedPlugins}/libexec/netdata/plugins.d/cgroup-network"; + "use unified cgroups" = "yes"; + }; + }; + mkConfig = generators.toINI {} (recursiveUpdate localConfig cfg.config); + configFile = pkgs.writeText "netdata.conf" (if cfg.configText != null then cfg.configText else mkConfig); + + defaultUser = "netdata"; + +in { + options = { + services.netdata = { + enable = mkEnableOption "netdata"; + + package = mkPackageOption pkgs "netdata" { }; + + user = mkOption { + type = types.str; + default = "netdata"; + description = "User account under which netdata runs."; + }; + + group = mkOption { + type = types.str; + default = "netdata"; + description = "Group under which netdata runs."; + }; + + configText = mkOption { + type = types.nullOr types.lines; + description = "Verbatim netdata.conf, cannot be combined with config."; + default = null; + example = '' + [global] + debug log = syslog + access log = syslog + error log = syslog + ''; + }; + + python = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable python-based plugins + ''; + }; + extraPackages = mkOption { + type = types.functionTo (types.listOf types.package); + default = ps: []; + defaultText = literalExpression "ps: []"; + example = literalExpression '' + ps: [ + ps.psycopg2 + ps.docker + ps.dnspython + ] + ''; + description = '' + Extra python packages available at runtime + to enable additional python plugins. + ''; + }; + }; + + extraPluginPaths = mkOption { + type = types.listOf types.path; + default = [ ]; + example = literalExpression '' + [ "/path/to/plugins.d" ] + ''; + description = '' + Extra paths to add to the netdata global "plugins directory" + option. Useful for when you want to include your own + collection scripts. + + Details about writing a custom netdata plugin are available at: + <https://docs.netdata.cloud/collectors/plugins.d/> + + Cannot be combined with configText. + ''; + }; + + config = mkOption { + type = types.attrsOf types.attrs; + default = {}; + description = "netdata.conf configuration as nix attributes. cannot be combined with configText."; + example = literalExpression '' + global = { + "debug log" = "syslog"; + "access log" = "syslog"; + "error log" = "syslog"; + }; + ''; + }; + + configDir = mkOption { + type = types.attrsOf types.path; + default = {}; + description = '' + Complete netdata config directory except netdata.conf. + The default configuration is merged with changes + defined in this option. + Each top-level attribute denotes a path in the configuration + directory as in environment.etc. + Its value is the absolute path and must be readable by netdata. + Cannot be combined with configText. + ''; + example = literalExpression '' + "health_alarm_notify.conf" = pkgs.writeText "health_alarm_notify.conf" ''' + sendmail="/path/to/sendmail" + '''; + "health.d" = "/run/secrets/netdata/health.d"; + ''; + }; + + claimTokenFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + If set, automatically registers the agent using the given claim token + file. + ''; + }; + + enableAnalyticsReporting = mkOption { + type = types.bool; + default = false; + description = '' + Enable reporting of anonymous usage statistics to Netdata Inc. via either + Google Analytics (in versions prior to 1.29.4), or Netdata Inc.'s + self-hosted PostHog (in versions 1.29.4 and later). + See: <https://learn.netdata.cloud/docs/agent/anonymous-statistics> + ''; + }; + + deadlineBeforeStopSec = mkOption { + type = types.int; + default = 120; + description = '' + In order to detect when netdata is misbehaving, we run a concurrent task pinging netdata (wait-for-netdata-up) + in the systemd unit. + + If after a while, this task does not succeed, we stop the unit and mark it as failed. + + You can control this deadline in seconds with this option, it's useful to bump it + if you have (1) a lot of data (2) doing upgrades (3) have low IOPS/throughput. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = + [ { assertion = cfg.config != {} -> cfg.configText == null ; + message = "Cannot specify both config and configText"; + } + ]; + + services.netdata.configDir.".opt-out-from-anonymous-statistics" = mkIf (!cfg.enableAnalyticsReporting) (pkgs.writeText ".opt-out-from-anonymous-statistics" ""); + environment.etc."netdata/netdata.conf".source = configFile; + environment.etc."netdata/conf.d".source = configDirectory; + + systemd.services.netdata = { + description = "Real time performance monitoring"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = (with pkgs; [ + curl + gawk + iproute2 + which + procps + bash + util-linux # provides logger command; required for syslog health alarms + ]) + ++ lib.optional cfg.python.enable (pkgs.python3.withPackages cfg.python.extraPackages) + ++ lib.optional config.virtualisation.libvirtd.enable (config.virtualisation.libvirtd.package); + environment = { + PYTHONPATH = "${cfg.package}/libexec/netdata/python.d/python_modules"; + NETDATA_PIPENAME = "/run/netdata/ipc"; + } // lib.optionalAttrs (!cfg.enableAnalyticsReporting) { + DO_NOT_TRACK = "1"; + }; + restartTriggers = [ + config.environment.etc."netdata/netdata.conf".source + config.environment.etc."netdata/conf.d".source + ]; + serviceConfig = { + ExecStart = "${cfg.package}/bin/netdata -P /run/netdata/netdata.pid -D -c /etc/netdata/netdata.conf"; + ExecReload = "${pkgs.util-linux}/bin/kill -s HUP -s USR1 -s USR2 $MAINPID"; + ExecStartPost = pkgs.writeShellScript "wait-for-netdata-up" '' + while [ "$(${cfg.package}/bin/netdatacli ping)" != pong ]; do sleep 0.5; done + ''; + + TimeoutStopSec = cfg.deadlineBeforeStopSec; + Restart = "on-failure"; + # User and group + User = cfg.user; + Group = cfg.group; + # Performance + LimitNOFILE = "30000"; + # Runtime directory and mode + RuntimeDirectory = "netdata"; + RuntimeDirectoryMode = "0750"; + # State directory and mode + StateDirectory = "netdata"; + StateDirectoryMode = "0750"; + # Cache directory and mode + CacheDirectory = "netdata"; + CacheDirectoryMode = "0750"; + # Logs directory and mode + LogsDirectory = "netdata"; + LogsDirectoryMode = "0750"; + # Configuration directory and mode + ConfigurationDirectory = "netdata"; + ConfigurationDirectoryMode = "0755"; + # Capabilities + CapabilityBoundingSet = [ + "CAP_DAC_OVERRIDE" # is required for freeipmi and slabinfo plugins + "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 + "CAP_SYS_PTRACE" # is required for apps plugin + "CAP_SYS_RESOURCE" # is required for ebpf plugin + "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"; + ProtectHome = "read-only"; + PrivateTmp = true; + ProtectControlGroups = true; + PrivateMounts = true; + } // (lib.optionalAttrs (cfg.claimTokenFile != null) { + LoadCredential = [ + "netdata_claim_token:${cfg.claimTokenFile}" + ]; + + ExecStartPre = pkgs.writeShellScript "netdata-claim" '' + set -euo pipefail + + if [[ -f /var/lib/netdata/cloud.d/claimed_id ]]; then + # Already registered + exit + fi + + exec ${cfg.package}/bin/netdata-claim.sh \ + -token="$(< "$CREDENTIALS_DIRECTORY/netdata_claim_token")" \ + -url=https://app.netdata.cloud \ + -daemon-not-running + ''; + }); + }; + + systemd.enableCgroupAccounting = true; + + security.wrappers = { + "apps.plugin" = { + source = "${cfg.package}/libexec/netdata/plugins.d/apps.plugin.org"; + capabilities = "cap_dac_read_search,cap_sys_ptrace+ep"; + owner = cfg.user; + group = cfg.group; + permissions = "u+rx,g+x,o-rwx"; + }; + + "cgroup-network" = { + source = "${cfg.package}/libexec/netdata/plugins.d/cgroup-network.org"; + capabilities = "cap_setuid+ep"; + owner = cfg.user; + group = cfg.group; + permissions = "u+rx,g+x,o-rwx"; + }; + + "perf.plugin" = { + source = "${cfg.package}/libexec/netdata/plugins.d/perf.plugin.org"; + capabilities = "cap_sys_admin+ep"; + owner = cfg.user; + group = cfg.group; + 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"; + owner = cfg.user; + group = cfg.group; + permissions = "u+rx,g+x,o-rwx"; + }; + + } // optionalAttrs (cfg.package.withIpmi) { + "freeipmi.plugin" = { + source = "${cfg.package}/libexec/netdata/plugins.d/freeipmi.plugin.org"; + capabilities = "cap_dac_override,cap_fowner+ep"; + owner = cfg.user; + group = cfg.group; + permissions = "u+rx,g+x,o-rwx"; + }; + }; + + security.pam.loginLimits = [ + { domain = "netdata"; type = "soft"; item = "nofile"; value = "10000"; } + { domain = "netdata"; type = "hard"; item = "nofile"; value = "30000"; } + ]; + + users.users = optionalAttrs (cfg.user == defaultUser) { + ${defaultUser} = { + group = defaultUser; + isSystemUser = true; + }; + }; + + users.groups = optionalAttrs (cfg.group == defaultUser) { + ${defaultUser} = { }; + }; + + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/nezha-agent.nix b/nixpkgs/nixos/modules/services/monitoring/nezha-agent.nix new file mode 100644 index 000000000000..8312a425d28f --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/nezha-agent.nix @@ -0,0 +1,103 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.nezha-agent; +in +{ + meta = { + maintainers = with lib.maintainers; [ moraxyc ]; + }; + options = { + services.nezha-agent = { + enable = lib.mkEnableOption "Agent of Nezha Monitoring"; + + package = lib.mkPackageOption pkgs "nezha-agent" { }; + debug = lib.mkEnableOption "verbose log"; + tls = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Enable SSL/TLS encryption. + ''; + }; + disableCommandExecute = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Disable executing the command from dashboard. + ''; + }; + skipConnection = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Do not monitor the number of connections. + ''; + }; + skipProcess = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Do not monitor the number of processes. + ''; + }; + reportDelay = lib.mkOption { + type = lib.types.enum [ 1 2 3 4 ]; + default = 1; + description = '' + The interval between system status reportings. + The value must be an integer from 1 to 4 + ''; + }; + passwordFile = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + Path to the file contained the password from dashboard. + ''; + }; + server = lib.mkOption { + type = lib.types.str; + description = '' + Address to the dashboard + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.packages = [ cfg.package ]; + + systemd.services.nezha-agent = { + serviceConfig = { + ProtectSystem = "full"; + PrivateDevices = "yes"; + PrivateTmp = "yes"; + NoNewPrivileges = true; + }; + path = [ cfg.package ]; + startLimitIntervalSec = 10; + startLimitBurst = 3; + script = lib.concatStringsSep " " ( + [ + "${cfg.package}/bin/agent" + "--disable-auto-update" + "--disable-force-update" + "--password $(cat ${cfg.passwordFile})" + ] + ++ lib.optional cfg.debug "--debug" + ++ lib.optional cfg.disableCommandExecute "--disable-command-execute" + ++ lib.optional (cfg.reportDelay != null) "--report-delay ${toString cfg.reportDelay}" + ++ lib.optional (cfg.server != null) "--server ${cfg.server}" + ++ lib.optional cfg.skipConnection "--skip-conn" + ++ lib.optional cfg.skipProcess "--skip-procs" + ++ lib.optional cfg.tls "--tls" + ); + wantedBy = [ "multi-user.target" ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/ocsinventory-agent.md b/nixpkgs/nixos/modules/services/monitoring/ocsinventory-agent.md new file mode 100644 index 000000000000..50e246fb6531 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/ocsinventory-agent.md @@ -0,0 +1,33 @@ +# OCS Inventory Agent {#module-services-ocsinventory-agent} + +[OCS Inventory NG](https://ocsinventory-ng.org/) or Open Computers and Software inventory +is an application designed to help IT administrator to keep track of the hardware and software +configurations of computers that are installed on their network. + +OCS Inventory collects information about the hardware and software of networked machines +through the **OCS Inventory Agent** program. + +This NixOS module enables you to install and configure this agent so that it sends information from your computer to the OCS Inventory server. + +For more technical information about OCS Inventory Agent, refer to [the Wiki documentation](https://wiki.ocsinventory-ng.org/03.Basic-documentation/Setting-up-the-UNIX-agent-manually-on-client-computers/). + + +## Basic Usage {#module-services-ocsinventory-agent-basic-usage} + +A minimal configuration looks like this: + +```nix +{ + services.ocsinventory-agent = { + enable = true; + settings = { + server = "https://ocsinventory.localhost:8080/ocsinventory"; + tag = "01234567890123"; + }; + }; +} +``` + +This configuration will periodically run the ocsinventory-agent SystemD service. + +The OCS Inventory Agent will inventory the computer and then sends the results to the specified OCS Inventory Server. diff --git a/nixpkgs/nixos/modules/services/monitoring/ocsinventory-agent.nix b/nixpkgs/nixos/modules/services/monitoring/ocsinventory-agent.nix new file mode 100644 index 000000000000..591738ed4ef7 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/ocsinventory-agent.nix @@ -0,0 +1,134 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.ocsinventory-agent; + + settingsFormat = pkgs.formats.keyValue { + mkKeyValue = lib.generators.mkKeyValueDefault { } "="; + }; + +in +{ + meta = { + doc = ./ocsinventory-agent.md; + maintainers = with lib.maintainers; [ anthonyroussel ]; + }; + + options = { + services.ocsinventory-agent = { + enable = lib.mkEnableOption "OCS Inventory Agent"; + + package = lib.mkPackageOption pkgs "ocsinventory-agent" { }; + + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = settingsFormat.type.nestedTypes.elemType; + + options = { + server = lib.mkOption { + type = lib.types.nullOr lib.types.str; + example = "https://ocsinventory.localhost:8080/ocsinventory"; + default = null; + description = '' + The URI of the OCS Inventory server where to send the inventory file. + + This option is ignored if {option}`services.ocsinventory-agent.settings.local` is set. + ''; + }; + + local = lib.mkOption { + type = lib.types.nullOr lib.types.path; + example = "/var/lib/ocsinventory-agent/reports"; + default = null; + description = '' + If specified, the OCS Inventory Agent will run in offline mode + and the resulting inventory file will be stored in the specified path. + ''; + }; + + ca = lib.mkOption { + type = lib.types.path; + default = "/etc/ssl/certs/ca-certificates.crt"; + description = '' + Path to CA certificates file in PEM format, for server + SSL certificate validation. + ''; + }; + + tag = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "01234567890123"; + description = "Tag for the generated inventory."; + }; + + debug = lib.mkEnableOption "debug mode"; + }; + }; + default = { }; + example = { + ca = "/etc/ssl/certs/ca-certificates.crt"; + debug = true; + server = "https://ocsinventory.localhost:8080/ocsinventory"; + tag = "01234567890123"; + }; + description = '' + Configuration for /etc/ocsinventory-agent/ocsinventory-agent.cfg. + + Refer to + {manpage}`ocsinventory-agent(1)` for available options. + ''; + }; + + interval = lib.mkOption { + type = lib.types.str; + default = "daily"; + example = "06:00"; + description = '' + How often we run the ocsinventory-agent service. Runs by default every daily. + + The format is described in + {manpage}`systemd.time(7)`. + ''; + }; + }; + }; + + config = + let + configFile = settingsFormat.generate "ocsinventory-agent.cfg" cfg.settings; + + in lib.mkIf cfg.enable { + # Path of the configuration file is hard-coded and cannot be changed + # https://github.com/OCSInventory-NG/UnixAgent/blob/v2.10.0/lib/Ocsinventory/Agent/Config.pm#L78 + # + environment.etc."ocsinventory-agent/ocsinventory-agent.cfg".source = configFile; + + systemd.services.ocsinventory-agent = { + description = "OCS Inventory Agent service"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + reloadTriggers = [ configFile ]; + + serviceConfig = { + ExecStart = lib.getExe cfg.package; + ConfigurationDirectory = "ocsinventory-agent"; + StateDirectory = "ocsinventory-agent"; + }; + }; + + systemd.timers.ocsinventory-agent = { + description = "Launch OCS Inventory Agent regularly"; + wantedBy = [ "timers.target" ]; + + timerConfig = { + OnCalendar = cfg.interval; + AccuracySec = "1h"; + RandomizedDelaySec = 240; + Persistent = true; + Unit = "ocsinventory-agent.service"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/opentelemetry-collector.nix b/nixpkgs/nixos/modules/services/monitoring/opentelemetry-collector.nix new file mode 100644 index 000000000000..459cc8532490 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/opentelemetry-collector.nix @@ -0,0 +1,68 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) mkEnableOption mkPackageOption mkIf mkOption types getExe; + + cfg = config.services.opentelemetry-collector; + opentelemetry-collector = cfg.package; + + settingsFormat = pkgs.formats.yaml {}; +in { + options.services.opentelemetry-collector = { + enable = mkEnableOption "Opentelemetry Collector"; + + package = mkPackageOption pkgs "opentelemetry-collector" { }; + + settings = mkOption { + type = settingsFormat.type; + default = {}; + description = '' + Specify the configuration for Opentelemetry Collector in Nix. + + See https://opentelemetry.io/docs/collector/configuration/ for available options. + ''; + }; + + configFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Specify a path to a configuration file that Opentelemetry Collector should use. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [{ + assertion = ( + (cfg.settings == {}) != (cfg.configFile == null) + ); + message = '' + Please specify a configuration for Opentelemetry Collector with either + 'services.opentelemetry-collector.settings' or + 'services.opentelemetry-collector.configFile'. + ''; + }]; + + systemd.services.opentelemetry-collector = { + description = "Opentelemetry Collector Service Daemon"; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = let + conf = if cfg.configFile == null + then settingsFormat.generate "config.yaml" cfg.settings + else cfg.configFile; + in + { + ExecStart = "${getExe opentelemetry-collector} --config=file:${conf}"; + DynamicUser = true; + Restart = "always"; + ProtectSystem = "full"; + DevicePolicy = "closed"; + NoNewPrivileges = true; + WorkingDirectory = "/var/lib/opentelemetry-collector"; + StateDirectory = "opentelemetry-collector"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/osquery.nix b/nixpkgs/nixos/modules/services/monitoring/osquery.nix new file mode 100644 index 000000000000..872f5e96a412 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/osquery.nix @@ -0,0 +1,99 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.services.osquery; + dirname = path: with lib.strings; with lib.lists; concatStringsSep "/" + (init (splitString "/" (normalizePath path))); + + # conf is the osquery configuration file used when the --config_plugin=filesystem. + # filesystem is the osquery default value for the config_plugin flag. + conf = pkgs.writeText "osquery.conf" (builtins.toJSON cfg.settings); + + # flagfile is the file containing osquery command line flags to be + # provided to the application using the special --flagfile option. + flagfile = pkgs.writeText "osquery.flags" + (concatStringsSep "\n" + (mapAttrsToList (name: value: "--${name}=${value}") + # Use the conf derivation if not otherwise specified. + ({ config_path = conf; } // cfg.flags))); + + osqueryi = pkgs.runCommand "osqueryi" { nativeBuildInputs = [ pkgs.makeWrapper ]; } '' + mkdir -p $out/bin + makeWrapper ${pkgs.osquery}/bin/osqueryi $out/bin/osqueryi \ + --add-flags "--flagfile ${flagfile} --disable-database" + ''; +in +{ + options.services.osquery = { + enable = mkEnableOption "osqueryd daemon"; + + settings = mkOption { + default = { }; + description = '' + Configuration to be written to the osqueryd JSON configuration file. + To understand the configuration format, refer to https://osquery.readthedocs.io/en/stable/deployment/configuration/#configuration-components. + ''; + example = { + options.utc = false; + }; + type = types.attrs; + }; + + flags = mkOption { + default = { }; + description = '' + Attribute set of flag names and values to be written to the osqueryd flagfile. + For more information, refer to https://osquery.readthedocs.io/en/stable/installation/cli-flags. + ''; + example = { + config_refresh = "10"; + }; + type = with types; + submodule { + freeformType = attrsOf str; + options = { + database_path = mkOption { + default = "/var/lib/osquery/osquery.db"; + readOnly = true; + description = "Path used for the database file."; + type = path; + }; + logger_path = mkOption { + default = "/var/log/osquery"; + readOnly = true; + description = "Base directory used for logging."; + type = path; + }; + pidfile = mkOption { + default = "/run/osquery/osqueryd.pid"; + readOnly = true; + description = "Path used for pid file."; + type = path; + }; + }; + }; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = [ osqueryi ]; + systemd.services.osqueryd = { + after = [ "network.target" "syslog.service" ]; + description = "The osquery daemon"; + serviceConfig = { + ExecStart = "${pkgs.osquery}/bin/osqueryd --flagfile ${flagfile}"; + PIDFile = cfg.flags.pidfile; + LogsDirectory = cfg.flags.logger_path; + StateDirectory = dirname cfg.flags.database_path; + Restart = "always"; + }; + wantedBy = [ "multi-user.target" ]; + }; + systemd.tmpfiles.settings."10-osquery".${dirname (cfg.flags.pidfile)}.d = { + user = "root"; + group = "root"; + mode = "0755"; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/parsedmarc.md b/nixpkgs/nixos/modules/services/monitoring/parsedmarc.md new file mode 100644 index 000000000000..765846bbbaf3 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/parsedmarc.md @@ -0,0 +1,118 @@ +# parsedmarc {#module-services-parsedmarc} +[parsedmarc](https://domainaware.github.io/parsedmarc/) is a service +which parses incoming [DMARC](https://dmarc.org/) reports and stores +or sends them to a downstream service for further analysis. In +combination with Elasticsearch, Grafana and the included Grafana +dashboard, it provides a handy overview of DMARC reports over time. + +## Basic usage {#module-services-parsedmarc-basic-usage} +A very minimal setup which reads incoming reports from an external +email address and saves them to a local Elasticsearch instance looks +like this: + +```nix +{ + services.parsedmarc = { + enable = true; + settings.imap = { + host = "imap.example.com"; + user = "alice@example.com"; + password = "/path/to/imap_password_file"; + }; + provision.geoIp = false; # Not recommended! + }; +} +``` + +Note that GeoIP provisioning is disabled in the example for +simplicity, but should be turned on for fully functional reports. + +## Local mail {#module-services-parsedmarc-local-mail} +Instead of watching an external inbox, a local inbox can be +automatically provisioned. The recipient's name is by default set to +`dmarc`, but can be configured in +[services.parsedmarc.provision.localMail.recipientName](options.html#opt-services.parsedmarc.provision.localMail.recipientName). You +need to add an MX record pointing to the host. More concretely: for +the example to work, an MX record needs to be set up for +`monitoring.example.com` and the complete email address that should be +configured in the domain's dmarc policy is +`dmarc@monitoring.example.com`. + +```nix +{ + services.parsedmarc = { + enable = true; + provision = { + localMail = { + enable = true; + hostname = monitoring.example.com; + }; + geoIp = false; # Not recommended! + }; + }; +} +``` + +## Grafana and GeoIP {#module-services-parsedmarc-grafana-geoip} +The reports can be visualized and summarized with parsedmarc's +official Grafana dashboard. For all views to work, and for the data to +be complete, GeoIP databases are also required. The following example +shows a basic deployment where the provisioned Elasticsearch instance +is automatically added as a Grafana datasource, and the dashboard is +added to Grafana as well. + +```nix +{ + services.parsedmarc = { + enable = true; + provision = { + localMail = { + enable = true; + hostname = url; + }; + grafana = { + datasource = true; + dashboard = true; + }; + }; + }; + + # Not required, but recommended for full functionality + services.geoipupdate = { + settings = { + AccountID = 000000; + LicenseKey = "/path/to/license_key_file"; + }; + }; + + services.grafana = { + enable = true; + addr = "0.0.0.0"; + domain = url; + rootUrl = "https://" + url; + protocol = "socket"; + security = { + adminUser = "admin"; + adminPasswordFile = "/path/to/admin_password_file"; + secretKeyFile = "/path/to/secret_key_file"; + }; + }; + + services.nginx = { + enable = true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + recommendedProxySettings = true; + upstreams.grafana.servers."unix:/${config.services.grafana.socket}" = {}; + virtualHosts.${url} = { + root = config.services.grafana.staticRootPath; + enableACME = true; + forceSSL = true; + locations."/".tryFiles = "$uri @grafana"; + locations."@grafana".proxyPass = "http://grafana"; + }; + }; + users.users.nginx.extraGroups = [ "grafana" ]; +} +``` diff --git a/nixpkgs/nixos/modules/services/monitoring/parsedmarc.nix b/nixpkgs/nixos/modules/services/monitoring/parsedmarc.nix new file mode 100644 index 000000000000..a14ade59c29e --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/parsedmarc.nix @@ -0,0 +1,545 @@ +{ config, lib, options, pkgs, ... }: + +let + cfg = config.services.parsedmarc; + opt = options.services.parsedmarc; + isSecret = v: isAttrs v && v ? _secret && isString v._secret; + ini = pkgs.formats.ini { + mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" rec { + mkValueString = v: + if isInt v then toString v + else if isString v then v + else if true == v then "True" + else if false == v then "False" + else if isSecret v then hashString "sha256" v._secret + else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}"; + }; + }; + inherit (builtins) elem isAttrs isString isInt isList typeOf hashString; +in +{ + options.services.parsedmarc = { + + enable = lib.mkEnableOption '' + parsedmarc, a DMARC report monitoring service + ''; + + provision = { + localMail = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether Postfix and Dovecot should be set up to receive + mail locally. parsedmarc will be configured to watch the + local inbox as the automatically created user specified in + [](#opt-services.parsedmarc.provision.localMail.recipientName) + ''; + }; + + recipientName = lib.mkOption { + type = lib.types.str; + default = "dmarc"; + description = '' + The DMARC mail recipient name, i.e. the name part of the + email address which receives DMARC reports. + + A local user with this name will be set up and assigned a + randomized password on service start. + ''; + }; + + hostname = lib.mkOption { + type = lib.types.str; + default = config.networking.fqdn; + defaultText = lib.literalExpression "config.networking.fqdn"; + example = "monitoring.example.com"; + description = '' + The hostname to use when configuring Postfix. + + Should correspond to the host's fully qualified domain + name and the domain part of the email address which + receives DMARC reports. You also have to set up an MX record + pointing to this domain name. + ''; + }; + }; + + geoIp = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to enable and configure the [geoipupdate](#opt-services.geoipupdate.enable) + service to automatically fetch GeoIP databases. Not crucial, + but recommended for full functionality. + + To finish the setup, you need to manually set the [](#opt-services.geoipupdate.settings.AccountID) and + [](#opt-services.geoipupdate.settings.LicenseKey) + options. + ''; + }; + + elasticsearch = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to set up and use a local instance of Elasticsearch. + ''; + }; + + grafana = { + datasource = lib.mkOption { + type = lib.types.bool; + default = cfg.provision.elasticsearch && config.services.grafana.enable; + defaultText = lib.literalExpression '' + config.${opt.provision.elasticsearch} && config.${options.services.grafana.enable} + ''; + apply = x: x && cfg.provision.elasticsearch; + description = '' + Whether the automatically provisioned Elasticsearch + instance should be added as a grafana datasource. Has no + effect unless + [](#opt-services.parsedmarc.provision.elasticsearch) + is also enabled. + ''; + }; + + dashboard = lib.mkOption { + type = lib.types.bool; + default = config.services.grafana.enable; + defaultText = lib.literalExpression "config.services.grafana.enable"; + description = '' + Whether the official parsedmarc grafana dashboard should + be provisioned to the local grafana instance. + ''; + }; + }; + }; + + settings = lib.mkOption { + example = lib.literalExpression '' + { + imap = { + host = "imap.example.com"; + user = "alice@example.com"; + password = { _secret = "/run/keys/imap_password" }; + }; + mailbox = { + watch = true; + batch_size = 30; + }; + splunk_hec = { + url = "https://splunkhec.example.com"; + token = { _secret = "/run/keys/splunk_token" }; + index = "email"; + }; + } + ''; + description = '' + Configuration parameters to set in + {file}`parsedmarc.ini`. For a full list of + available parameters, see + <https://domainaware.github.io/parsedmarc/#configuration-file>. + + Settings containing secret data should be set to an attribute + set containing the attribute `_secret` - a + string pointing to a file containing the value the option + should be set to. See the example to get a better picture of + this: in the resulting {file}`parsedmarc.ini` + file, the `splunk_hec.token` key will be set + to the contents of the + {file}`/run/keys/splunk_token` file. + ''; + + type = lib.types.submodule { + freeformType = ini.type; + + options = { + general = { + save_aggregate = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Save aggregate report data to Elasticsearch and/or Splunk. + ''; + }; + + save_forensic = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Save forensic report data to Elasticsearch and/or Splunk. + ''; + }; + }; + + mailbox = { + watch = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Use the IMAP IDLE command to process messages as they arrive. + ''; + }; + + delete = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Delete messages after processing them, instead of archiving them. + ''; + }; + }; + + imap = { + host = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = '' + The IMAP server hostname or IP address. + ''; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 993; + description = '' + The IMAP server port. + ''; + }; + + ssl = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Use an encrypted SSL/TLS connection. + ''; + }; + + user = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + The IMAP server username. + ''; + }; + + password = lib.mkOption { + type = with lib.types; nullOr (either path (attrsOf path)); + default = null; + description = '' + The IMAP server password. + + Always handled as a secret whether the value is + wrapped in a `{ _secret = ...; }` + attrset or not (refer to [](#opt-services.parsedmarc.settings) for + details). + ''; + apply = x: if isAttrs x || x == null then x else { _secret = x; }; + }; + }; + + smtp = { + host = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + The SMTP server hostname or IP address. + ''; + }; + + port = lib.mkOption { + type = with lib.types; nullOr port; + default = null; + description = '' + The SMTP server port. + ''; + }; + + ssl = lib.mkOption { + type = with lib.types; nullOr bool; + default = null; + description = '' + Use an encrypted SSL/TLS connection. + ''; + }; + + user = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + The SMTP server username. + ''; + }; + + password = lib.mkOption { + type = with lib.types; nullOr (either path (attrsOf path)); + default = null; + description = '' + The SMTP server password. + + Always handled as a secret whether the value is + wrapped in a `{ _secret = ...; }` + attrset or not (refer to [](#opt-services.parsedmarc.settings) for + details). + ''; + apply = x: if isAttrs x || x == null then x else { _secret = x; }; + }; + + from = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + The `From` address to use for the + outgoing mail. + ''; + }; + + to = lib.mkOption { + type = with lib.types; nullOr (listOf str); + default = null; + description = '' + The addresses to send outgoing mail to. + ''; + apply = x: if x == [] || x == null then null else lib.concatStringsSep "," x; + }; + }; + + elasticsearch = { + hosts = lib.mkOption { + default = []; + type = with lib.types; listOf str; + apply = x: if x == [] then null else lib.concatStringsSep "," x; + description = '' + A list of Elasticsearch hosts to push parsed reports + to. + ''; + }; + + user = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + Username to use when connecting to Elasticsearch, if + required. + ''; + }; + + password = lib.mkOption { + type = with lib.types; nullOr (either path (attrsOf path)); + default = null; + description = '' + The password to use when connecting to Elasticsearch, + if required. + + Always handled as a secret whether the value is + wrapped in a `{ _secret = ...; }` + attrset or not (refer to [](#opt-services.parsedmarc.settings) for + details). + ''; + apply = x: if isAttrs x || x == null then x else { _secret = x; }; + }; + + ssl = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to use an encrypted SSL/TLS connection. + ''; + }; + + cert_path = lib.mkOption { + type = lib.types.path; + default = "/etc/ssl/certs/ca-certificates.crt"; + description = '' + The path to a TLS certificate bundle used to verify + the server's certificate. + ''; + }; + }; + }; + + }; + }; + + }; + + config = lib.mkIf cfg.enable { + + warnings = let + deprecationWarning = optname: "Starting in 8.0.0, the `${optname}` option has been moved from the `services.parsedmarc.settings.imap`" + + "configuration section to the `services.parsedmarc.settings.mailbox` configuration section."; + hasImapOpt = lib.flip builtins.hasAttr cfg.settings.imap; + movedOptions = [ "reports_folder" "archive_folder" "watch" "delete" "test" "batch_size" ]; + in builtins.map deprecationWarning (builtins.filter hasImapOpt movedOptions); + + services.elasticsearch.enable = lib.mkDefault cfg.provision.elasticsearch; + + services.geoipupdate = lib.mkIf cfg.provision.geoIp { + enable = true; + settings = { + EditionIDs = [ + "GeoLite2-ASN" + "GeoLite2-City" + "GeoLite2-Country" + ]; + DatabaseDirectory = "/var/lib/GeoIP"; + }; + }; + + services.dovecot2 = lib.mkIf cfg.provision.localMail.enable { + enable = true; + protocols = [ "imap" ]; + }; + + services.postfix = lib.mkIf cfg.provision.localMail.enable { + enable = true; + origin = cfg.provision.localMail.hostname; + config = { + myhostname = cfg.provision.localMail.hostname; + mydestination = cfg.provision.localMail.hostname; + }; + }; + + services.grafana = { + declarativePlugins = with pkgs.grafanaPlugins; + lib.mkIf cfg.provision.grafana.dashboard [ + grafana-worldmap-panel + grafana-piechart-panel + ]; + + provision = { + enable = cfg.provision.grafana.datasource || cfg.provision.grafana.dashboard; + datasources.settings.datasources = + let + esVersion = lib.getVersion config.services.elasticsearch.package; + in + lib.mkIf cfg.provision.grafana.datasource [ + { + name = "dmarc-ag"; + type = "elasticsearch"; + access = "proxy"; + url = "http://localhost:9200"; + jsonData = { + timeField = "date_range"; + inherit esVersion; + }; + } + { + name = "dmarc-fo"; + type = "elasticsearch"; + access = "proxy"; + url = "http://localhost:9200"; + jsonData = { + timeField = "date_range"; + inherit esVersion; + }; + } + ]; + dashboards.settings.providers = lib.mkIf cfg.provision.grafana.dashboard [{ + name = "parsedmarc"; + options.path = "${pkgs.parsedmarc.dashboard}"; + }]; + }; + }; + + services.parsedmarc.settings = lib.mkMerge [ + (lib.mkIf cfg.provision.elasticsearch { + elasticsearch = { + hosts = [ "http://localhost:9200" ]; + ssl = false; + }; + }) + (lib.mkIf cfg.provision.localMail.enable { + imap = { + host = "localhost"; + port = 143; + ssl = false; + user = cfg.provision.localMail.recipientName; + password = "${pkgs.writeText "imap-password" "@imap-password@"}"; + }; + mailbox = { + watch = true; + }; + }) + ]; + + systemd.services.parsedmarc = + let + # Remove any empty attributes from the config, i.e. empty + # lists, empty attrsets and null. This makes it possible to + # list interesting options in `settings` without them always + # ending up in the resulting config. + filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ null [] {} ])) cfg.settings; + + # Extract secrets (attributes set to an attrset with a + # "_secret" key) from the settings and generate the commands + # to run to perform the secret replacements. + secretPaths = lib.catAttrs "_secret" (lib.collect isSecret filteredConfig); + parsedmarcConfig = ini.generate "parsedmarc.ini" filteredConfig; + mkSecretReplacement = file: '' + replace-secret ${lib.escapeShellArgs [ (hashString "sha256" file) file "/run/parsedmarc/parsedmarc.ini" ]} + ''; + secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths; + in + { + wantedBy = [ "multi-user.target" ]; + after = [ "postfix.service" "dovecot2.service" "elasticsearch.service" ]; + path = with pkgs; [ replace-secret openssl shadow ]; + serviceConfig = { + ExecStartPre = let + startPreFullPrivileges = '' + set -o errexit -o pipefail -o nounset -o errtrace + shopt -s inherit_errexit + + umask u=rwx,g=,o= + cp ${parsedmarcConfig} /run/parsedmarc/parsedmarc.ini + chown parsedmarc:parsedmarc /run/parsedmarc/parsedmarc.ini + ${secretReplacements} + '' + lib.optionalString cfg.provision.localMail.enable '' + openssl rand -hex 64 >/run/parsedmarc/dmarc_user_passwd + replace-secret '@imap-password@' '/run/parsedmarc/dmarc_user_passwd' /run/parsedmarc/parsedmarc.ini + echo "Setting new randomized password for user '${cfg.provision.localMail.recipientName}'." + cat <(echo -n "${cfg.provision.localMail.recipientName}:") /run/parsedmarc/dmarc_user_passwd | chpasswd + ''; + in + "+${pkgs.writeShellScript "parsedmarc-start-pre-full-privileges" startPreFullPrivileges}"; + Type = "simple"; + User = "parsedmarc"; + Group = "parsedmarc"; + DynamicUser = true; + RuntimeDirectory = "parsedmarc"; + RuntimeDirectoryMode = "0700"; + CapabilityBoundingSet = ""; + PrivateDevices = true; + PrivateMounts = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictRealtime = true; + RestrictNamespaces = true; + MemoryDenyWriteExecute = true; + LockPersonality = true; + SystemCallArchitectures = "native"; + ExecStart = "${lib.getExe pkgs.parsedmarc} -c /run/parsedmarc/parsedmarc.ini"; + }; + }; + + users.users.${cfg.provision.localMail.recipientName} = lib.mkIf cfg.provision.localMail.enable { + isNormalUser = true; + description = "DMARC mail recipient"; + }; + }; + + meta.doc = ./parsedmarc.md; + meta.maintainers = [ lib.maintainers.talyz ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/alertmanager-irc-relay.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/alertmanager-irc-relay.nix new file mode 100644 index 000000000000..eda4277c1bac --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/alertmanager-irc-relay.nix @@ -0,0 +1,102 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.prometheus.alertmanagerIrcRelay; + + configFormat = pkgs.formats.yaml { }; + configFile = configFormat.generate "alertmanager-irc-relay.yml" cfg.settings; +in +{ + options.services.prometheus.alertmanagerIrcRelay = { + enable = mkEnableOption "Alertmanager IRC Relay"; + + package = mkPackageOption pkgs "alertmanager-irc-relay" { }; + + extraFlags = mkOption { + type = types.listOf types.str; + default = []; + description = "Extra command line options to pass to alertmanager-irc-relay."; + }; + + settings = mkOption { + type = configFormat.type; + example = literalExpression '' + { + http_host = "localhost"; + http_port = 8000; + + irc_host = "irc.example.com"; + irc_port = 7000; + irc_nickname = "myalertbot"; + + irc_channels = [ + { name = "#mychannel"; } + ]; + } + ''; + description = '' + Configuration for Alertmanager IRC Relay as a Nix attribute set. + For a reference, check out the + [example configuration](https://github.com/google/alertmanager-irc-relay#configuring-and-running-the-bot) + and the + [source code](https://github.com/google/alertmanager-irc-relay/blob/master/config.go). + + Note: The webhook's URL MUST point to the IRC channel where the message + should be posted. For `#mychannel` from the example, this would be + `http://localhost:8080/mychannel`. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.services.alertmanager-irc-relay = { + description = "Alertmanager IRC Relay"; + + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + + serviceConfig = { + ExecStart = '' + ${cfg.package}/bin/alertmanager-irc-relay \ + -config ${configFile} \ + ${escapeShellArgs cfg.extraFlags} + ''; + + DynamicUser = true; + NoNewPrivileges = true; + + ProtectProc = "invisible"; + ProtectSystem = "strict"; + ProtectHome = "tmpfs"; + + PrivateTmp = true; + PrivateDevices = true; + PrivateIPC = true; + + ProtectHostname = true; + ProtectClock = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictRealtime = true; + RestrictSUIDSGID = true; + + SystemCallFilter = [ + "@system-service" + "~@cpu-emulation" + "~@privileged" + "~@reboot" + "~@setuid" + "~@swap" + ]; + }; + }; + }; + + meta.maintainers = [ maintainers.oxzi ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/alertmanager.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/alertmanager.nix new file mode 100644 index 000000000000..d1d8f2caaf63 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/alertmanager.nix @@ -0,0 +1,197 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.prometheus.alertmanager; + mkConfigFile = pkgs.writeText "alertmanager.yml" (builtins.toJSON cfg.configuration); + + checkedConfig = file: + if cfg.checkConfig then + pkgs.runCommand "checked-config" { nativeBuildInputs = [ cfg.package ]; } '' + ln -s ${file} $out + amtool check-config $out + '' else file; + + alertmanagerYml = let + yml = if cfg.configText != null then + pkgs.writeText "alertmanager.yml" cfg.configText + else mkConfigFile; + in checkedConfig yml; + + cmdlineArgs = cfg.extraFlags ++ [ + "--config.file /tmp/alert-manager-substituted.yaml" + "--web.listen-address ${cfg.listenAddress}:${toString cfg.port}" + "--log.level ${cfg.logLevel}" + "--storage.path /var/lib/alertmanager" + (toString (map (peer: "--cluster.peer ${peer}:9094") cfg.clusterPeers)) + ] ++ (optional (cfg.webExternalUrl != null) + "--web.external-url ${cfg.webExternalUrl}" + ) ++ (optional (cfg.logFormat != null) + "--log.format ${cfg.logFormat}" + ); +in { + imports = [ + (mkRemovedOptionModule [ "services" "prometheus" "alertmanager" "user" ] "The alertmanager service is now using systemd's DynamicUser mechanism which obviates a user setting.") + (mkRemovedOptionModule [ "services" "prometheus" "alertmanager" "group" ] "The alertmanager service is now using systemd's DynamicUser mechanism which obviates a group setting.") + (mkRemovedOptionModule [ "services" "prometheus" "alertmanagerURL" ] '' + Due to incompatibility, the alertmanagerURL option has been removed, + please use 'services.prometheus.alertmanagers' instead. + '') + ]; + + options = { + services.prometheus.alertmanager = { + enable = mkEnableOption "Prometheus Alertmanager"; + + package = mkPackageOption pkgs "prometheus-alertmanager" { }; + + configuration = mkOption { + type = types.nullOr types.attrs; + default = null; + description = '' + Alertmanager configuration as nix attribute set. + ''; + }; + + configText = mkOption { + type = types.nullOr types.lines; + default = null; + description = '' + Alertmanager configuration as YAML text. If non-null, this option + defines the text that is written to alertmanager.yml. If null, the + contents of alertmanager.yml is generated from the structured config + options. + ''; + }; + + checkConfig = mkOption { + type = types.bool; + default = true; + description = '' + Check configuration with `amtool check-config`. The call to `amtool` is + subject to sandboxing by Nix. + + If you use credentials stored in external files + (`environmentFile`, etc), + they will not be visible to `amtool` + and it will report errors, despite a correct configuration. + ''; + }; + + logFormat = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + If set use a syslog logger or JSON logging. + ''; + }; + + logLevel = mkOption { + type = types.enum ["debug" "info" "warn" "error" "fatal"]; + default = "warn"; + description = '' + Only log messages with the given severity or above. + ''; + }; + + webExternalUrl = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy). + Used for generating relative and absolute links back to Alertmanager itself. + If the URL has a path portion, it will be used to prefix all HTTP endoints served by Alertmanager. + If omitted, relevant URL components will be derived automatically. + ''; + }; + + listenAddress = mkOption { + type = types.str; + default = ""; + description = '' + Address to listen on for the web interface and API. Empty string will listen on all interfaces. + "localhost" will listen on 127.0.0.1 (but not ::1). + ''; + }; + + port = mkOption { + type = types.port; + default = 9093; + description = '' + Port to listen on for the web interface and API. + ''; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = '' + Open port in firewall for incoming connections. + ''; + }; + + clusterPeers = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Initial peers for HA cluster. + ''; + }; + + extraFlags = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Extra commandline options when launching the Alertmanager. + ''; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/root/alertmanager.env"; + description = '' + File to load as environment file. Environment variables + from this file will be interpolated into the config file + using envsubst with this syntax: + `$ENVIRONMENT ''${VARIABLE}` + ''; + }; + }; + }; + + config = mkMerge [ + (mkIf cfg.enable { + assertions = singleton { + assertion = cfg.configuration != null || cfg.configText != null; + message = "Can not enable alertmanager without a configuration. " + + "Set either the `configuration` or `configText` attribute."; + }; + }) + (mkIf cfg.enable { + networking.firewall.allowedTCPPorts = optional cfg.openFirewall cfg.port; + + systemd.services.alertmanager = { + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + preStart = '' + ${lib.getBin pkgs.envsubst}/bin/envsubst -o "/tmp/alert-manager-substituted.yaml" \ + -i "${alertmanagerYml}" + ''; + serviceConfig = { + Restart = "always"; + StateDirectory = "alertmanager"; + DynamicUser = true; # implies PrivateTmp + EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; + WorkingDirectory = "/tmp"; + ExecStart = "${cfg.package}/bin/alertmanager" + + optionalString (length cmdlineArgs != 0) (" \\\n " + + concatStringsSep " \\\n " cmdlineArgs); + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + }; + }) + ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/default.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/default.nix new file mode 100644 index 000000000000..7e707a13b790 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/default.nix @@ -0,0 +1,1863 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + yaml = pkgs.formats.yaml { }; + cfg = config.services.prometheus; + checkConfigEnabled = + (lib.isBool cfg.checkConfig && cfg.checkConfig) + || cfg.checkConfig == "syntax-only"; + + workingDir = "/var/lib/" + cfg.stateDir; + + triggerReload = pkgs.writeShellScriptBin "trigger-reload-prometheus" '' + PATH="${makeBinPath (with pkgs; [ systemd ])}" + if systemctl -q is-active prometheus.service; then + systemctl reload prometheus.service + fi + ''; + + reload = pkgs.writeShellScriptBin "reload-prometheus" '' + PATH="${makeBinPath (with pkgs; [ systemd coreutils gnugrep ])}" + cursor=$(journalctl --show-cursor -n0 | grep -oP "cursor: \K.*") + kill -HUP $MAINPID + journalctl -u prometheus.service --after-cursor="$cursor" -f \ + | grep -m 1 "Completed loading of configuration file" > /dev/null + ''; + + # a wrapper that verifies that the configuration is valid + promtoolCheck = what: name: file: + if checkConfigEnabled then + pkgs.runCommandLocal + "${name}-${replaceStrings [" "] [""] what}-checked" + { nativeBuildInputs = [ cfg.package.cli ]; } '' + ln -s ${file} $out + promtool ${what} $out + '' else file; + + generatedPrometheusYml = yaml.generate "prometheus.yml" promConfig; + + # This becomes the main config file for Prometheus + promConfig = { + global = filterValidPrometheus cfg.globalConfig; + scrape_configs = filterValidPrometheus cfg.scrapeConfigs; + remote_write = filterValidPrometheus cfg.remoteWrite; + remote_read = filterValidPrometheus cfg.remoteRead; + rule_files = optionals (!(cfg.enableAgentMode)) (map (promtoolCheck "check rules" "rules") (cfg.ruleFiles ++ [ + (pkgs.writeText "prometheus.rules" (concatStringsSep "\n" cfg.rules)) + ])); + alerting = { + inherit (cfg) alertmanagers; + }; + }; + + prometheusYml = + let + yml = + if cfg.configText != null then + pkgs.writeText "prometheus.yml" cfg.configText + else generatedPrometheusYml; + in + promtoolCheck "check config ${lib.optionalString (cfg.checkConfig == "syntax-only") "--syntax-only"}" "prometheus.yml" yml; + + cmdlineArgs = cfg.extraFlags ++ [ + "--config.file=${ + if cfg.enableReload + then "/etc/prometheus/prometheus.yaml" + else prometheusYml + }" + "--web.listen-address=${cfg.listenAddress}:${builtins.toString cfg.port}" + ] ++ ( + if (cfg.enableAgentMode) then [ + "--enable-feature=agent" + ] else [ + "--alertmanager.notification-queue-capacity=${toString cfg.alertmanagerNotificationQueueCapacity }" + "--storage.tsdb.path=${workingDir}/data/" + ]) + ++ optional (cfg.webExternalUrl != null) "--web.external-url=${cfg.webExternalUrl}" + ++ optional (cfg.retentionTime != null) "--storage.tsdb.retention.time=${cfg.retentionTime}" + ++ optional (cfg.webConfigFile != null) "--web.config.file=${cfg.webConfigFile}"; + + filterValidPrometheus = filterAttrsListRecursive (n: v: !(n == "_module" || v == null)); + filterAttrsListRecursive = pred: x: + if isAttrs x then + listToAttrs + ( + concatMap + (name: + let v = x.${name}; in + if pred name v then [ + (nameValuePair name (filterAttrsListRecursive pred v)) + ] else [ ] + ) + (attrNames x) + ) + else if isList x then + map (filterAttrsListRecursive pred) x + else x; + + # + # Config types: helper functions + # + + mkDefOpt = type: defaultStr: description: mkOpt type (description + '' + + Defaults to ````${defaultStr}```` in prometheus + when set to `null`. + ''); + + mkOpt = type: description: mkOption { + type = types.nullOr type; + default = null; + description = description; + }; + + mkSdConfigModule = extraOptions: types.submodule { + options = { + basic_auth = mkOpt promTypes.basic_auth '' + Optional HTTP basic authentication information. + ''; + + authorization = mkOpt + (types.submodule { + options = { + type = mkDefOpt types.str "Bearer" '' + Sets the authentication type. + ''; + + credentials = mkOpt types.str '' + Sets the credentials. It is mutually exclusive with `credentials_file`. + ''; + + credentials_file = mkOpt types.str '' + Sets the credentials to the credentials read from the configured file. + It is mutually exclusive with `credentials`. + ''; + }; + }) '' + Optional `Authorization` header configuration. + ''; + + oauth2 = mkOpt promtypes.oauth2 '' + Optional OAuth 2.0 configuration. + Cannot be used at the same time as basic_auth or authorization. + ''; + + proxy_url = mkOpt types.str '' + Optional proxy URL. + ''; + + follow_redirects = mkDefOpt types.bool "true" '' + Configure whether HTTP requests follow HTTP 3xx redirects. + ''; + + tls_config = mkOpt promTypes.tls_config '' + TLS configuration. + ''; + } // extraOptions; + }; + + # + # Config types: general + # + + promTypes.globalConfig = types.submodule { + options = { + scrape_interval = mkDefOpt types.str "1m" '' + How frequently to scrape targets by default. + ''; + + scrape_timeout = mkDefOpt types.str "10s" '' + How long until a scrape request times out. + ''; + + evaluation_interval = mkDefOpt types.str "1m" '' + How frequently to evaluate rules by default. + ''; + + external_labels = mkOpt (types.attrsOf types.str) '' + The labels to add to any time series or alerts when + communicating with external systems (federation, remote + storage, Alertmanager). + ''; + }; + }; + + promTypes.basic_auth = types.submodule { + options = { + username = mkOption { + type = types.str; + description = '' + HTTP username + ''; + }; + password = mkOpt types.str "HTTP password"; + password_file = mkOpt types.str "HTTP password file"; + }; + }; + + promTypes.tls_config = types.submodule { + options = { + ca_file = mkOpt types.str '' + CA certificate to validate API server certificate with. + ''; + + cert_file = mkOpt types.str '' + Certificate file for client cert authentication to the server. + ''; + + key_file = mkOpt types.str '' + Key file for client cert authentication to the server. + ''; + + server_name = mkOpt types.str '' + ServerName extension to indicate the name of the server. + http://tools.ietf.org/html/rfc4366#section-3.1 + ''; + + insecure_skip_verify = mkOpt types.bool '' + Disable validation of the server certificate. + ''; + }; + }; + + promtypes.oauth2 = types.submodule { + options = { + client_id = mkOpt types.str '' + OAuth client ID. + ''; + + client_secret = mkOpt types.str '' + OAuth client secret. + ''; + + client_secret_file = mkOpt types.str '' + Read the client secret from a file. It is mutually exclusive with `client_secret`. + ''; + + scopes = mkOpt (types.listOf types.str) '' + Scopes for the token request. + ''; + + token_url = mkOpt types.str '' + The URL to fetch the token from. + ''; + + endpoint_params = mkOpt (types.attrsOf types.str) '' + Optional parameters to append to the token URL. + ''; + }; + }; + + promTypes.scrape_config = types.submodule { + options = { + authorization = mkOption { + type = types.nullOr types.attrs; + default = null; + description = '' + Sets the `Authorization` header on every scrape request with the configured credentials. + ''; + }; + job_name = mkOption { + type = types.str; + description = '' + The job name assigned to scraped metrics by default. + ''; + }; + scrape_interval = mkOpt types.str '' + How frequently to scrape targets from this job. Defaults to the + globally configured default. + ''; + + scrape_timeout = mkOpt types.str '' + Per-target timeout when scraping this job. Defaults to the + globally configured default. + ''; + + metrics_path = mkDefOpt types.str "/metrics" '' + The HTTP resource path on which to fetch metrics from targets. + ''; + + honor_labels = mkDefOpt types.bool "false" '' + Controls how Prometheus handles conflicts between labels + that are already present in scraped data and labels that + Prometheus would attach server-side ("job" and "instance" + labels, manually configured target labels, and labels + generated by service discovery implementations). + + If honor_labels is set to "true", label conflicts are + resolved by keeping label values from the scraped data and + ignoring the conflicting server-side labels. + + If honor_labels is set to "false", label conflicts are + resolved by renaming conflicting labels in the scraped data + to "exported_\<original-label\>" (for example + "exported_instance", "exported_job") and then attaching + server-side labels. This is useful for use cases such as + federation, where all labels specified in the target should + be preserved. + ''; + + honor_timestamps = mkDefOpt types.bool "true" '' + honor_timestamps controls whether Prometheus respects the timestamps present + in scraped data. + + If honor_timestamps is set to `true`, the timestamps of the metrics exposed + by the target will be used. + + If honor_timestamps is set to `false`, the timestamps of the metrics exposed + by the target will be ignored. + ''; + + scheme = mkDefOpt (types.enum [ "http" "https" ]) "http" '' + The URL scheme with which to fetch metrics from targets. + ''; + + params = mkOpt (types.attrsOf (types.listOf types.str)) '' + Optional HTTP URL parameters. + ''; + + basic_auth = mkOpt promTypes.basic_auth '' + Sets the `Authorization` header on every scrape request with the + configured username and password. + password and password_file are mutually exclusive. + ''; + + bearer_token = mkOpt types.str '' + Sets the `Authorization` header on every scrape request with + the configured bearer token. It is mutually exclusive with + {option}`bearer_token_file`. + ''; + + bearer_token_file = mkOpt types.str '' + Sets the `Authorization` header on every scrape request with + the bearer token read from the configured file. It is mutually + exclusive with {option}`bearer_token`. + ''; + + tls_config = mkOpt promTypes.tls_config '' + Configures the scrape request's TLS settings. + ''; + + proxy_url = mkOpt types.str '' + Optional proxy URL. + ''; + + azure_sd_configs = mkOpt (types.listOf promTypes.azure_sd_config) '' + List of Azure service discovery configurations. + ''; + + consul_sd_configs = mkOpt (types.listOf promTypes.consul_sd_config) '' + List of Consul service discovery configurations. + ''; + + digitalocean_sd_configs = mkOpt (types.listOf promTypes.digitalocean_sd_config) '' + List of DigitalOcean service discovery configurations. + ''; + + docker_sd_configs = mkOpt (types.listOf promTypes.docker_sd_config) '' + List of Docker service discovery configurations. + ''; + + dockerswarm_sd_configs = mkOpt (types.listOf promTypes.dockerswarm_sd_config) '' + List of Docker Swarm service discovery configurations. + ''; + + dns_sd_configs = mkOpt (types.listOf promTypes.dns_sd_config) '' + List of DNS service discovery configurations. + ''; + + ec2_sd_configs = mkOpt (types.listOf promTypes.ec2_sd_config) '' + List of EC2 service discovery configurations. + ''; + + eureka_sd_configs = mkOpt (types.listOf promTypes.eureka_sd_config) '' + List of Eureka service discovery configurations. + ''; + + file_sd_configs = mkOpt (types.listOf promTypes.file_sd_config) '' + List of file service discovery configurations. + ''; + + gce_sd_configs = mkOpt (types.listOf promTypes.gce_sd_config) '' + List of Google Compute Engine service discovery configurations. + + See [the relevant Prometheus configuration docs](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#gce_sd_config) + for more detail. + ''; + + hetzner_sd_configs = mkOpt (types.listOf promTypes.hetzner_sd_config) '' + List of Hetzner service discovery configurations. + ''; + + http_sd_configs = mkOpt (types.listOf promTypes.http_sd_config) '' + List of HTTP service discovery configurations. + ''; + + kubernetes_sd_configs = mkOpt (types.listOf promTypes.kubernetes_sd_config) '' + List of Kubernetes service discovery configurations. + ''; + + kuma_sd_configs = mkOpt (types.listOf promTypes.kuma_sd_config) '' + List of Kuma service discovery configurations. + ''; + + lightsail_sd_configs = mkOpt (types.listOf promTypes.lightsail_sd_config) '' + List of Lightsail service discovery configurations. + ''; + + linode_sd_configs = mkOpt (types.listOf promTypes.linode_sd_config) '' + List of Linode service discovery configurations. + ''; + + marathon_sd_configs = mkOpt (types.listOf promTypes.marathon_sd_config) '' + List of Marathon service discovery configurations. + ''; + + nerve_sd_configs = mkOpt (types.listOf promTypes.nerve_sd_config) '' + List of AirBnB's Nerve service discovery configurations. + ''; + + openstack_sd_configs = mkOpt (types.listOf promTypes.openstack_sd_config) '' + List of OpenStack service discovery configurations. + ''; + + puppetdb_sd_configs = mkOpt (types.listOf promTypes.puppetdb_sd_config) '' + List of PuppetDB service discovery configurations. + ''; + + scaleway_sd_configs = mkOpt (types.listOf promTypes.scaleway_sd_config) '' + List of Scaleway service discovery configurations. + ''; + + serverset_sd_configs = mkOpt (types.listOf promTypes.serverset_sd_config) '' + List of Zookeeper Serverset service discovery configurations. + ''; + + triton_sd_configs = mkOpt (types.listOf promTypes.triton_sd_config) '' + List of Triton Serverset service discovery configurations. + ''; + + uyuni_sd_configs = mkOpt (types.listOf promTypes.uyuni_sd_config) '' + List of Uyuni Serverset service discovery configurations. + ''; + + static_configs = mkOpt (types.listOf promTypes.static_config) '' + List of labeled target groups for this job. + ''; + + relabel_configs = mkOpt (types.listOf promTypes.relabel_config) '' + List of relabel configurations. + ''; + + metric_relabel_configs = mkOpt (types.listOf promTypes.relabel_config) '' + List of metric relabel configurations. + ''; + + body_size_limit = mkDefOpt types.str "0" '' + An uncompressed response body larger than this many bytes will cause the + scrape to fail. 0 means no limit. Example: 100MB. + This is an experimental feature, this behaviour could + change or be removed in the future. + ''; + + sample_limit = mkDefOpt types.int "0" '' + Per-scrape limit on number of scraped samples that will be accepted. + If more than this number of samples are present after metric relabelling + the entire scrape will be treated as failed. 0 means no limit. + ''; + + label_limit = mkDefOpt types.int "0" '' + Per-scrape limit on number of labels that will be accepted for a sample. If + more than this number of labels are present post metric-relabeling, the + entire scrape will be treated as failed. 0 means no limit. + ''; + + label_name_length_limit = mkDefOpt types.int "0" '' + Per-scrape limit on length of labels name that will be accepted for a sample. + If a label name is longer than this number post metric-relabeling, the entire + scrape will be treated as failed. 0 means no limit. + ''; + + label_value_length_limit = mkDefOpt types.int "0" '' + Per-scrape limit on length of labels value that will be accepted for a sample. + If a label value is longer than this number post metric-relabeling, the + entire scrape will be treated as failed. 0 means no limit. + ''; + + target_limit = mkDefOpt types.int "0" '' + Per-scrape config limit on number of unique targets that will be + accepted. If more than this number of targets are present after target + relabeling, Prometheus will mark the targets as failed without scraping them. + 0 means no limit. This is an experimental feature, this behaviour could + change in the future. + ''; + }; + }; + + # + # Config types: service discovery + # + + # For this one, the docs actually define all types needed to use mkSdConfigModule, but a bunch + # of them are marked with 'currently not support by Azure' so we don't bother adding them in + # here. + promTypes.azure_sd_config = types.submodule { + options = { + environment = mkDefOpt types.str "AzurePublicCloud" '' + The Azure environment. + ''; + + authentication_method = mkDefOpt (types.enum [ "OAuth" "ManagedIdentity" ]) "OAuth" '' + The authentication method, either OAuth or ManagedIdentity. + See https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview + ''; + + subscription_id = mkOption { + type = types.str; + description = '' + The subscription ID. + ''; + }; + + tenant_id = mkOpt types.str '' + Optional tenant ID. Only required with authentication_method OAuth. + ''; + + client_id = mkOpt types.str '' + Optional client ID. Only required with authentication_method OAuth. + ''; + + client_secret = mkOpt types.str '' + Optional client secret. Only required with authentication_method OAuth. + ''; + + refresh_interval = mkDefOpt types.str "300s" '' + Refresh interval to re-read the instance list. + ''; + + port = mkDefOpt types.port "80" '' + The port to scrape metrics from. If using the public IP + address, this must instead be specified in the relabeling + rule. + ''; + + proxy_url = mkOpt types.str '' + Optional proxy URL. + ''; + + follow_redirects = mkDefOpt types.bool "true" '' + Configure whether HTTP requests follow HTTP 3xx redirects. + ''; + + tls_config = mkOpt promTypes.tls_config '' + TLS configuration. + ''; + }; + }; + + promTypes.consul_sd_config = mkSdConfigModule { + server = mkDefOpt types.str "localhost:8500" '' + Consul server to query. + ''; + + token = mkOpt types.str "Consul token"; + + datacenter = mkOpt types.str "Consul datacenter"; + + scheme = mkDefOpt types.str "http" "Consul scheme"; + + username = mkOpt types.str "Consul username"; + + password = mkOpt types.str "Consul password"; + + tls_config = mkOpt promTypes.tls_config '' + Configures the Consul request's TLS settings. + ''; + + services = mkOpt (types.listOf types.str) '' + A list of services for which targets are retrieved. + ''; + + tags = mkOpt (types.listOf types.str) '' + An optional list of tags used to filter nodes for a given + service. Services must contain all tags in the list. + ''; + + node_meta = mkOpt (types.attrsOf types.str) '' + Node metadata used to filter nodes for a given service. + ''; + + tag_separator = mkDefOpt types.str "," '' + The string by which Consul tags are joined into the tag label. + ''; + + allow_stale = mkOpt types.bool '' + Allow stale Consul results + (see <https://www.consul.io/api/index.html#consistency-modes>). + + Will reduce load on Consul. + ''; + + refresh_interval = mkDefOpt types.str "30s" '' + The time after which the provided names are refreshed. + + On large setup it might be a good idea to increase this value + because the catalog will change all the time. + ''; + }; + + promTypes.digitalocean_sd_config = mkSdConfigModule { + port = mkDefOpt types.port "80" '' + The port to scrape metrics from. + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + The time after which the droplets are refreshed. + ''; + }; + + mkDockerSdConfigModule = extraOptions: mkSdConfigModule ({ + host = mkOption { + type = types.str; + description = '' + Address of the Docker daemon. + ''; + }; + + port = mkDefOpt types.port "80" '' + The port to scrape metrics from, when `role` is nodes, and for discovered + tasks and services that don't have published ports. + ''; + + filters = mkOpt + (types.listOf (types.submodule { + options = { + name = mkOption { + type = types.str; + description = '' + Name of the filter. The available filters are listed in the upstream documentation: + Services: <https://docs.docker.com/engine/api/v1.40/#operation/ServiceList> + Tasks: <https://docs.docker.com/engine/api/v1.40/#operation/TaskList> + Nodes: <https://docs.docker.com/engine/api/v1.40/#operation/NodeList> + ''; + }; + values = mkOption { + type = types.str; + description = '' + Value for the filter. + ''; + }; + }; + })) '' + Optional filters to limit the discovery process to a subset of available resources. + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + The time after which the containers are refreshed. + ''; + } // extraOptions); + + promTypes.docker_sd_config = mkDockerSdConfigModule { + host_networking_host = mkDefOpt types.str "localhost" '' + The host to use if the container is in host networking mode. + ''; + }; + + promTypes.dockerswarm_sd_config = mkDockerSdConfigModule { + role = mkOption { + type = types.enum [ "services" "tasks" "nodes" ]; + description = '' + Role of the targets to retrieve. Must be `services`, `tasks`, or `nodes`. + ''; + }; + }; + + promTypes.dns_sd_config = types.submodule { + options = { + names = mkOption { + type = types.listOf types.str; + description = '' + A list of DNS SRV record names to be queried. + ''; + }; + + type = mkDefOpt (types.enum [ "SRV" "A" "AAAA" ]) "SRV" '' + The type of DNS query to perform. One of SRV, A, or AAAA. + ''; + + port = mkOpt types.port '' + The port number used if the query type is not SRV. + ''; + + refresh_interval = mkDefOpt types.str "30s" '' + The time after which the provided names are refreshed. + ''; + }; + }; + + promTypes.ec2_sd_config = types.submodule { + options = { + region = mkOption { + type = types.str; + description = '' + The AWS Region. If blank, the region from the instance metadata is used. + ''; + }; + endpoint = mkOpt types.str '' + Custom endpoint to be used. + ''; + + access_key = mkOpt types.str '' + The AWS API key id. If blank, the environment variable + `AWS_ACCESS_KEY_ID` is used. + ''; + + secret_key = mkOpt types.str '' + The AWS API key secret. If blank, the environment variable + `AWS_SECRET_ACCESS_KEY` is used. + ''; + + profile = mkOpt types.str '' + Named AWS profile used to connect to the API. + ''; + + role_arn = mkOpt types.str '' + AWS Role ARN, an alternative to using AWS API keys. + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + Refresh interval to re-read the instance list. + ''; + + port = mkDefOpt types.port "80" '' + The port to scrape metrics from. If using the public IP + address, this must instead be specified in the relabeling + rule. + ''; + + filters = mkOpt + (types.listOf (types.submodule { + options = { + name = mkOption { + type = types.str; + description = '' + See [this list](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html) + for the available filters. + ''; + }; + + values = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Value of the filter. + ''; + }; + }; + })) '' + Filters can be used optionally to filter the instance list by other criteria. + ''; + }; + }; + + promTypes.eureka_sd_config = mkSdConfigModule { + server = mkOption { + type = types.str; + description = '' + The URL to connect to the Eureka server. + ''; + }; + }; + + promTypes.file_sd_config = types.submodule { + options = { + files = mkOption { + type = types.listOf types.str; + description = '' + Patterns for files from which target groups are extracted. Refer + to the Prometheus documentation for permitted filename patterns + and formats. + ''; + }; + + refresh_interval = mkDefOpt types.str "5m" '' + Refresh interval to re-read the files. + ''; + }; + }; + + promTypes.gce_sd_config = types.submodule { + options = { + # Use `mkOption` instead of `mkOpt` for project and zone because they are + # required configuration values for `gce_sd_config`. + project = mkOption { + type = types.str; + description = '' + The GCP Project. + ''; + }; + + zone = mkOption { + type = types.str; + description = '' + The zone of the scrape targets. If you need multiple zones use multiple + gce_sd_configs. + ''; + }; + + filter = mkOpt types.str '' + Filter can be used optionally to filter the instance list by other + criteria Syntax of this filter string is described here in the filter + query parameter section: <https://cloud.google.com/compute/docs/reference/latest/instances/list>. + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + Refresh interval to re-read the cloud instance list. + ''; + + port = mkDefOpt types.port "80" '' + The port to scrape metrics from. If using the public IP address, this + must instead be specified in the relabeling rule. + ''; + + tag_separator = mkDefOpt types.str "," '' + The tag separator used to separate concatenated GCE instance network tags. + + See the GCP documentation on network tags for more information: + <https://cloud.google.com/vpc/docs/add-remove-network-tags> + ''; + }; + }; + + promTypes.hetzner_sd_config = mkSdConfigModule { + role = mkOption { + type = types.enum [ "robot" "hcloud" ]; + description = '' + The Hetzner role of entities that should be discovered. + One of `robot` or `hcloud`. + ''; + }; + + port = mkDefOpt types.port "80" '' + The port to scrape metrics from. + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + The time after which the servers are refreshed. + ''; + }; + + promTypes.http_sd_config = types.submodule { + options = { + url = mkOption { + type = types.str; + description = '' + URL from which the targets are fetched. + ''; + }; + + refresh_interval = mkDefOpt types.str "60s" '' + Refresh interval to re-query the endpoint. + ''; + + basic_auth = mkOpt promTypes.basic_auth '' + Authentication information used to authenticate to the API server. + password and password_file are mutually exclusive. + ''; + + proxy_url = mkOpt types.str '' + Optional proxy URL. + ''; + + follow_redirects = mkDefOpt types.bool "true" '' + Configure whether HTTP requests follow HTTP 3xx redirects. + ''; + + tls_config = mkOpt promTypes.tls_config '' + Configures the scrape request's TLS settings. + ''; + }; + }; + + promTypes.kubernetes_sd_config = mkSdConfigModule { + api_server = mkOpt types.str '' + The API server addresses. If left empty, Prometheus is assumed to run inside + of the cluster and will discover API servers automatically and use the pod's + CA certificate and bearer token file at /var/run/secrets/kubernetes.io/serviceaccount/. + ''; + + role = mkOption { + type = types.enum [ "endpoints" "service" "pod" "node" "ingress" ]; + description = '' + The Kubernetes role of entities that should be discovered. + One of endpoints, service, pod, node, or ingress. + ''; + }; + + kubeconfig_file = mkOpt types.str '' + Optional path to a kubeconfig file. + Note that api_server and kube_config are mutually exclusive. + ''; + + namespaces = mkOpt + ( + types.submodule { + options = { + names = mkOpt (types.listOf types.str) '' + Namespace name. + ''; + }; + } + ) '' + Optional namespace discovery. If omitted, all namespaces are used. + ''; + + selectors = mkOpt + ( + types.listOf ( + types.submodule { + options = { + role = mkOption { + type = types.str; + description = '' + Selector role + ''; + }; + + label = mkOpt types.str '' + Selector label + ''; + + field = mkOpt types.str '' + Selector field + ''; + }; + } + ) + ) '' + Optional label and field selectors to limit the discovery process to a subset of available resources. + See https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/ + and https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ to learn more about the possible + filters that can be used. Endpoints role supports pod, service and endpoints selectors, other roles + only support selectors matching the role itself (e.g. node role can only contain node selectors). + + Note: When making decision about using field/label selector make sure that this + is the best approach - it will prevent Prometheus from reusing single list/watch + for all scrape configs. This might result in a bigger load on the Kubernetes API, + because per each selector combination there will be additional LIST/WATCH. On the other hand, + if you just want to monitor small subset of pods in large cluster it's recommended to use selectors. + Decision, if selectors should be used or not depends on the particular situation. + ''; + }; + + promTypes.kuma_sd_config = mkSdConfigModule { + server = mkOption { + type = types.str; + description = '' + Address of the Kuma Control Plane's MADS xDS server. + ''; + }; + + refresh_interval = mkDefOpt types.str "30s" '' + The time to wait between polling update requests. + ''; + + fetch_timeout = mkDefOpt types.str "2m" '' + The time after which the monitoring assignments are refreshed. + ''; + }; + + promTypes.lightsail_sd_config = types.submodule { + options = { + region = mkOpt types.str '' + The AWS region. If blank, the region from the instance metadata is used. + ''; + + endpoint = mkOpt types.str '' + Custom endpoint to be used. + ''; + + access_key = mkOpt types.str '' + The AWS API keys. If blank, the environment variable `AWS_ACCESS_KEY_ID` is used. + ''; + + secret_key = mkOpt types.str '' + The AWS API keys. If blank, the environment variable `AWS_SECRET_ACCESS_KEY` is used. + ''; + + profile = mkOpt types.str '' + Named AWS profile used to connect to the API. + ''; + + role_arn = mkOpt types.str '' + AWS Role ARN, an alternative to using AWS API keys. + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + Refresh interval to re-read the instance list. + ''; + + port = mkDefOpt types.port "80" '' + The port to scrape metrics from. If using the public IP address, this must + instead be specified in the relabeling rule. + ''; + }; + }; + + promTypes.linode_sd_config = mkSdConfigModule { + port = mkDefOpt types.port "80" '' + The port to scrape metrics from. + ''; + + tag_separator = mkDefOpt types.str "," '' + The string by which Linode Instance tags are joined into the tag label. + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + The time after which the linode instances are refreshed. + ''; + }; + + promTypes.marathon_sd_config = mkSdConfigModule { + servers = mkOption { + type = types.listOf types.str; + description = '' + List of URLs to be used to contact Marathon servers. You need to provide at least one server URL. + ''; + }; + + refresh_interval = mkDefOpt types.str "30s" '' + Polling interval. + ''; + + auth_token = mkOpt types.str '' + Optional authentication information for token-based authentication: + <https://docs.mesosphere.com/1.11/security/ent/iam-api/#passing-an-authentication-token> + It is mutually exclusive with `auth_token_file` and other authentication mechanisms. + ''; + + auth_token_file = mkOpt types.str '' + Optional authentication information for token-based authentication: + <https://docs.mesosphere.com/1.11/security/ent/iam-api/#passing-an-authentication-token> + It is mutually exclusive with `auth_token` and other authentication mechanisms. + ''; + }; + + promTypes.nerve_sd_config = types.submodule { + options = { + servers = mkOption { + type = types.listOf types.str; + description = '' + The Zookeeper servers. + ''; + }; + + paths = mkOption { + type = types.listOf types.str; + description = '' + Paths can point to a single service, or the root of a tree of services. + ''; + }; + + timeout = mkDefOpt types.str "10s" '' + Timeout value. + ''; + }; + }; + + promTypes.openstack_sd_config = types.submodule { + options = + let + userDescription = '' + username is required if using Identity V2 API. Consult with your provider's + control panel to discover your account's username. In Identity V3, either + userid or a combination of username and domain_id or domain_name are needed. + ''; + + domainDescription = '' + At most one of domain_id and domain_name must be provided if using username + with Identity V3. Otherwise, either are optional. + ''; + + projectDescription = '' + The project_id and project_name fields are optional for the Identity V2 API. + Some providers allow you to specify a project_name instead of the project_id. + Some require both. Your provider's authentication policies will determine + how these fields influence authentication. + ''; + + applicationDescription = '' + The application_credential_id or application_credential_name fields are + required if using an application credential to authenticate. Some providers + allow you to create an application credential to authenticate rather than a + password. + ''; + in + { + role = mkOption { + type = types.str; + description = '' + The OpenStack role of entities that should be discovered. + ''; + }; + + region = mkOption { + type = types.str; + description = '' + The OpenStack Region. + ''; + }; + + identity_endpoint = mkOpt types.str '' + identity_endpoint specifies the HTTP endpoint that is required to work with + the Identity API of the appropriate version. While it's ultimately needed by + all of the identity services, it will often be populated by a provider-level + function. + ''; + + username = mkOpt types.str userDescription; + userid = mkOpt types.str userDescription; + + password = mkOpt types.str '' + password for the Identity V2 and V3 APIs. Consult with your provider's + control panel to discover your account's preferred method of authentication. + ''; + + domain_name = mkOpt types.str domainDescription; + domain_id = mkOpt types.str domainDescription; + + project_name = mkOpt types.str projectDescription; + project_id = mkOpt types.str projectDescription; + + application_credential_name = mkOpt types.str applicationDescription; + application_credential_id = mkOpt types.str applicationDescription; + + application_credential_secret = mkOpt types.str '' + The application_credential_secret field is required if using an application + credential to authenticate. + ''; + + all_tenants = mkDefOpt types.bool "false" '' + Whether the service discovery should list all instances for all projects. + It is only relevant for the 'instance' role and usually requires admin permissions. + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + Refresh interval to re-read the instance list. + ''; + + port = mkDefOpt types.port "80" '' + The port to scrape metrics from. If using the public IP address, this must + instead be specified in the relabeling rule. + ''; + + availability = mkDefOpt (types.enum [ "public" "admin" "internal" ]) "public" '' + The availability of the endpoint to connect to. Must be one of public, admin or internal. + ''; + + tls_config = mkOpt promTypes.tls_config '' + TLS configuration. + ''; + }; + }; + + promTypes.puppetdb_sd_config = mkSdConfigModule { + url = mkOption { + type = types.str; + description = '' + The URL of the PuppetDB root query endpoint. + ''; + }; + + query = mkOption { + type = types.str; + description = '' + Puppet Query Language (PQL) query. Only resources are supported. + https://puppet.com/docs/puppetdb/latest/api/query/v4/pql.html + ''; + }; + + include_parameters = mkDefOpt types.bool "false" '' + Whether to include the parameters as meta labels. + Due to the differences between parameter types and Prometheus labels, + some parameters might not be rendered. The format of the parameters might + also change in future releases. + + Note: Enabling this exposes parameters in the Prometheus UI and API. Make sure + that you don't have secrets exposed as parameters if you enable this. + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + Refresh interval to re-read the resources list. + ''; + + port = mkDefOpt types.port "80" '' + The port to scrape metrics from. + ''; + }; + + promTypes.scaleway_sd_config = types.submodule { + options = { + access_key = mkOption { + type = types.str; + description = '' + Access key to use. https://console.scaleway.com/project/credentials + ''; + }; + + secret_key = mkOpt types.str '' + Secret key to use when listing targets. https://console.scaleway.com/project/credentials + It is mutually exclusive with `secret_key_file`. + ''; + + secret_key_file = mkOpt types.str '' + Sets the secret key with the credentials read from the configured file. + It is mutually exclusive with `secret_key`. + ''; + + project_id = mkOption { + type = types.str; + description = '' + Project ID of the targets. + ''; + }; + + role = mkOption { + type = types.enum [ "instance" "baremetal" ]; + description = '' + Role of the targets to retrieve. Must be `instance` or `baremetal`. + ''; + }; + + port = mkDefOpt types.port "80" '' + The port to scrape metrics from. + ''; + + api_url = mkDefOpt types.str "https://api.scaleway.com" '' + API URL to use when doing the server listing requests. + ''; + + zone = mkDefOpt types.str "fr-par-1" '' + Zone is the availability zone of your targets (e.g. fr-par-1). + ''; + + name_filter = mkOpt types.str '' + Specify a name filter (works as a LIKE) to apply on the server listing request. + ''; + + tags_filter = mkOpt (types.listOf types.str) '' + Specify a tag filter (a server needs to have all defined tags to be listed) to apply on the server listing request. + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + Refresh interval to re-read the managed targets list. + ''; + + proxy_url = mkOpt types.str '' + Optional proxy URL. + ''; + + follow_redirects = mkDefOpt types.bool "true" '' + Configure whether HTTP requests follow HTTP 3xx redirects. + ''; + + tls_config = mkOpt promTypes.tls_config '' + TLS configuration. + ''; + }; + }; + + # These are exactly the same. + promTypes.serverset_sd_config = promTypes.nerve_sd_config; + + promTypes.triton_sd_config = types.submodule { + options = { + account = mkOption { + type = types.str; + description = '' + The account to use for discovering new targets. + ''; + }; + + role = mkDefOpt (types.enum [ "container" "cn" ]) "container" '' + The type of targets to discover, can be set to: + - "container" to discover virtual machines (SmartOS zones, lx/KVM/bhyve branded zones) running on Triton + - "cn" to discover compute nodes (servers/global zones) making up the Triton infrastructure + ''; + + dns_suffix = mkOption { + type = types.str; + description = '' + The DNS suffix which should be applied to target. + ''; + }; + + endpoint = mkOption { + type = types.str; + description = '' + The Triton discovery endpoint (e.g. `cmon.us-east-3b.triton.zone`). This is + often the same value as dns_suffix. + ''; + }; + + groups = mkOpt (types.listOf types.str) '' + A list of groups for which targets are retrieved, only supported when targeting the `container` role. + If omitted all containers owned by the requesting account are scraped. + ''; + + port = mkDefOpt types.port "9163" '' + The port to use for discovery and metric scraping. + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + The interval which should be used for refreshing targets. + ''; + + version = mkDefOpt types.int "1" '' + The Triton discovery API version. + ''; + + tls_config = mkOpt promTypes.tls_config '' + TLS configuration. + ''; + }; + }; + + promTypes.uyuni_sd_config = mkSdConfigModule { + server = mkOption { + type = types.str; + description = '' + The URL to connect to the Uyuni server. + ''; + }; + + username = mkOption { + type = types.str; + description = '' + Credentials are used to authenticate the requests to Uyuni API. + ''; + }; + + password = mkOption { + type = types.str; + description = '' + Credentials are used to authenticate the requests to Uyuni API. + ''; + }; + + entitlement = mkDefOpt types.str "monitoring_entitled" '' + The entitlement string to filter eligible systems. + ''; + + separator = mkDefOpt types.str "," '' + The string by which Uyuni group names are joined into the groups label + ''; + + refresh_interval = mkDefOpt types.str "60s" '' + Refresh interval to re-read the managed targets list. + ''; + }; + + promTypes.static_config = types.submodule { + options = { + targets = mkOption { + type = types.listOf types.str; + description = '' + The targets specified by the target group. + ''; + }; + labels = mkOption { + type = types.attrsOf types.str; + default = { }; + description = '' + Labels assigned to all metrics scraped from the targets. + ''; + }; + }; + }; + + # + # Config types: relabling + # + + promTypes.relabel_config = types.submodule { + options = { + source_labels = mkOpt (types.listOf types.str) '' + The source labels select values from existing labels. Their content + is concatenated using the configured separator and matched against + the configured regular expression. + ''; + + separator = mkDefOpt types.str ";" '' + Separator placed between concatenated source label values. + ''; + + target_label = mkOpt types.str '' + Label to which the resulting value is written in a replace action. + It is mandatory for replace actions. + ''; + + regex = mkDefOpt types.str "(.*)" '' + Regular expression against which the extracted value is matched. + ''; + + modulus = mkOpt types.int '' + Modulus to take of the hash of the source label values. + ''; + + replacement = mkDefOpt types.str "$1" '' + Replacement value against which a regex replace is performed if the + regular expression matches. + ''; + + action = + mkDefOpt (types.enum [ "replace" "lowercase" "uppercase" "keep" "drop" "hashmod" "labelmap" "labeldrop" "labelkeep" ]) "replace" '' + Action to perform based on regex matching. + ''; + }; + }; + + # + # Config types : remote read / write + # + + promTypes.remote_write = types.submodule { + options = { + url = mkOption { + type = types.str; + description = '' + ServerName extension to indicate the name of the server. + http://tools.ietf.org/html/rfc4366#section-3.1 + ''; + }; + remote_timeout = mkOpt types.str '' + Timeout for requests to the remote write endpoint. + ''; + headers = mkOpt (types.attrsOf types.str) '' + Custom HTTP headers to be sent along with each remote write request. + Be aware that headers that are set by Prometheus itself can't be overwritten. + ''; + write_relabel_configs = mkOpt (types.listOf promTypes.relabel_config) '' + List of remote write relabel configurations. + ''; + name = mkOpt types.str '' + Name of the remote write config, which if specified must be unique among remote write configs. + The name will be used in metrics and logging in place of a generated value to help users distinguish between + remote write configs. + ''; + basic_auth = mkOpt promTypes.basic_auth '' + Sets the `Authorization` header on every remote write request with the + configured username and password. + password and password_file are mutually exclusive. + ''; + bearer_token = mkOpt types.str '' + Sets the `Authorization` header on every remote write request with + the configured bearer token. It is mutually exclusive with `bearer_token_file`. + ''; + bearer_token_file = mkOpt types.str '' + Sets the `Authorization` header on every remote write request with the bearer token + read from the configured file. It is mutually exclusive with `bearer_token`. + ''; + tls_config = mkOpt promTypes.tls_config '' + Configures the remote write request's TLS settings. + ''; + proxy_url = mkOpt types.str "Optional Proxy URL."; + queue_config = mkOpt + (types.submodule { + options = { + capacity = mkOpt types.int '' + Number of samples to buffer per shard before we block reading of more + samples from the WAL. It is recommended to have enough capacity in each + shard to buffer several requests to keep throughput up while processing + occasional slow remote requests. + ''; + max_shards = mkOpt types.int '' + Maximum number of shards, i.e. amount of concurrency. + ''; + min_shards = mkOpt types.int '' + Minimum number of shards, i.e. amount of concurrency. + ''; + max_samples_per_send = mkOpt types.int '' + Maximum number of samples per send. + ''; + batch_send_deadline = mkOpt types.str '' + Maximum time a sample will wait in buffer. + ''; + min_backoff = mkOpt types.str '' + Initial retry delay. Gets doubled for every retry. + ''; + max_backoff = mkOpt types.str '' + Maximum retry delay. + ''; + }; + }) '' + Configures the queue used to write to remote storage. + ''; + metadata_config = mkOpt + (types.submodule { + options = { + send = mkOpt types.bool '' + Whether metric metadata is sent to remote storage or not. + ''; + send_interval = mkOpt types.str '' + How frequently metric metadata is sent to remote storage. + ''; + }; + }) '' + Configures the sending of series metadata to remote storage. + Metadata configuration is subject to change at any point + or be removed in future releases. + ''; + }; + }; + + promTypes.remote_read = types.submodule { + options = { + url = mkOption { + type = types.str; + description = '' + ServerName extension to indicate the name of the server. + http://tools.ietf.org/html/rfc4366#section-3.1 + ''; + }; + name = mkOpt types.str '' + Name of the remote read config, which if specified must be unique among remote read configs. + The name will be used in metrics and logging in place of a generated value to help users distinguish between + remote read configs. + ''; + required_matchers = mkOpt (types.attrsOf types.str) '' + An optional list of equality matchers which have to be + present in a selector to query the remote read endpoint. + ''; + remote_timeout = mkOpt types.str '' + Timeout for requests to the remote read endpoint. + ''; + headers = mkOpt (types.attrsOf types.str) '' + Custom HTTP headers to be sent along with each remote read request. + Be aware that headers that are set by Prometheus itself can't be overwritten. + ''; + read_recent = mkOpt types.bool '' + Whether reads should be made for queries for time ranges that + the local storage should have complete data for. + ''; + basic_auth = mkOpt promTypes.basic_auth '' + Sets the `Authorization` header on every remote read request with the + configured username and password. + password and password_file are mutually exclusive. + ''; + bearer_token = mkOpt types.str '' + Sets the `Authorization` header on every remote read request with + the configured bearer token. It is mutually exclusive with `bearer_token_file`. + ''; + bearer_token_file = mkOpt types.str '' + Sets the `Authorization` header on every remote read request with the bearer token + read from the configured file. It is mutually exclusive with `bearer_token`. + ''; + tls_config = mkOpt promTypes.tls_config '' + Configures the remote read request's TLS settings. + ''; + proxy_url = mkOpt types.str "Optional Proxy URL."; + }; + }; + +in +{ + + imports = [ + (mkRenamedOptionModule [ "services" "prometheus2" ] [ "services" "prometheus" ]) + (mkRemovedOptionModule [ "services" "prometheus" "environmentFile" ] + "It has been removed since it was causing issues (https://github.com/NixOS/nixpkgs/issues/126083) and Prometheus now has native support for secret files, i.e. `basic_auth.password_file` and `authorization.credentials_file`.") + (mkRemovedOptionModule [ "services" "prometheus" "alertmanagerTimeout" ] + "Deprecated upstream and no longer had any effect") + ]; + + options.services.prometheus = { + + enable = mkEnableOption "Prometheus monitoring daemon"; + + package = mkPackageOption pkgs "prometheus" { }; + + port = mkOption { + type = types.port; + default = 9090; + description = '' + Port to listen on. + ''; + }; + + listenAddress = mkOption { + type = types.str; + default = "0.0.0.0"; + description = '' + Address to listen on for the web interface, API, and telemetry. + ''; + }; + + stateDir = mkOption { + type = types.str; + default = "prometheus2"; + description = '' + Directory below `/var/lib` to store Prometheus metrics data. + This directory will be created automatically using systemd's StateDirectory mechanism. + ''; + }; + + extraFlags = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Extra commandline options when launching Prometheus. + ''; + }; + + enableReload = mkOption { + default = false; + type = types.bool; + description = '' + Reload prometheus when configuration file changes (instead of restart). + + The following property holds: switching to a configuration + (`switch-to-configuration`) that changes the prometheus + configuration only finishes successfully when prometheus has finished + loading the new configuration. + ''; + }; + + enableAgentMode = mkEnableOption "agent mode"; + + configText = mkOption { + type = types.nullOr types.lines; + default = null; + description = '' + If non-null, this option defines the text that is written to + prometheus.yml. If null, the contents of prometheus.yml is generated + from the structured config options. + ''; + }; + + globalConfig = mkOption { + type = promTypes.globalConfig; + default = { }; + description = '' + Parameters that are valid in all configuration contexts. They + also serve as defaults for other configuration sections + ''; + }; + + remoteRead = mkOption { + type = types.listOf promTypes.remote_read; + default = [ ]; + description = '' + Parameters of the endpoints to query from. + See [the official documentation](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_read) for more information. + ''; + }; + + remoteWrite = mkOption { + type = types.listOf promTypes.remote_write; + default = [ ]; + description = '' + Parameters of the endpoints to send samples to. + See [the official documentation](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write) for more information. + ''; + }; + + rules = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + Alerting and/or Recording rules to evaluate at runtime. + ''; + }; + + ruleFiles = mkOption { + type = types.listOf types.path; + default = [ ]; + description = '' + Any additional rules files to include in this configuration. + ''; + }; + + scrapeConfigs = mkOption { + type = types.listOf promTypes.scrape_config; + default = [ ]; + description = '' + A list of scrape configurations. + ''; + }; + + alertmanagers = mkOption { + type = types.listOf types.attrs; + example = literalExpression '' + [ { + scheme = "https"; + path_prefix = "/alertmanager"; + static_configs = [ { + targets = [ + "prometheus.domain.tld" + ]; + } ]; + } ] + ''; + default = [ ]; + description = '' + A list of alertmanagers to send alerts to. + See [the official documentation](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#alertmanager_config) for more information. + ''; + }; + + alertmanagerNotificationQueueCapacity = mkOption { + type = types.int; + default = 10000; + description = '' + The capacity of the queue for pending alert manager notifications. + ''; + }; + + webExternalUrl = mkOption { + type = types.nullOr types.str; + default = null; + example = "https://example.com/"; + description = '' + The URL under which Prometheus is externally reachable (for example, + if Prometheus is served via a reverse proxy). + ''; + }; + + webConfigFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Specifies which file should be used as web.config.file and be passed on startup. + See https://prometheus.io/docs/prometheus/latest/configuration/https/ for valid options. + ''; + }; + + checkConfig = mkOption { + type = with types; either bool (enum [ "syntax-only" ]); + default = true; + example = "syntax-only"; + description = '' + Check configuration with `promtool check`. The call to `promtool` is + subject to sandboxing by Nix. + + If you use credentials stored in external files + (`password_file`, `bearer_token_file`, etc), + they will not be visible to `promtool` + and it will report errors, despite a correct configuration. + To resolve this, you may set this option to `"syntax-only"` + in order to only syntax check the Prometheus configuration. + ''; + }; + + retentionTime = mkOption { + type = types.nullOr types.str; + default = null; + example = "15d"; + description = '' + How long to retain samples in storage. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + ( + let + # Match something with dots (an IPv4 address) or something ending in + # a square bracket (an IPv6 addresses) followed by a port number. + legacy = builtins.match "(.*\\..*|.*]):([[:digit:]]+)" cfg.listenAddress; + in + { + assertion = legacy == null; + message = '' + Do not specify the port for Prometheus to listen on in the + listenAddress option; use the port option instead: + services.prometheus.listenAddress = ${builtins.elemAt legacy 0}; + services.prometheus.port = ${builtins.elemAt legacy 1}; + ''; + } + ) + ]; + + users.groups.prometheus.gid = config.ids.gids.prometheus; + users.users.prometheus = { + description = "Prometheus daemon user"; + uid = config.ids.uids.prometheus; + group = "prometheus"; + }; + environment.etc."prometheus/prometheus.yaml" = mkIf cfg.enableReload { + source = prometheusYml; + }; + systemd.services.prometheus = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + ExecStart = "${cfg.package}/bin/prometheus" + + optionalString (length cmdlineArgs != 0) (" \\\n " + + concatStringsSep " \\\n " cmdlineArgs); + ExecReload = mkIf cfg.enableReload "+${reload}/bin/reload-prometheus"; + User = "prometheus"; + Restart = "always"; + RuntimeDirectory = "prometheus"; + RuntimeDirectoryMode = "0700"; + WorkingDirectory = workingDir; + StateDirectory = cfg.stateDir; + StateDirectoryMode = "0700"; + # Hardening + AmbientCapabilities = lib.mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = if (cfg.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ]; + DeviceAllow = [ "/dev/null rw" ]; + DevicePolicy = "strict"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "full"; + RemoveIPC = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@privileged" ]; + }; + }; + # prometheus-config-reload will activate after prometheus. However, what we + # don't want is that on startup it immediately reloads prometheus because + # prometheus itself might have just started. + # + # Instead we only want to reload prometheus when the config file has + # changed. So on startup prometheus-config-reload will just output a + # harmless message and then stay active (RemainAfterExit). + # + # Then, when the config file has changed, switch-to-configuration notices + # that this service has changed (restartTriggers) and needs to be reloaded + # (reloadIfChanged). The reload command then reloads prometheus. + systemd.services.prometheus-config-reload = mkIf cfg.enableReload { + wantedBy = [ "prometheus.service" ]; + after = [ "prometheus.service" ]; + reloadIfChanged = true; + restartTriggers = [ prometheusYml ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + TimeoutSec = 60; + ExecStart = "${pkgs.logger}/bin/logger 'prometheus-config-reload will only reload prometheus when reloaded itself.'"; + ExecReload = [ "${triggerReload}/bin/trigger-reload-prometheus" ]; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters.md b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters.md new file mode 100644 index 000000000000..d291020d3673 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters.md @@ -0,0 +1,185 @@ +# Prometheus exporters {#module-services-prometheus-exporters} + +Prometheus exporters provide metrics for the +[prometheus monitoring system](https://prometheus.io). + +## Configuration {#module-services-prometheus-exporters-configuration} + +One of the most common exporters is the +[node exporter](https://github.com/prometheus/node_exporter), +it provides hardware and OS metrics from the host it's +running on. The exporter could be configured as follows: +```nix +{ + services.prometheus.exporters.node = { + enable = true; + port = 9100; + enabledCollectors = [ + "logind" + "systemd" + ]; + disabledCollectors = [ + "textfile" + ]; + openFirewall = true; + firewallFilter = "-i br0 -p tcp -m tcp --dport 9100"; + }; +} +``` +It should now serve all metrics from the collectors that are explicitly +enabled and the ones that are +[enabled by default](https://github.com/prometheus/node_exporter#enabled-by-default), +via http under `/metrics`. In this +example the firewall should just allow incoming connections to the +exporter's port on the bridge interface `br0` (this would +have to be configured separately of course). For more information about +configuration see `man configuration.nix` or search through +the [available options](https://nixos.org/nixos/options.html#prometheus.exporters). + +Prometheus can now be configured to consume the metrics produced by the exporter: +```nix +{ + services.prometheus = { + # ... + + scrapeConfigs = [ + { + job_name = "node"; + static_configs = [{ + targets = [ "localhost:${toString config.services.prometheus.exporters.node.port}" ]; + }]; + } + ]; + + # ... + }; +} +``` + +## Adding a new exporter {#module-services-prometheus-exporters-new-exporter} + +To add a new exporter, it has to be packaged first (see +`nixpkgs/pkgs/servers/monitoring/prometheus/` for +examples), then a module can be added. The postfix exporter is used in this +example: + + - Some default options for all exporters are provided by + `nixpkgs/nixos/modules/services/monitoring/prometheus/exporters.nix`: + + - `enable` + - `port` + - `listenAddress` + - `extraFlags` + - `openFirewall` + - `firewallFilter` + - `firewallRules` + - `user` + - `group` + - As there is already a package available, the module can now be added. This + is accomplished by adding a new file to the + `nixos/modules/services/monitoring/prometheus/exporters/` + directory, which will be called postfix.nix and contains all exporter + specific options and configuration: + ```nix + # nixpkgs/nixos/modules/services/prometheus/exporters/postfix.nix + { config, lib, pkgs, options }: + + with lib; + + let + # for convenience we define cfg here + cfg = config.services.prometheus.exporters.postfix; + in + { + port = 9154; # The postfix exporter listens on this port by default + + # `extraOpts` is an attribute set which contains additional options + # (and optional overrides for default options). + # Note that this attribute is optional. + extraOpts = { + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + logfilePath = mkOption { + type = types.path; + default = /var/log/postfix_exporter_input.log; + example = /var/log/mail.log; + description = '' + Path where Postfix writes log entries. + This file will be truncated by this exporter! + ''; + }; + showqPath = mkOption { + type = types.path; + default = /var/spool/postfix/public/showq; + example = /var/lib/postfix/queue/public/showq; + description = '' + Path at which Postfix places its showq socket. + ''; + }; + }; + + # `serviceOpts` is an attribute set which contains configuration + # for the exporter's systemd service. One of + # `serviceOpts.script` and `serviceOpts.serviceConfig.ExecStart` + # has to be specified here. This will be merged with the default + # service configuration. + # Note that by default 'DynamicUser' is 'true'. + serviceOpts = { + serviceConfig = { + DynamicUser = false; + ExecStart = '' + ${pkgs.prometheus-postfix-exporter}/bin/postfix_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path ${cfg.telemetryPath} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; + } + ``` + - This should already be enough for the postfix exporter. Additionally one + could now add assertions and conditional default values. This can be done + in the 'meta-module' that combines all exporter definitions and generates + the submodules: + `nixpkgs/nixos/modules/services/prometheus/exporters.nix` + +## Updating an exporter module {#module-services-prometheus-exporters-update-exporter-module} + +Should an exporter option change at some point, it is possible to add +information about the change to the exporter definition similar to +`nixpkgs/nixos/modules/rename.nix`: +```nix +{ config, lib, pkgs, options }: + +with lib; + +let + cfg = config.services.prometheus.exporters.nginx; +in +{ + port = 9113; + extraOpts = { + # additional module options + # ... + }; + serviceOpts = { + # service configuration + # ... + }; + imports = [ + # 'services.prometheus.exporters.nginx.telemetryEndpoint' -> 'services.prometheus.exporters.nginx.telemetryPath' + (mkRenamedOptionModule [ "telemetryEndpoint" ] [ "telemetryPath" ]) + + # removed option 'services.prometheus.exporters.nginx.insecure' + (mkRemovedOptionModule [ "insecure" ] '' + This option was replaced by 'prometheus.exporters.nginx.sslVerify' which defaults to true. + '') + ({ options.warnings = options.warnings; }) + ]; +} +``` diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters.nix new file mode 100644 index 000000000000..2dc12a221bf0 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters.nix @@ -0,0 +1,461 @@ +{ 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; + + 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" + "buildkite-agent" + "collectd" + "dmarc" + "dnsmasq" + "dnssec" + "domain" + "dovecot" + "fastly" + "flow" + "fritz" + "fritzbox" + "graphite" + "idrac" + "imap-mailstat" + "influxdb" + "ipmi" + "jitsi" + "json" + "junos-czerwonk" + "kea" + "keylight" + "knot" + "lnd" + "mail" + "mikrotik" + "minio" + "modemmanager" + "mongodb" + "mysqld" + "nats" + "nextcloud" + "nginx" + "nginxlog" + "node" + "nut" + "openldap" + "pgbouncer" + "php-fpm" + "pihole" + "ping" + "postfix" + "postgres" + "process" + "pve" + "py-air-control" + "redis" + "restic" + "rspamd" + "rtl_433" + "sabnzbd" + "scaphandre" + "script" + "shelly" + "smartctl" + "smokeping" + "snmp" + "sql" + "statsd" + "surfboard" + "systemd" + "tor" + "unbound" + "unifi" + "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 []; + users.users."${name}-exporter" = (mkIf (conf.user == "${name}-exporter" && !enableDynamicUser) { + description = "Prometheus ${name} exporter service user"; + isSystemUser = true; + inherit (conf) group; + }); + users.groups = (mkIf (conf.group == "${name}-exporter" && !enableDynamicUser) { + "${name}-exporter" = {}; + }); + 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 +{ + + imports = (lib.forEach [ "blackboxExporter" "collectdExporter" "fritzboxExporter" + "jsonExporter" "minioExporter" "nginxExporter" "nodeExporter" + "snmpExporter" "unifiExporter" "varnishExporter" ] + (opt: lib.mkRemovedOptionModule [ "services" "prometheus" "${opt}" ] '' + The prometheus exporters are now configured using `services.prometheus.exporters'. + See the 18.03 release notes for more information. + '' )); + + options.services.prometheus.exporters = mkOption { + type = types.submodule { + options = (mkSubModules); + imports = [ + ../../../misc/assertions.nix + (lib.mkRenamedOptionModule [ "unifi-poller" ] [ "unpoller" ]) + ]; + }; + 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.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.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) + ); + 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! + ''; + } ] ++ (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. + '' + ) + (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; + services.prometheus.exporters.minio.minioAccessSecret = mkDefault config.services.minio.secretKey; + })] ++ [(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; + })] ++ (mapAttrsToList (name: conf: + mkExporterConf { + inherit name; + inherit (conf) serviceOpts; + conf = cfg.${name}; + }) exporterOpts) + ); + + meta = { + doc = ./exporters.md; + maintainers = [ maintainers.willibutz ]; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/apcupsd.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/apcupsd.nix new file mode 100644 index 000000000000..f188fe1f68fb --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/apcupsd.nix @@ -0,0 +1,37 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.apcupsd; + inherit (lib) mkOption types concatStringsSep; +in +{ + port = 9162; + extraOpts = { + apcupsdAddress = mkOption { + type = types.str; + default = ":3551"; + description = '' + Address of the apcupsd Network Information Server (NIS). + ''; + }; + + apcupsdNetwork = mkOption { + type = types.enum ["tcp" "tcp4" "tcp6"]; + default = "tcp"; + description = '' + Network of the apcupsd Network Information Server (NIS): one of "tcp", "tcp4", or "tcp6". + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-apcupsd-exporter}/bin/apcupsd_exporter \ + -telemetry.addr ${cfg.listenAddress}:${toString cfg.port} \ + -apcupsd.addr ${cfg.apcupsdAddress} \ + -apcupsd.network ${cfg.apcupsdNetwork} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix new file mode 100644 index 000000000000..e98982c0dd31 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/artifactory.nix @@ -0,0 +1,58 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.artifactory; + inherit (lib) mkOption types concatStringsSep; +in +{ + port = 9531; + extraOpts = { + scrapeUri = mkOption { + type = types.str; + default = "http://localhost:8081/artifactory"; + description = '' + URI on which to scrape JFrog Artifactory. + ''; + }; + + artiUsername = mkOption { + type = types.str; + description = '' + Username for authentication against JFrog Artifactory API. + ''; + }; + + artiPassword = mkOption { + type = types.str; + default = ""; + description = '' + Password for authentication against JFrog Artifactory API. + One of the password or access token needs to be set. + ''; + }; + + artiAccessToken = mkOption { + type = types.str; + default = ""; + description = '' + Access token for authentication against JFrog Artifactory API. + One of the password or access token needs to be set. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-artifactory-exporter}/bin/artifactory_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --artifactory.scrape-uri ${cfg.scrapeUri} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + Environment = [ + "ARTI_USERNAME=${cfg.artiUsername}" + "ARTI_PASSWORD=${cfg.artiPassword}" + "ARTI_ACCESS_TOKEN=${cfg.artiAccessToken}" + ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/bind.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/bind.nix new file mode 100644 index 000000000000..1c7dcf8b1ef0 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/bind.nix @@ -0,0 +1,53 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.bind; + inherit (lib) mkOption types concatStringsSep; +in +{ + port = 9119; + extraOpts = { + bindURI = mkOption { + type = types.str; + default = "http://localhost:8053/"; + description = '' + HTTP XML API address of an Bind server. + ''; + }; + bindTimeout = mkOption { + type = types.str; + default = "10s"; + description = '' + Timeout for trying to get stats from Bind. + ''; + }; + bindVersion = mkOption { + type = types.enum [ "xml.v2" "xml.v3" "auto" ]; + default = "auto"; + description = '' + BIND statistics version. Can be detected automatically. + ''; + }; + bindGroups = mkOption { + type = types.listOf (types.enum [ "server" "view" "tasks" ]); + default = [ "server" "view" ]; + description = '' + List of statistics to collect. Available: [server, view, tasks] + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-bind-exporter}/bin/bind_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --bind.pid-file /var/run/named/named.pid \ + --bind.timeout ${toString cfg.bindTimeout} \ + --bind.stats-url ${cfg.bindURI} \ + --bind.stats-version ${cfg.bindVersion} \ + --bind.stats-groups ${concatStringsSep "," cfg.bindGroups} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/bird.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/bird.nix new file mode 100644 index 000000000000..5d91eeed106d --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/bird.nix @@ -0,0 +1,54 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.bird; + inherit (lib) + mkOption + types + concatStringsSep + singleton + ; +in +{ + port = 9324; + extraOpts = { + birdVersion = mkOption { + type = types.enum [ 1 2 ]; + default = 2; + description = '' + Specifies whether BIRD1 or BIRD2 is in use. + ''; + }; + birdSocket = mkOption { + type = types.path; + default = "/run/bird/bird.ctl"; + description = '' + Path to BIRD2 (or BIRD1 v4) socket. + ''; + }; + newMetricFormat = mkOption { + type = types.bool; + default = true; + description = '' + Enable the new more-generic metric format. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + SupplementaryGroups = singleton (if cfg.birdVersion == 1 then "bird" else "bird2"); + ExecStart = '' + ${pkgs.prometheus-bird-exporter}/bin/bird_exporter \ + -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + -bird.socket ${cfg.birdSocket} \ + -bird.v2=${if cfg.birdVersion == 2 then "true" else "false"} \ + -format.new=${if cfg.newMetricFormat then "true" else "false"} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + RestrictAddressFamilies = [ + # Need AF_UNIX to collect data + "AF_UNIX" + ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix new file mode 100644 index 000000000000..e44140b1f51a --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/bitcoin.nix @@ -0,0 +1,81 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.bitcoin; + inherit (lib) mkOption types concatStringsSep; +in +{ + port = 9332; + extraOpts = { + rpcUser = mkOption { + type = types.str; + default = "bitcoinrpc"; + description = '' + RPC user name. + ''; + }; + + rpcPasswordFile = mkOption { + type = types.path; + description = '' + File containing RPC password. + ''; + }; + + rpcScheme = mkOption { + type = types.enum [ "http" "https" ]; + default = "http"; + description = '' + Whether to connect to bitcoind over http or https. + ''; + }; + + rpcHost = mkOption { + type = types.str; + default = "localhost"; + description = '' + RPC host. + ''; + }; + + rpcPort = mkOption { + type = types.port; + default = 8332; + description = '' + RPC port number. + ''; + }; + + refreshSeconds = mkOption { + type = types.ints.unsigned; + default = 300; + description = '' + How often to ask bitcoind for metrics. + ''; + }; + + extraEnv = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Extra environment variables for the exporter. + ''; + }; + }; + serviceOpts = { + script = '' + export BITCOIN_RPC_PASSWORD=$(cat ${cfg.rpcPasswordFile}) + exec ${pkgs.prometheus-bitcoin-exporter}/bin/bitcoind-monitor.py + ''; + + environment = { + BITCOIN_RPC_USER = cfg.rpcUser; + BITCOIN_RPC_SCHEME = cfg.rpcScheme; + BITCOIN_RPC_HOST = cfg.rpcHost; + BITCOIN_RPC_PORT = toString cfg.rpcPort; + METRICS_ADDR = cfg.listenAddress; + METRICS_PORT = toString cfg.port; + REFRESH_SECONDS = toString cfg.refreshSeconds; + } // cfg.extraEnv; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix new file mode 100644 index 000000000000..33a1fdc52805 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/blackbox.nix @@ -0,0 +1,74 @@ +{ config, lib, pkgs, options, ... }: + +let + logPrefix = "services.prometheus.exporter.blackbox"; + cfg = config.services.prometheus.exporters.blackbox; + inherit (lib) + mkOption + types + concatStringsSep + escapeShellArg + ; + + # This ensures that we can deal with string paths, path types and + # store-path strings with context. + coerceConfigFile = file: + if (builtins.isPath file) || (lib.isStorePath file) then + file + else + (lib.warn '' + ${logPrefix}: configuration file "${file}" is being copied to the nix-store. + If you would like to avoid that, please set enableConfigCheck to false. + '' /. + file); + checkConfigLocation = file: + if lib.hasPrefix "/tmp/" file then + throw + "${logPrefix}: configuration file must not reside within /tmp - it won't be visible to the systemd service." + else + file; + checkConfig = file: + pkgs.runCommand "checked-blackbox-exporter.conf" { + preferLocalBuild = true; + nativeBuildInputs = [ pkgs.buildPackages.prometheus-blackbox-exporter ]; + } '' + ln -s ${coerceConfigFile file} $out + blackbox_exporter --config.check --config.file $out + ''; +in { + port = 9115; + extraOpts = { + configFile = mkOption { + type = types.path; + description = '' + Path to configuration file. + ''; + }; + enableConfigCheck = mkOption { + type = types.bool; + default = true; + description = '' + Whether to run a correctness check for the configuration file. This depends + on the configuration file residing in the nix-store. Paths passed as string will + be copied to the store. + ''; + }; + }; + + serviceOpts = let + adjustedConfigFile = if cfg.enableConfigCheck then + checkConfig cfg.configFile + else + checkConfigLocation cfg.configFile; + in { + serviceConfig = { + AmbientCapabilities = [ "CAP_NET_RAW" ]; # for ping probes + ExecStart = '' + ${pkgs.prometheus-blackbox-exporter}/bin/blackbox_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --config.file ${escapeShellArg adjustedConfigFile} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix new file mode 100644 index 000000000000..0af1e33b2c44 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/buildkite-agent.nix @@ -0,0 +1,69 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.buildkite-agent; + inherit (lib) + mkOption + types + concatStringsSep + optionalString + literalExpression + ; +in +{ + port = 9876; + extraOpts = { + tokenPath = mkOption { + type = types.nullOr types.path; + apply = final: if final == null then null else toString final; + description = '' + The token from your Buildkite "Agents" page. + + A run-time path to the token file, which is supposed to be provisioned + outside of Nix store. + ''; + }; + interval = mkOption { + type = types.str; + default = "30s"; + example = "1min"; + description = '' + How often to update metrics. + ''; + }; + endpoint = mkOption { + type = types.str; + default = "https://agent.buildkite.com/v3"; + description = '' + The Buildkite Agent API endpoint. + ''; + }; + queues = mkOption { + type = with types; nullOr (listOf str); + default = null; + example = literalExpression ''[ "my-queue1" "my-queue2" ]''; + description = '' + Which specific queues to process. + ''; + }; + }; + serviceOpts = { + script = + let + queues = concatStringsSep " " (map (q: "-queue ${q}") cfg.queues); + in + '' + export BUILDKITE_AGENT_TOKEN="$(cat ${toString cfg.tokenPath})" + exec ${pkgs.buildkite-agent-metrics}/bin/buildkite-agent-metrics \ + -backend prometheus \ + -interval ${cfg.interval} \ + -endpoint ${cfg.endpoint} \ + ${optionalString (cfg.queues != null) queues} \ + -prometheus-addr "${cfg.listenAddress}:${toString cfg.port}" ${concatStringsSep " " cfg.extraFlags} + ''; + serviceConfig = { + DynamicUser = false; + RuntimeDirectory = "buildkite-agent-metrics"; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix new file mode 100644 index 000000000000..6f4c936fd409 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix @@ -0,0 +1,83 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.collectd; + inherit (lib) + mkOption + mkEnableOption + types + optionalString + concatStringsSep + escapeShellArg + ; +in +{ + port = 9103; + extraOpts = { + collectdBinary = { + enable = mkEnableOption "collectd binary protocol receiver"; + + authFile = mkOption { + default = null; + type = types.nullOr types.path; + description = "File mapping user names to pre-shared keys (passwords)."; + }; + + port = mkOption { + type = types.port; + default = 25826; + description = "Network address on which to accept collectd binary network packets."; + }; + + listenAddress = mkOption { + type = types.str; + default = "0.0.0.0"; + description = '' + Address to listen on for binary network packets. + ''; + }; + + securityLevel = mkOption { + type = types.enum ["None" "Sign" "Encrypt"]; + default = "None"; + description = '' + Minimum required security level for accepted packets. + ''; + }; + }; + + logFormat = mkOption { + type = types.enum [ "logfmt" "json" ]; + default = "logfmt"; + example = "json"; + description = '' + Set the log format. + ''; + }; + + logLevel = mkOption { + type = types.enum ["debug" "info" "warn" "error" "fatal"]; + default = "info"; + description = '' + Only log messages with the given severity or above. + ''; + }; + }; + serviceOpts = let + collectSettingsArgs = optionalString (cfg.collectdBinary.enable) '' + --collectd.listen-address ${cfg.collectdBinary.listenAddress}:${toString cfg.collectdBinary.port} \ + --collectd.security-level ${cfg.collectdBinary.securityLevel} \ + ''; + in { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-collectd-exporter}/bin/collectd_exporter \ + --log.format ${escapeShellArg cfg.logFormat} \ + --log.level ${cfg.logLevel} \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + ${collectSettingsArgs} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/dmarc.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/dmarc.nix new file mode 100644 index 000000000000..3674fab1e4f8 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/dmarc.nix @@ -0,0 +1,116 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.dmarc; + inherit (lib) mkOption types optionalString; + + json = builtins.toJSON { + inherit (cfg) folders port; + listen_addr = cfg.listenAddress; + storage_path = "$STATE_DIRECTORY"; + imap = (builtins.removeAttrs cfg.imap [ "passwordFile" ]) // { password = "$IMAP_PASSWORD"; use_ssl = true; }; + poll_interval_seconds = cfg.pollIntervalSeconds; + deduplication_max_seconds = cfg.deduplicationMaxSeconds; + logging = { + version = 1; + disable_existing_loggers = false; + }; + }; +in { + port = 9797; + extraOpts = { + imap = { + host = mkOption { + type = types.str; + default = "localhost"; + description = '' + Hostname of IMAP server to connect to. + ''; + }; + port = mkOption { + type = types.port; + default = 993; + description = '' + Port of the IMAP server to connect to. + ''; + }; + username = mkOption { + type = types.str; + example = "postmaster@example.org"; + description = '' + Login username for the IMAP connection. + ''; + }; + passwordFile = mkOption { + type = types.str; + example = "/run/secrets/dovecot_pw"; + description = '' + File containing the login password for the IMAP connection. + ''; + }; + }; + folders = { + inbox = mkOption { + type = types.str; + default = "INBOX"; + description = '' + IMAP mailbox that is checked for incoming DMARC aggregate reports + ''; + }; + done = mkOption { + type = types.str; + default = "Archive"; + description = '' + IMAP mailbox that successfully processed reports are moved to. + ''; + }; + error = mkOption { + type = types.str; + default = "Invalid"; + description = '' + IMAP mailbox that emails are moved to that could not be processed. + ''; + }; + }; + pollIntervalSeconds = mkOption { + type = types.ints.unsigned; + default = 60; + description = '' + How often to poll the IMAP server in seconds. + ''; + }; + deduplicationMaxSeconds = mkOption { + type = types.ints.unsigned; + default = 604800; + defaultText = "7 days (in seconds)"; + description = '' + How long individual report IDs will be remembered to avoid + counting double delivered reports twice. + ''; + }; + debug = mkOption { + type = types.bool; + default = false; + description = '' + Whether to declare enable `--debug`. + ''; + }; + }; + serviceOpts = { + path = with pkgs; [ envsubst coreutils ]; + serviceConfig = { + StateDirectory = "prometheus-dmarc-exporter"; + WorkingDirectory = "/var/lib/prometheus-dmarc-exporter"; + ExecStart = "${pkgs.writeShellScript "setup-cfg" '' + export IMAP_PASSWORD="$(<${cfg.imap.passwordFile})" + envsubst \ + -i ${pkgs.writeText "dmarc-exporter.json.template" json} \ + -o ''${STATE_DIRECTORY}/dmarc-exporter.json + + exec ${pkgs.dmarc-metrics-exporter}/bin/dmarc-metrics-exporter \ + --configuration /var/lib/prometheus-dmarc-exporter/dmarc-exporter.json \ + ${optionalString cfg.debug "--debug"} + ''}"; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/dnsmasq.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/dnsmasq.nix new file mode 100644 index 000000000000..ba438ea74a3b --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/dnsmasq.nix @@ -0,0 +1,42 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.dnsmasq; + inherit (lib) + mkOption + types + concatStringsSep + escapeShellArg + ; +in +{ + port = 9153; + extraOpts = { + dnsmasqListenAddress = mkOption { + type = types.str; + default = "localhost:53"; + description = '' + Address on which dnsmasq listens. + ''; + }; + leasesPath = mkOption { + type = types.path; + default = "/var/lib/misc/dnsmasq.leases"; + example = "/var/lib/dnsmasq/dnsmasq.leases"; + description = '' + Path to the `dnsmasq.leases` file. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-dnsmasq-exporter}/bin/dnsmasq_exporter \ + --listen ${cfg.listenAddress}:${toString cfg.port} \ + --dnsmasq ${cfg.dnsmasqListenAddress} \ + --leases_path ${escapeShellArg cfg.leasesPath} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/dnssec.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/dnssec.nix new file mode 100644 index 000000000000..dda1ad1988a6 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/dnssec.nix @@ -0,0 +1,90 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.prometheus.exporters.dnssec; + configFormat = pkgs.formats.toml { }; + configFile = configFormat.generate "dnssec-checks.toml" cfg.configuration; +in { + port = 9204; + extraOpts = { + configuration = lib.mkOption { + type = lib.types.nullOr lib.types.attrs; + default = null; + description = '' + dnssec exporter configuration as nix attribute set. + + See <https://github.com/chrj/prometheus-dnssec-exporter/blob/master/README.md> + for the description of the configuration file format. + ''; + example = lib.literalExpression '' + { + records = [ + { + zone = "ietf.org"; + record = "@"; + type = "SOA"; + } + { + zone = "verisigninc.com"; + record = "@"; + type = "SOA"; + } + ]; + } + ''; + }; + + listenAddress = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Listen address as host IP and port definition. + ''; + example = ":9204"; + }; + + resolvers = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + DNSSEC capable resolver to be used for the check. + ''; + example = [ "0.0.0.0:53" ]; + }; + + timeout = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + DNS request timeout duration. + ''; + example = "10s"; + }; + + extraFlags = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Extra commandline options when launching Prometheus. + ''; + }; + }; + + serviceOpts = { + serviceConfig = let + startScript = pkgs.writeShellScriptBin "prometheus-dnssec-exporter-start" + "${lib.concatStringsSep " " + ([ "${pkgs.prometheus-dnssec-exporter}/bin/prometheus-dnssec-exporter" ] + ++ lib.optionals (cfg.configuration != null) + [ "-config ${configFile}" ] + ++ lib.optionals (cfg.listenAddress != null) + [ "-listen-address ${lib.escapeShellArg cfg.listenAddress}" ] + ++ lib.optionals (cfg.resolvers != [ ]) [ + "-resolvers ${ + lib.escapeShellArg (lib.concatStringsSep "," cfg.resolvers) + }" + ] ++ lib.optionals (cfg.timeout != null) + [ "-timeout ${lib.escapeShellArg cfg.timeout}" ] ++ cfg.extraFlags)}"; + in { ExecStart = lib.getExe startScript; }; + }; +} + diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/domain.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/domain.nix new file mode 100644 index 000000000000..c271a040d288 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/domain.nix @@ -0,0 +1,18 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.domain; + inherit (lib) concatStringsSep; +in +{ + port = 9222; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-domain-exporter}/bin/domain_exporter \ + --bind ${cfg.listenAddress}:${toString cfg.port} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix new file mode 100644 index 000000000000..f11e91fd761a --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/dovecot.nix @@ -0,0 +1,96 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.dovecot; + inherit (lib) + mkOption + types + escapeShellArg + concatStringsSep + ; +in +{ + port = 9166; + extraOpts = { + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + socketPath = mkOption { + type = types.path; + default = "/var/run/dovecot/stats"; + example = "/var/run/dovecot2/old-stats"; + description = '' + Path under which the stats socket is placed. + The user/group under which the exporter runs, + should be able to access the socket in order + to scrape the metrics successfully. + + Please keep in mind that the stats module has changed in + [Dovecot 2.3+](https://wiki2.dovecot.org/Upgrading/2.3) which + is not [compatible with this exporter](https://github.com/kumina/dovecot_exporter/issues/8). + + The following extra config has to be passed to Dovecot to ensure that recent versions + work with this exporter: + ``` + { + services.prometheus.exporters.dovecot.enable = true; + services.prometheus.exporters.dovecot.socketPath = "/var/run/dovecot2/old-stats"; + services.dovecot2.mailPlugins.globally.enable = [ "old_stats" ]; + services.dovecot2.extraConfig = ''' + service old-stats { + unix_listener old-stats { + user = dovecot-exporter + group = dovecot-exporter + mode = 0660 + } + fifo_listener old-stats-mail { + mode = 0660 + user = dovecot + group = dovecot + } + fifo_listener old-stats-user { + mode = 0660 + user = dovecot + group = dovecot + } + } + plugin { + old_stats_refresh = 30 secs + old_stats_track_cmds = yes + } + '''; + } + ``` + ''; + }; + scopes = mkOption { + type = types.listOf types.str; + default = [ "user" ]; + example = [ "user" "global" ]; + description = '' + Stats scopes to query. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + DynamicUser = false; + ExecStart = '' + ${pkgs.prometheus-dovecot-exporter}/bin/dovecot_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path ${cfg.telemetryPath} \ + --dovecot.socket-path ${escapeShellArg cfg.socketPath} \ + --dovecot.scopes ${concatStringsSep "," cfg.scopes} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + RestrictAddressFamilies = [ + # Need AF_UNIX to collect data + "AF_UNIX" + ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/exportarr.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/exportarr.nix new file mode 100644 index 000000000000..45c2c697519c --- /dev/null +++ b/nixpkgs/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 = '' + The full URL to Sonarr, Radarr, or Lidarr. + ''; + }; + + apiKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + File containing the api-key. + ''; + }; + + package = lib.mkPackageOption pkgs "exportarr" { }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = '' + 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/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix new file mode 100644 index 000000000000..097ea3959478 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix @@ -0,0 +1,55 @@ +{ config +, lib +, pkgs +, options +, ... +}: + +let + inherit (lib) + escapeShellArgs + mkOption + optionals + types + ; + + cfg = config.services.prometheus.exporters.fastly; +in +{ + port = 9118; + extraOpts = with types; { + configFile = mkOption { + type = nullOr path; + default = null; + example = "./fastly-exporter-config.txt"; + description = '' + Path to a fastly-exporter configuration file. + Example one can be generated with `fastly-exporter --config-file-example`. + ''; + }; + + tokenPath = mkOption { + type = path; + description = '' + A run-time path to the token file, which is supposed to be provisioned + outside of Nix store. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + LoadCredential = "fastly-api-token:${cfg.tokenPath}"; + }; + script = let + call = escapeShellArgs ([ + "${pkgs.prometheus-fastly-exporter}/bin/fastly-exporter" + "-listen" "${cfg.listenAddress}:${toString cfg.port}" + ] ++ optionals (cfg.configFile != null) [ + "--config-file" cfg.configFile + ] ++ cfg.extraFlags); + in '' + export FASTLY_API_TOKEN="$(cat $CREDENTIALS_DIRECTORY/fastly-api-token)" + ${call} + ''; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/flow.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/flow.nix new file mode 100644 index 000000000000..7719215952a5 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/flow.nix @@ -0,0 +1,55 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.flow; + inherit (lib) + mkOption + types + literalExpression + concatStringsSep + optionalString + ; +in { + port = 9590; + extraOpts = { + brokers = mkOption { + type = types.listOf types.str; + example = literalExpression ''[ "kafka.example.org:19092" ]''; + description = "List of Kafka brokers to connect to."; + }; + + asn = mkOption { + type = types.ints.positive; + example = 65542; + description = "The ASN being monitored."; + }; + + partitions = mkOption { + type = types.listOf types.int; + default = []; + description = '' + The number of the partitions to consume, none means all. + ''; + }; + + topic = mkOption { + type = types.str; + example = "pmacct.acct"; + description = "The Kafka topic to consume from."; + }; + }; + + serviceOpts = { + serviceConfig = { + DynamicUser = true; + ExecStart = '' + ${pkgs.prometheus-flow-exporter}/bin/flow-exporter \ + -asn ${toString cfg.asn} \ + -topic ${cfg.topic} \ + -brokers ${concatStringsSep "," cfg.brokers} \ + ${optionalString (cfg.partitions != []) "-partitions ${concatStringsSep "," cfg.partitions}"} \ + -addr ${cfg.listenAddress}:${toString cfg.port} ${concatStringsSep " " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/fritz.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/fritz.nix new file mode 100644 index 000000000000..fcf7e8784f60 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/fritz.nix @@ -0,0 +1,97 @@ +{ config, lib, pkgs, utils, ... }: +let + inherit (lib) mkOption types; + cfg = config.services.prometheus.exporters.fritz; + yaml = pkgs.formats.yaml { }; + configFile = yaml.generate "fritz-exporter.yaml" cfg.settings; +in +{ + port = 9787; + + extraOpts = { + settings = mkOption { + description = "Configuration settings for fritz-exporter."; + type = types.submodule { + freeformType = yaml.type; + + options = { + # Pull existing port option into config file. + port = mkOption { + type = types.port; + default = cfg.port; + internal = true; + visible = false; + }; + # Pull existing listen address option into config file. + listen_address = mkOption { + type = types.str; + default = cfg.listenAddress; + internal = true; + visible = false; + }; + log_level = mkOption { + type = types.enum [ "DEBUG" "INFO" "WARNING" "ERROR" "CRITICAL" ]; + default = "INFO"; + description = '' + Log level to use for the exporter. + ''; + }; + devices = mkOption { + default = []; + description = "Fritz!-devices to monitor using the exporter."; + type = with types; listOf (submodule { + freeformType = yaml.type; + + options = { + name = mkOption { + type = types.str; + default = ""; + description = '' + Name to use for the device. + ''; + }; + hostname = mkOption { + type = types.str; + default = "fritz.box"; + description = '' + Hostname under which the target device is reachable. + ''; + }; + username = mkOption { + type = types.str; + description = '' + Username to authenticate with the target device. + ''; + }; + password_file = mkOption { + type = types.path; + description = '' + Path to a file which contains the password to authenticate with the target device. + Needs to be readable by the user the exporter runs under. + ''; + }; + host_info = mkOption { + type = types.bool; + description = '' + Enable extended host info for this device. *Warning*: This will heavily increase scrape time. + ''; + default = false; + }; + }; + }); + }; + }; + }; + }; + }; + + serviceOpts = { + serviceConfig = { + ExecStart = utils.escapeSystemdExecArgs ([ + (lib.getExe pkgs.fritz-exporter) + "--config" configFile + ] ++ cfg.extraFlags); + DynamicUser = false; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/fritzbox.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/fritzbox.nix new file mode 100644 index 000000000000..29470147c13f --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/fritzbox.nix @@ -0,0 +1,37 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.fritzbox; + inherit (lib) mkOption types concatStringsSep; +in +{ + port = 9133; + extraOpts = { + gatewayAddress = mkOption { + type = types.str; + default = "fritz.box"; + description = '' + The hostname or IP of the FRITZ!Box. + ''; + }; + + gatewayPort = mkOption { + type = types.int; + default = 49000; + description = '' + The port of the FRITZ!Box UPnP service. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-fritzbox-exporter}/bin/exporter \ + -listen-address ${cfg.listenAddress}:${toString cfg.port} \ + -gateway-address ${cfg.gatewayAddress} \ + -gateway-port ${toString cfg.gatewayPort} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/graphite.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/graphite.nix new file mode 100644 index 000000000000..523a720425c0 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/graphite.nix @@ -0,0 +1,41 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.graphite; + format = pkgs.formats.yaml { }; +in +{ + port = 9108; + extraOpts = { + graphitePort = lib.mkOption { + type = lib.types.port; + default = 9109; + description = '' + Port to use for the graphite server. + ''; + }; + mappingSettings = lib.mkOption { + type = lib.types.submodule { + freeformType = format.type; + options = { }; + }; + default = { }; + description = '' + Mapping configuration for the exporter, see + <https://github.com/prometheus/graphite_exporter#yaml-config> for + available options. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-graphite-exporter}/bin/graphite_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --graphite.listen-address ${cfg.listenAddress}:${toString cfg.graphitePort} \ + --graphite.mapping-config ${format.generate "mapping.yml" cfg.mappingSettings} \ + ${lib.concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/idrac.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/idrac.nix new file mode 100644 index 000000000000..54696187feb1 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/idrac.nix @@ -0,0 +1,69 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.idrac; + inherit (lib) mkOption types; + + configFile = if cfg.configurationPath != null + then cfg.configurationPath + else pkgs.writeText "idrac.yml" (builtins.toJSON cfg.configuration); +in +{ + port = 9348; + extraOpts = { + configurationPath = mkOption { + type = with types; nullOr path; + default = null; + example = "/etc/prometheus-idrac-exporter/idrac.yml"; + description = '' + Path to the service's config file. This path can either be a computed path in /nix/store or a path in the local filesystem. + + The config file should NOT be stored in /nix/store as it will contain passwords and/or keys in plain text. + + Mutually exclusive with `configuration` option. + + Configuration reference: https://github.com/mrlhansen/idrac_exporter/#configuration + ''; + }; + configuration = mkOption { + type = types.nullOr types.attrs; + description = '' + Configuration for iDRAC exporter, as a nix attribute set. + + Configuration reference: https://github.com/mrlhansen/idrac_exporter/#configuration + + Mutually exclusive with `configurationPath` option. + ''; + default = null; + example = { + timeout = 10; + retries = 1; + hosts = { + default = { + username = "username"; + password = "password"; + }; + }; + metrics = { + system = true; + sensors = true; + power = true; + sel = true; + storage = true; + memory = true; + }; + }; + }; + }; + + serviceOpts = { + serviceConfig = { + LoadCredential = "configFile:${configFile}"; + ExecStart = "${pkgs.prometheus-idrac-exporter}/bin/idrac_exporter -config %d/configFile"; + Environment = [ + "IDRAC_EXPORTER_LISTEN_ADDRESS=${cfg.listenAddress}" + "IDRAC_EXPORTER_LISTEN_PORT=${toString cfg.port}" + ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/imap-mailstat.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/imap-mailstat.nix new file mode 100644 index 000000000000..92d02a3f1463 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/imap-mailstat.nix @@ -0,0 +1,78 @@ +{ config, lib, pkgs, options, ... }: + +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}" + ) + ); + inherit (lib) + mkOption + types + concatStrings + concatStringsSep + attrValues + mapAttrs + optionalString + ; + 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 = 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 = '' + Enable metric with timestamp of oldest unseen mail + ''; + }; + accounts = mkOption { + type = types.attrsOf (types.submodule accountOptions); + default = {}; + description = '' + Accounts to monitor + ''; + }; + configurationFile = mkOption { + type = types.path; + example = "/path/to/config-file"; + description = '' + 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/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/influxdb.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/influxdb.nix new file mode 100644 index 000000000000..3a5680439d4c --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/influxdb.nix @@ -0,0 +1,33 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.influxdb; + inherit (lib) mkOption types concatStringsSep; +in +{ + port = 9122; + extraOpts = { + sampleExpiry = mkOption { + type = types.str; + default = "5m"; + example = "10m"; + description = "How long a sample is valid for"; + }; + udpBindAddress = mkOption { + type = types.str; + default = ":9122"; + example = "192.0.2.1:9122"; + description = "Address on which to listen for udp packets"; + }; + }; + serviceOpts = { + serviceConfig = { + RuntimeDirectory = "prometheus-influxdb-exporter"; + ExecStart = '' + ${pkgs.prometheus-influxdb-exporter}/bin/influxdb_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --influxdb.sample-expiry ${cfg.sampleExpiry} ${concatStringsSep " " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/ipmi.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/ipmi.nix new file mode 100644 index 000000000000..51db6059081f --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/ipmi.nix @@ -0,0 +1,47 @@ +{ config, lib, pkgs, options, ... }: + +let + logPrefix = "services.prometheus.exporter.ipmi"; + cfg = config.services.prometheus.exporters.ipmi; + inherit (lib) + mkOption + types + concatStringsSep + optionals + escapeShellArg + ; +in { + port = 9290; + + extraOpts = { + configFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Path to configuration file. + ''; + }; + + webConfigFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Path to configuration file that can enable TLS or authentication. + ''; + }; + }; + + serviceOpts.serviceConfig = { + ExecStart = with cfg; concatStringsSep " " ([ + "${pkgs.prometheus-ipmi-exporter}/bin/ipmi_exporter" + "--web.listen-address ${listenAddress}:${toString port}" + ] ++ optionals (cfg.webConfigFile != null) [ + "--web.config.file ${escapeShellArg cfg.webConfigFile}" + ] ++ optionals (cfg.configFile != null) [ + "--config.file ${escapeShellArg cfg.configFile}" + ] ++ extraFlags); + + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix new file mode 100644 index 000000000000..6a6c003c1977 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/jitsi.nix @@ -0,0 +1,44 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.jitsi; + inherit (lib) + mkOption + types + escapeShellArg + concatStringsSep + ; +in +{ + port = 9700; + extraOpts = { + url = mkOption { + type = types.str; + default = "http://localhost:8080/colibri/stats"; + description = '' + Jitsi Videobridge metrics URL to monitor. + This is usually /colibri/stats on port 8080 of the jitsi videobridge host. + ''; + }; + interval = mkOption { + type = types.str; + default = "30s"; + example = "1min"; + description = '' + How often to scrape new data + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-jitsi-exporter}/bin/jitsiexporter \ + -url ${escapeShellArg cfg.url} \ + -host ${cfg.listenAddress} \ + -port ${toString cfg.port} \ + -interval ${toString cfg.interval} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/json.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/json.nix new file mode 100644 index 000000000000..1c8db0ea3e0b --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/json.nix @@ -0,0 +1,48 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.json; + inherit (lib) + mkOption + types + escapeShellArg + concatStringsSep + mkRemovedOptionModule + ; +in +{ + port = 7979; + extraOpts = { + configFile = mkOption { + type = types.path; + description = '' + Path to configuration file. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-json-exporter}/bin/json_exporter \ + --config.file ${escapeShellArg cfg.configFile} \ + --web.listen-address="${cfg.listenAddress}:${toString cfg.port}" \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; + imports = [ + (mkRemovedOptionModule [ "url" ] '' + This option was removed. The URL of the endpoint serving JSON + must now be provided to the exporter by prometheus via the url + parameter `target'. + + In prometheus a scrape URL would look like this: + + http://some.json-exporter.host:7979/probe?target=https://example.com/some/json/endpoint + + For more information, take a look at the official documentation + (https://github.com/prometheus-community/json_exporter) of the json_exporter. + '') + ({ options.warnings = options.warnings; options.assertions = options.assertions; }) + ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/junos-czerwonk.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/junos-czerwonk.nix new file mode 100644 index 000000000000..3519cce6e821 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/junos-czerwonk.nix @@ -0,0 +1,77 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.junos-czerwonk; + inherit (lib) + mkOption + types + escapeShellArg + mkIf + concatStringsSep + ; + + configFile = if cfg.configuration != null then configurationFile else (escapeShellArg cfg.configurationFile); + + configurationFile = pkgs.writeText "prometheus-junos-czerwonk-exporter.conf" (builtins.toJSON (cfg.configuration)); +in +{ + port = 9326; + extraOpts = { + environmentFile = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + File containing env-vars to be substituted into the exporter's config. + ''; + }; + configurationFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Specify the JunOS exporter configuration file to use. + ''; + }; + configuration = mkOption { + type = types.nullOr types.attrs; + default = null; + description = '' + JunOS exporter configuration as nix attribute set. Mutually exclusive with the `configurationFile` option. + ''; + example = { + devices = [ + { + host = "router1"; + key_file = "/path/to/key"; + } + ]; + }; + }; + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + DynamicUser = false; + EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; + RuntimeDirectory = "prometheus-junos-czerwonk-exporter"; + ExecStartPre = [ + "${pkgs.writeShellScript "subst-secrets-junos-czerwonk-exporter" '' + umask 0077 + ${pkgs.envsubst}/bin/envsubst -i ${configFile} -o ''${RUNTIME_DIRECTORY}/junos-exporter.json + ''}" + ]; + ExecStart = '' + ${pkgs.prometheus-junos-czerwonk-exporter}/bin/junos_exporter \ + -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + -web.telemetry-path ${cfg.telemetryPath} \ + -config.file ''${RUNTIME_DIRECTORY}/junos-exporter.json \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/kea.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/kea.nix new file mode 100644 index 000000000000..d0f2eb6b8a3c --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/kea.nix @@ -0,0 +1,57 @@ +{ config +, lib +, pkgs +, utils +, ... +}: + +let + cfg = config.services.prometheus.exporters.kea; + inherit (lib) + mkOption + types + mkRenamedOptionModule + literalExpression + ; +in { + imports = [ + (mkRenamedOptionModule [ "controlSocketPaths" ] [ "targets" ]) + ]; + port = 9547; + extraOpts = { + targets = mkOption { + type = types.listOf types.str; + example = literalExpression '' + [ + "/run/kea/kea-dhcp4.socket" + "/run/kea/kea-dhcp6.socket" + "http://127.0.0.1:8547" + ] + ''; + description = '' + Paths or URLs to the Kea control socket. + ''; + }; + }; + serviceOpts = { + after = [ + "kea-dhcp4-server.service" + "kea-dhcp6-server.service" + ]; + serviceConfig = { + User = "kea"; + DynamicUser = true; + ExecStart = utils.escapeSystemdExecArgs ([ + (lib.getExe pkgs.prometheus-kea-exporter) + "--address" cfg.listenAddress + "--port" cfg.port + ] ++ cfg.extraFlags ++ cfg.targets); + RuntimeDirectory = "kea"; + RuntimeDirectoryPreserve = true; + RestrictAddressFamilies = [ + # Need AF_UNIX to collect data + "AF_UNIX" + ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/keylight.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/keylight.nix new file mode 100644 index 000000000000..44169cce6745 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/keylight.nix @@ -0,0 +1,18 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.keylight; + inherit (lib) concatStringsSep; +in +{ + port = 9288; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-keylight-exporter}/bin/keylight_exporter \ + -metrics.addr ${cfg.listenAddress}:${toString cfg.port} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/knot.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/knot.nix new file mode 100644 index 000000000000..ed902fc27c15 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/knot.nix @@ -0,0 +1,62 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.knot; + inherit (lib) + mkOption + types + literalExpression + concatStringsSep + ; +in { + port = 9433; + extraOpts = { + knotLibraryPath = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression ''"''${pkgs.knot-dns.out}/lib/libknot.so"''; + description = '' + Path to the library of `knot-dns`. + ''; + }; + + knotSocketPath = mkOption { + type = types.str; + default = "/run/knot/knot.sock"; + description = '' + Socket path of {manpage}`knotd(8)`. + ''; + }; + + knotSocketTimeout = mkOption { + type = types.ints.positive; + default = 2000; + description = '' + Timeout in seconds. + ''; + }; + }; + serviceOpts = { + path = with pkgs; [ + procps + ]; + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-knot-exporter}/bin/knot-exporter \ + --web-listen-addr ${cfg.listenAddress} \ + --web-listen-port ${toString cfg.port} \ + --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" + ]; + RestrictAddressFamilies = [ + # Need AF_UNIX to collect data + "AF_UNIX" + ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/lnd.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/lnd.nix new file mode 100644 index 000000000000..edface276f54 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/lnd.nix @@ -0,0 +1,45 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.lnd; + inherit (lib) mkOption types concatStringsSep; +in +{ + port = 9092; + extraOpts = { + lndHost = mkOption { + type = types.str; + default = "localhost:10009"; + description = '' + lnd instance gRPC address:port. + ''; + }; + + lndTlsPath = mkOption { + type = types.path; + description = '' + Path to lnd TLS certificate. + ''; + }; + + lndMacaroonDir = mkOption { + type = types.path; + description = '' + Path to lnd macaroons. + ''; + }; + }; + serviceOpts.serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-lnd-exporter}/bin/lndmon \ + --prometheus.listenaddr=${cfg.listenAddress}:${toString cfg.port} \ + --prometheus.logdir=/var/log/prometheus-lnd-exporter \ + --lnd.host=${cfg.lndHost} \ + --lnd.tlspath=${cfg.lndTlsPath} \ + --lnd.macaroondir=${cfg.lndMacaroonDir} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + LogsDirectory = "prometheus-lnd-exporter"; + ReadOnlyPaths = [ cfg.lndTlsPath cfg.lndMacaroonDir ]; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/mail.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/mail.nix new file mode 100644 index 000000000000..f6dd6f7eb994 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/mail.nix @@ -0,0 +1,200 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.mail; + inherit (lib) + mkOption + types + mapAttrs' + nameValuePair + toLower + filterAttrs + escapeShellArg + literalExpression + mkIf + concatStringsSep + ; + + configFile = if cfg.configuration != null then configurationFile else (escapeShellArg cfg.configFile); + + configurationFile = pkgs.writeText "prometheus-mail-exporter.conf" (builtins.toJSON ( + # removes the _module attribute, null values and converts attrNames to lowercase + mapAttrs' (name: value: + if name == "servers" + then nameValuePair (toLower name) + ((map (srv: (mapAttrs' (n: v: nameValuePair (toLower n) v) + (filterAttrs (n: v: !(n == "_module" || v == null)) srv) + ))) value) + else nameValuePair (toLower name) value + ) (filterAttrs (n: _: !(n == "_module")) cfg.configuration) + )); + + serverOptions.options = { + name = mkOption { + type = types.str; + description = '' + Value for label 'configname' which will be added to all metrics. + ''; + }; + server = mkOption { + type = types.str; + description = '' + Hostname of the server that should be probed. + ''; + }; + port = mkOption { + type = types.port; + example = 587; + description = '' + Port to use for SMTP. + ''; + }; + from = mkOption { + type = types.str; + example = "exporteruser@domain.tld"; + description = '' + Content of 'From' Header for probing mails. + ''; + }; + to = mkOption { + type = types.str; + example = "exporteruser@domain.tld"; + description = '' + Content of 'To' Header for probing mails. + ''; + }; + detectionDir = mkOption { + type = types.path; + example = "/var/spool/mail/exporteruser/new"; + description = '' + Directory in which new mails for the exporter user are placed. + Note that this needs to exist when the exporter starts. + ''; + }; + login = mkOption { + type = types.nullOr types.str; + default = null; + example = "exporteruser@domain.tld"; + description = '' + Username to use for SMTP authentication. + ''; + }; + passphrase = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Password to use for SMTP authentication. + ''; + }; + }; + + exporterOptions.options = { + monitoringInterval = mkOption { + type = types.str; + example = "10s"; + description = '' + Time interval between two probe attempts. + ''; + }; + mailCheckTimeout = mkOption { + type = types.str; + description = '' + Timeout until mails are considered "didn't make it". + ''; + }; + disableFileDeletion = mkOption { + type = types.bool; + default = false; + description = '' + Disables the exporter's function to delete probing mails. + ''; + }; + servers = mkOption { + type = types.listOf (types.submodule serverOptions); + default = []; + example = literalExpression '' + [ { + name = "testserver"; + server = "smtp.domain.tld"; + port = 587; + from = "exporteruser@domain.tld"; + to = "exporteruser@domain.tld"; + detectionDir = "/path/to/Maildir/new"; + } ] + ''; + description = '' + List of servers that should be probed. + + *Note:* if your mailserver has {manpage}`rspamd(8)` configured, + it can happen that emails from this exporter are marked as spam. + + It's possible to work around the issue with a config like this: + ``` + { + services.rspamd.locals."multimap.conf".text = ''' + ALLOWLIST_PROMETHEUS { + filter = "email:domain:tld"; + type = "from"; + map = "''${pkgs.writeText "allowmap" "domain.tld"}"; + score = -100.0; + } + '''; + } + ``` + ''; + }; + }; +in +{ + port = 9225; + extraOpts = { + environmentFile = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + File containing env-vars to be substituted into the exporter's config. + ''; + }; + configFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Specify the mailexporter configuration file to use. + ''; + }; + configuration = mkOption { + type = types.nullOr (types.submodule exporterOptions); + default = null; + description = '' + Specify the mailexporter configuration file to use. + ''; + }; + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + DynamicUser = false; + EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; + RuntimeDirectory = "prometheus-mail-exporter"; + ExecStartPre = [ + "${pkgs.writeShellScript "subst-secrets-mail-exporter" '' + umask 0077 + ${pkgs.envsubst}/bin/envsubst -i ${configFile} -o ''${RUNTIME_DIRECTORY}/mail-exporter.json + ''}" + ]; + ExecStart = '' + ${pkgs.prometheus-mail-exporter}/bin/mailexporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path ${cfg.telemetryPath} \ + --config.file ''${RUNTIME_DIRECTORY}/mail-exporter.json \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/mikrotik.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/mikrotik.nix new file mode 100644 index 000000000000..cd438f13edd2 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/mikrotik.nix @@ -0,0 +1,71 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.mikrotik; + inherit (lib) + mkOption + types + literalExpression + concatStringsSep + escapeShellArg + ; +in +{ + port = 9436; + extraOpts = { + configFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Path to a mikrotik exporter configuration file. Mutually exclusive with + {option}`configuration` option. + ''; + example = literalExpression "./mikrotik.yml"; + }; + + configuration = mkOption { + type = types.nullOr types.attrs; + default = null; + description = '' + Mikrotik exporter configuration as nix attribute set. Mutually exclusive with + {option}`configFile` option. + + See <https://github.com/nshttpd/mikrotik-exporter/blob/master/README.md> + for the description of the configuration file format. + ''; + example = literalExpression '' + { + devices = [ + { + name = "my_router"; + address = "10.10.0.1"; + user = "prometheus"; + password = "changeme"; + } + ]; + features = { + bgp = true; + dhcp = true; + routes = true; + optics = true; + }; + } + ''; + }; + }; + serviceOpts = let + configFile = if cfg.configFile != null + then cfg.configFile + else "${pkgs.writeText "mikrotik-exporter.yml" (builtins.toJSON cfg.configuration)}"; + in { + serviceConfig = { + # -port is misleading name, it actually accepts address too + ExecStart = '' + ${pkgs.prometheus-mikrotik-exporter}/bin/mikrotik-exporter \ + -config-file=${escapeShellArg configFile} \ + -port=${cfg.listenAddress}:${toString cfg.port} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/minio.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/minio.nix new file mode 100644 index 000000000000..8faff5908b8a --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/minio.nix @@ -0,0 +1,69 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.minio; + inherit (lib) + mkOption + types + optionalString + concatStringsSep + escapeShellArg + ; +in +{ + port = 9290; + extraOpts = { + minioAddress = mkOption { + type = types.str; + example = "https://10.0.0.1:9000"; + description = '' + The URL of the minio server. + Use HTTPS if Minio accepts secure connections only. + By default this connects to the local minio server if enabled. + ''; + }; + + minioAccessKey = mkOption { + type = types.str; + example = "yourMinioAccessKey"; + description = '' + The value of the Minio access key. + It is required in order to connect to the server. + By default this uses the one from the local minio server if enabled + and `config.services.minio.accessKey`. + ''; + }; + + minioAccessSecret = mkOption { + type = types.str; + description = '' + The value of the Minio access secret. + It is required in order to connect to the server. + By default this uses the one from the local minio server if enabled + and `config.services.minio.secretKey`. + ''; + }; + + minioBucketStats = mkOption { + type = types.bool; + default = false; + description = '' + Collect statistics about the buckets and files in buckets. + It requires more computation, use it carefully in case of large buckets.. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-minio-exporter}/bin/minio-exporter \ + -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + -minio.server ${cfg.minioAddress} \ + -minio.access-key ${escapeShellArg cfg.minioAccessKey} \ + -minio.access-secret ${escapeShellArg cfg.minioAccessSecret} \ + ${optionalString cfg.minioBucketStats "-minio.bucket-stats"} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/modemmanager.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/modemmanager.nix new file mode 100644 index 000000000000..37ff49b27000 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/modemmanager.nix @@ -0,0 +1,36 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.modemmanager; + inherit (lib) mkOption types concatStringsSep; +in +{ + port = 9539; + extraOpts = { + refreshRate = mkOption { + type = types.str; + default = "5s"; + description = '' + How frequently ModemManager will refresh the extended signal quality + information for each modem. The duration should be specified in seconds + ("5s"), minutes ("1m"), or hours ("1h"). + ''; + }; + }; + serviceOpts = { + serviceConfig = { + # Required in order to authenticate with ModemManager via D-Bus. + SupplementaryGroups = "networkmanager"; + ExecStart = '' + ${pkgs.prometheus-modemmanager-exporter}/bin/modemmanager_exporter \ + -addr ${cfg.listenAddress}:${toString cfg.port} \ + -rate ${cfg.refreshRate} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + RestrictAddressFamilies = [ + # Need AF_UNIX to collect data + "AF_UNIX" + ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/mongodb.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/mongodb.nix new file mode 100644 index 000000000000..288434e93abb --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/mongodb.nix @@ -0,0 +1,76 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.mongodb; + inherit (lib) + mkOption + types + optionalString + getExe + length + concatStringsSep + concatMapStringsSep + escapeShellArgs + ; +in +{ + port = 9216; + extraOpts = { + uri = mkOption { + type = types.str; + default = "mongodb://localhost:27017/test"; + example = "mongodb://localhost:27017/test"; + description = "MongoDB URI to connect to."; + }; + collStats = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "db1.coll1" "db2" ]; + description = '' + List of comma separared databases.collections to get $collStats + ''; + }; + indexStats = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "db1.coll1" "db2" ]; + description = '' + List of comma separared databases.collections to get $indexStats + ''; + }; + collector = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ "diagnosticdata" "replicasetstatus" "dbstats" "topmetrics" "currentopmetrics" "indexstats" "dbstats" "profile" ]; + description = "Enabled collectors"; + }; + collectAll = mkOption { + type = types.bool; + default = false; + description = '' + Enable all collectors. Same as specifying all --collector.<name> + ''; + }; + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + example = "/metrics"; + description = "Metrics expose path"; + }; + }; + serviceOpts = { + serviceConfig = { + RuntimeDirectory = "prometheus-mongodb-exporter"; + ExecStart = '' + ${getExe pkgs.prometheus-mongodb-exporter} \ + --mongodb.uri="${cfg.uri}" \ + ${if cfg.collectAll then "--collect-all" else concatMapStringsSep " " (x: "--collect.${x}") cfg.collector} \ + ${optionalString (length cfg.collStats > 0) "--mongodb.collstats-colls=${concatStringsSep "," cfg.collStats}"} \ + ${optionalString (length cfg.indexStats > 0) "--mongodb.indexstats-colls=${concatStringsSep "," cfg.indexStats}"} \ + --web.listen-address="${cfg.listenAddress}:${toString cfg.port}" \ + --web.telemetry-path="${cfg.telemetryPath}" \ + ${escapeShellArgs cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/mysqld.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/mysqld.nix new file mode 100644 index 000000000000..5b2c2274f053 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/mysqld.nix @@ -0,0 +1,60 @@ +{ config, lib, pkgs, options, ... }: +let + cfg = config.services.prometheus.exporters.mysqld; + inherit (lib) types mkOption mkIf mkForce cli concatStringsSep optionalString escapeShellArgs; +in { + port = 9104; + extraOpts = { + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + + runAsLocalSuperUser = mkOption { + type = types.bool; + default = false; + description = '' + Whether to run the exporter as {option}`services.mysql.user`. + ''; + }; + + configFile = mkOption { + type = types.path; + example = "/var/lib/prometheus-mysqld-exporter.cnf"; + description = '' + Path to the services config file. + + See <https://github.com/prometheus/mysqld_exporter#running> for more information about + the available options. + + ::: {.warn} + Please do not store this file in the nix store if you choose to include any credentials here, + as it would be world-readable. + ::: + ''; + }; + }; + + serviceOpts = { + serviceConfig = { + DynamicUser = !cfg.runAsLocalSuperUser; + User = mkIf cfg.runAsLocalSuperUser (mkForce config.services.mysql.user); + LoadCredential = mkIf (cfg.configFile != null) (mkForce ("config:" + cfg.configFile)); + ExecStart = concatStringsSep " " [ + "${pkgs.prometheus-mysqld-exporter}/bin/mysqld_exporter" + "--web.listen-address=${cfg.listenAddress}:${toString cfg.port}" + "--web.telemetry-path=${cfg.telemetryPath}" + (optionalString (cfg.configFile != null) ''--config.my-cnf=''${CREDENTIALS_DIRECTORY}/config'') + (escapeShellArgs cfg.extraFlags) + ]; + RestrictAddressFamilies = [ + # The exporter can be configured to talk to a local mysql server via a unix socket. + "AF_UNIX" + ]; + }; + }; +} + diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nats.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nats.nix new file mode 100644 index 000000000000..224ce474d537 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nats.nix @@ -0,0 +1,31 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.nats; + inherit (lib) mkOption types concatStringsSep; +in +{ + port = 7777; + + extraOpts = { + url = mkOption { + type = types.str; + default = "http://127.0.0.1:8222"; + description = '' + NATS monitor endpoint to query. + ''; + }; + }; + + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-nats-exporter}/bin/prometheus-nats-exporter \ + -addr ${cfg.listenAddress} \ + -port ${toString cfg.port} \ + ${concatStringsSep " \\\n " cfg.extraFlags} \ + ${cfg.url} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix new file mode 100644 index 000000000000..d221bac8421a --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nextcloud.nix @@ -0,0 +1,76 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.nextcloud; + inherit (lib) + mkOption + types + escapeShellArg + concatStringsSep + ; +in +{ + port = 9205; + extraOpts = { + url = mkOption { + type = types.str; + example = "https://domain.tld"; + description = '' + URL to the Nextcloud serverinfo page. + Adding the path to the serverinfo API is optional, it defaults + to `/ocs/v2.php/apps/serverinfo/api/v1/info`. + ''; + }; + username = mkOption { + type = types.str; + default = "nextcloud-exporter"; + description = '' + Username for connecting to Nextcloud. + Note that this account needs to have admin privileges in Nextcloud. + Unused when using token authentication. + ''; + }; + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/path/to/password-file"; + description = '' + File containing the password for connecting to Nextcloud. + Make sure that this file is readable by the exporter user. + ''; + }; + tokenFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/path/to/token-file"; + description = '' + File containing the token for connecting to Nextcloud. + Make sure that this file is readable by the exporter user. + ''; + }; + timeout = mkOption { + type = types.str; + default = "5s"; + description = '' + Timeout for getting server info document. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + DynamicUser = false; + ExecStart = '' + ${pkgs.prometheus-nextcloud-exporter}/bin/nextcloud-exporter \ + --addr ${cfg.listenAddress}:${toString cfg.port} \ + --timeout ${cfg.timeout} \ + --server ${cfg.url} \ + ${if cfg.passwordFile != null then '' + --username ${cfg.username} \ + --password ${escapeShellArg "@${cfg.passwordFile}"} \ + '' else '' + --auth-token ${escapeShellArg "@${cfg.tokenFile}"} \ + ''} \ + ${concatStringsSep " \\\n " cfg.extraFlags}''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix new file mode 100644 index 000000000000..091ad2291d2a --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nginx.nix @@ -0,0 +1,75 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.nginx; + inherit (lib) + mkOption + types + mkMerge + mkRemovedOptionModule + mkRenamedOptionModule + mkIf + concatStringsSep + ; +in +{ + port = 9113; + extraOpts = { + scrapeUri = mkOption { + type = types.str; + default = "http://localhost/nginx_status"; + description = '' + Address to access the nginx status page. + Can be enabled with services.nginx.statusPage = true. + ''; + }; + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + sslVerify = mkOption { + type = types.bool; + default = true; + description = '' + Whether to perform certificate verification for https. + ''; + }; + constLabels = mkOption { + type = types.listOf types.str; + default = []; + example = [ + "label1=value1" + "label2=value2" + ]; + description = '' + A list of constant labels that will be used in every metric. + ''; + }; + }; + serviceOpts = mkMerge ([{ + environment.CONST_LABELS = concatStringsSep "," cfg.constLabels; + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-nginx-exporter}/bin/nginx-prometheus-exporter \ + --nginx.scrape-uri='${cfg.scrapeUri}' \ + --${lib.optionalString (!cfg.sslVerify) "no-"}nginx.ssl-verify \ + --web.listen-address=${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path=${cfg.telemetryPath} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }] ++ [(mkIf config.services.nginx.enable { + after = [ "nginx.service" ]; + requires = [ "nginx.service" ]; + })]); + imports = [ + (mkRenamedOptionModule [ "telemetryEndpoint" ] [ "telemetryPath" ]) + (mkRemovedOptionModule [ "insecure" ] '' + This option was replaced by 'prometheus.exporters.nginx.sslVerify'. + '') + ({ options.warnings = options.warnings; options.assertions = options.assertions; }) + ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix new file mode 100644 index 000000000000..2b4fd12895a3 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nginxlog.nix @@ -0,0 +1,50 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.nginxlog; + inherit (lib) mkOption types; +in { + port = 9117; + extraOpts = { + settings = mkOption { + type = types.attrs; + default = {}; + description = '' + All settings of nginxlog expressed as an Nix attrset. + + Check the official documentation for the corresponding YAML + settings that can all be used here: https://github.com/martin-helmich/prometheus-nginxlog-exporter + + The `listen` object is already generated by `port`, `listenAddress` and `metricsEndpoint` and + will be merged with the value of `settings` before writing it as JSON. + ''; + }; + + metricsEndpoint = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + }; + + serviceOpts = let + listenConfig = { + listen = { + port = cfg.port; + address = cfg.listenAddress; + metrics_endpoint = cfg.metricsEndpoint; + }; + }; + completeConfig = pkgs.writeText "nginxlog-exporter.yaml" (builtins.toJSON (lib.recursiveUpdate listenConfig cfg.settings)); + in { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-nginxlog-exporter}/bin/prometheus-nginxlog-exporter -config-file ${completeConfig} + ''; + Restart="always"; + ProtectSystem="full"; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/node.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/node.nix new file mode 100644 index 000000000000..9d6b51ad140d --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/node.nix @@ -0,0 +1,59 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.node; + inherit (lib) + mkOption + types + concatStringsSep + concatMapStringsSep + any + optionals + ; + collectorIsEnabled = final: any (collector: (final == collector)) cfg.enabledCollectors; + collectorIsDisabled = final: any (collector: (final == collector)) cfg.disabledCollectors; +in +{ + port = 9100; + extraOpts = { + enabledCollectors = mkOption { + type = types.listOf types.str; + default = []; + example = [ "systemd" ]; + description = '' + Collectors to enable. The collectors listed here are enabled in addition to the default ones. + ''; + }; + disabledCollectors = mkOption { + type = types.listOf types.str; + default = []; + example = [ "timex" ]; + description = '' + Collectors to disable which are enabled by default. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + DynamicUser = false; + RuntimeDirectory = "prometheus-node-exporter"; + ExecStart = '' + ${pkgs.prometheus-node-exporter}/bin/node_exporter \ + ${concatMapStringsSep " " (x: "--collector." + x) cfg.enabledCollectors} \ + ${concatMapStringsSep " " (x: "--no-collector." + x) cfg.disabledCollectors} \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} ${concatStringsSep " " cfg.extraFlags} + ''; + RestrictAddressFamilies = optionals (collectorIsEnabled "logind" || collectorIsEnabled "systemd") [ + # needs access to dbus via unix sockets (logind/systemd) + "AF_UNIX" + ] ++ optionals (collectorIsEnabled "network_route" || collectorIsEnabled "wifi" || ! collectorIsDisabled "netdev") [ + # needs netlink sockets for wireless collector + "AF_NETLINK" + ]; + # The timex collector needs to access clock APIs + ProtectClock = collectorIsDisabled "timex"; + # Allow space monitoring under /home + ProtectHome = true; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nut.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nut.nix new file mode 100644 index 000000000000..157bdadddfc9 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/nut.nix @@ -0,0 +1,67 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.nut; + inherit (lib) + mkOption + types + optionalString + concatStringsSep + ; +in +{ + port = 9199; + extraOpts = { + nutServer = mkOption { + type = types.str; + default = "127.0.0.1"; + description = '' + Hostname or address of the NUT server + ''; + }; + nutUser = mkOption { + type = types.str; + default = ""; + example = "nut"; + description = '' + The user to log in into NUT server. If set, passwordPath should + also be set. + + Default NUT configs usually permit reading variables without + authentication. + ''; + }; + passwordPath = mkOption { + type = types.nullOr types.path; + default = null; + apply = final: if final == null then null else toString final; + description = '' + A run-time path to the nutUser password file, which should be + provisioned outside of Nix store. + ''; + }; + nutVariables = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + List of NUT variable names to monitor. + + If no variables are set, all numeric variables will be exported automatically. + See the [upstream docs](https://github.com/DRuggeri/nut_exporter?tab=readme-ov-file#variables-and-information) + for more information. + ''; + }; + }; + serviceOpts = { + script = '' + ${optionalString (cfg.passwordPath != null) + "export NUT_EXPORTER_PASSWORD=$(cat ${toString cfg.passwordPath})"} + ${pkgs.prometheus-nut-exporter}/bin/nut_exporter \ + --nut.server=${cfg.nutServer} \ + --web.listen-address="${cfg.listenAddress}:${toString cfg.port}" \ + ${optionalString (cfg.nutUser != "") "--nut.username=${cfg.nutUser}"} \ + ${optionalString (cfg.nutVariables != []) "--nut.vars_enable=${concatStringsSep "," cfg.nutVariables}"} \ + ${concatStringsSep " " cfg.extraFlags} + ''; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/openldap.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/openldap.nix new file mode 100644 index 000000000000..bb65bd270933 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/openldap.nix @@ -0,0 +1,66 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.prometheus.exporters.openldap; + inherit (lib) mkOption types concatStringsSep; +in { + port = 9330; + extraOpts = { + ldapCredentialFile = mkOption { + type = types.path; + example = "/run/keys/ldap_pass"; + description = '' + Environment file to contain the credentials to authenticate against + `openldap`. + + The file should look like this: + ``` + --- + ldapUser: "cn=monitoring,cn=Monitor" + ldapPass: "secret" + ``` + ''; + }; + protocol = mkOption { + default = "tcp"; + example = "udp"; + type = types.str; + description = '' + Which protocol to use to connect against `openldap`. + ''; + }; + ldapAddr = mkOption { + default = "localhost:389"; + type = types.str; + description = '' + Address of the `openldap`-instance. + ''; + }; + metricsPath = mkOption { + default = "/metrics"; + type = types.str; + description = '' + URL path where metrics should be exposed. + ''; + }; + interval = mkOption { + default = "30s"; + type = types.str; + example = "1m"; + description = '' + Scrape interval of the exporter. + ''; + }; + }; + serviceOpts.serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-openldap-exporter}/bin/openldap_exporter \ + --promAddr ${cfg.listenAddress}:${toString cfg.port} \ + --metrPath ${cfg.metricsPath} \ + --ldapNet ${cfg.protocol} \ + --interval ${cfg.interval} \ + --config ${cfg.ldapCredentialFile} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/pgbouncer.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/pgbouncer.nix new file mode 100644 index 000000000000..71b602638632 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/pgbouncer.nix @@ -0,0 +1,150 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.pgbouncer; + inherit (lib) + mkOption + types + optionals + escapeShellArg + concatStringsSep + ; +in +{ + port = 9127; + extraOpts = { + + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + + connectionString = mkOption { + type = types.str; + default = ""; + example = "postgres://admin:@localhost:6432/pgbouncer?sslmode=require"; + description = '' + 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 = '' + 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 = '' + 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 = '' + Use systemd socket activation listeners instead of port listeners (Linux only). + ''; + }; + + logLevel = mkOption { + type = types.enum ["debug" "info" "warn" "error" ]; + default = "info"; + description = '' + Only log messages with the given severity or above. + ''; + }; + + logFormat = mkOption { + type = types.enum ["logfmt" "json"]; + default = "logfmt"; + description = '' + Output format of log messages. One of: [logfmt, json] + ''; + }; + + webConfigFile = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Path to configuration file that can enable TLS or authentication. + ''; + }; + + extraFlags = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + 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/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/php-fpm.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/php-fpm.nix new file mode 100644 index 000000000000..5d8253f26c43 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/php-fpm.nix @@ -0,0 +1,66 @@ +{ config +, lib +, pkgs +, options +, ... +}: + +let + logPrefix = "services.prometheus.exporter.php-fpm"; + cfg = config.services.prometheus.exporters.php-fpm; +in { + port = 9253; + extraOpts = { + package = lib.mkPackageOption pkgs "prometheus-php-fpm-exporter" {}; + + telemetryPath = lib.mkOption { + type = lib.types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/root/prometheus-php-fpm-exporter.env"; + description = '' + Environment file as defined in {manpage}`systemd.exec(5)`. + + Secrets may be passed to the service without adding them to the + world-readable Nix store, by specifying placeholder variables as + the option value in Nix and setting these variables accordingly in the + environment file. + + Environment variables from this file will be interpolated into the + config file using envsubst with this syntax: + `$ENVIRONMENT ''${VARIABLE}` + + For variables to use see [options and defaults](https://github.com/hipages/php-fpm_exporter#options-and-defaults). + + The main use is to set the PHP_FPM_SCRAPE_URI that indicate how to connect to PHP-FPM process. + + ``` + # Content of the environment file + PHP_FPM_SCRAPE_URI="unix:///tmp/php.sock;/status" + ``` + + Note that this file needs to be available on the host on which + this exporter is running. + ''; + }; + }; + + serviceOpts = { + serviceConfig = { + EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; + ExecStart = '' + ${lib.getExe cfg.package} server \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path ${cfg.telemetryPath} \ + ${lib.concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix new file mode 100644 index 000000000000..30b260dc3792 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix @@ -0,0 +1,82 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.pihole; + inherit (lib) + mkOption + types + mkRemovedOptionModule + optionalString + ; +in +{ + imports = [ + (mkRemovedOptionModule [ "interval"] "This option has been removed.") + ({ options.warnings = options.warnings; options.assertions = options.assertions; }) + ]; + + port = 9617; + extraOpts = { + apiToken = mkOption { + type = types.str; + default = ""; + example = "580a770cb40511eb85290242ac130003580a770cb40511eb85290242ac130003"; + description = '' + Pi-Hole API token which can be used instead of a password + ''; + }; + password = mkOption { + type = types.str; + default = ""; + example = "password"; + description = '' + The password to login into Pi-Hole. An api token can be used instead. + ''; + }; + piholeHostname = mkOption { + type = types.str; + default = "pihole"; + example = "127.0.0.1"; + description = '' + Hostname or address where to find the Pi-Hole webinterface + ''; + }; + piholePort = mkOption { + type = types.port; + default = 80; + example = 443; + description = '' + The port Pi-Hole webinterface is reachable on + ''; + }; + protocol = mkOption { + type = types.enum [ "http" "https" ]; + default = "http"; + example = "https"; + description = '' + The protocol which is used to connect to Pi-Hole + ''; + }; + timeout = mkOption { + type = types.str; + default = "5s"; + description = '' + Controls the timeout to connect to a Pi-Hole instance + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-pihole-exporter}/bin/pihole-exporter \ + ${optionalString (cfg.apiToken != "") "-pihole_api_token ${cfg.apiToken}"} \ + -pihole_hostname ${cfg.piholeHostname} \ + ${optionalString (cfg.password != "") "-pihole_password ${cfg.password}"} \ + -pihole_port ${toString cfg.piholePort} \ + -pihole_protocol ${cfg.protocol} \ + -port ${toString cfg.port} \ + -timeout ${cfg.timeout} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/ping.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/ping.nix new file mode 100644 index 000000000000..9122a6be66e6 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/ping.nix @@ -0,0 +1,48 @@ +{ config, lib, pkgs, options, ... }: + + +let + cfg = config.services.prometheus.exporters.ping; + inherit (lib) mkOption types concatStringsSep; + + settingsFormat = pkgs.formats.yaml {}; + configFile = settingsFormat.generate "config.yml" cfg.settings; +in +{ + port = 9427; + extraOpts = { + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + + settings = mkOption { + type = settingsFormat.type; + default = {}; + + description = '' + Configuration for ping_exporter, see + <https://github.com/czerwonk/ping_exporter> + for supported values. + ''; + }; + }; + + serviceOpts = { + serviceConfig = { + # ping-exporter needs `CAP_NET_RAW` to run as non root https://github.com/czerwonk/ping_exporter#running-as-non-root-user + CapabilityBoundingSet = [ "CAP_NET_RAW" ]; + AmbientCapabilities = [ "CAP_NET_RAW" ]; + ExecStart = '' + ${pkgs.prometheus-ping-exporter}/bin/ping_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path ${cfg.telemetryPath} \ + --config.path="${configFile}" \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix new file mode 100644 index 000000000000..7aa3622f16d6 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/postfix.nix @@ -0,0 +1,106 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.postfix; + inherit (lib) + mkOption + types + mkIf + escapeShellArg + concatStringsSep + optional + ; +in +{ + port = 9154; + extraOpts = { + group = mkOption { + type = types.str; + description = '' + Group under which the postfix exporter shall be run. + It should match the group that is allowed to access the + `showq` socket in the `queue/public/` directory. + Defaults to `services.postfix.setgidGroup` when postfix is enabled. + ''; + }; + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + logfilePath = mkOption { + type = types.path; + default = "/var/log/postfix_exporter_input.log"; + example = "/var/log/mail.log"; + description = '' + Path where Postfix writes log entries. + This file will be truncated by this exporter! + ''; + }; + showqPath = mkOption { + type = types.path; + default = "/var/lib/postfix/queue/public/showq"; + example = "/var/spool/postfix/public/showq"; + description = '' + Path where Postfix places its showq socket. + ''; + }; + systemd = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable reading metrics from the systemd journal instead of from a logfile + ''; + }; + unit = mkOption { + type = types.str; + default = "postfix.service"; + description = '' + Name of the postfix systemd unit. + ''; + }; + slice = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Name of the postfix systemd slice. + This overrides the {option}`systemd.unit`. + ''; + }; + journalPath = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Path to the systemd journal. + ''; + }; + }; + }; + serviceOpts = { + after = mkIf cfg.systemd.enable [ cfg.systemd.unit ]; + serviceConfig = { + DynamicUser = false; + # By default, each prometheus exporter only gets AF_INET & AF_INET6, + # but AF_UNIX is needed to read from the `showq`-socket. + RestrictAddressFamilies = [ "AF_UNIX" ]; + SupplementaryGroups = mkIf cfg.systemd.enable [ "systemd-journal" ]; + ExecStart = '' + ${pkgs.prometheus-postfix-exporter}/bin/postfix_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path ${cfg.telemetryPath} \ + --postfix.showq_path ${escapeShellArg cfg.showqPath} \ + ${concatStringsSep " \\\n " (cfg.extraFlags + ++ optional cfg.systemd.enable "--systemd.enable" + ++ optional cfg.systemd.enable (if cfg.systemd.slice != null + then "--systemd.slice ${cfg.systemd.slice}" + else "--systemd.unit ${cfg.systemd.unit}") + ++ optional (cfg.systemd.enable && (cfg.systemd.journalPath != null)) + "--systemd.journal_path ${escapeShellArg cfg.systemd.journalPath}" + ++ optional (!cfg.systemd.enable) "--postfix.logfile_path ${escapeShellArg cfg.logfilePath}")} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix new file mode 100644 index 000000000000..bf392382660a --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/postgres.nix @@ -0,0 +1,91 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.postgres; + inherit (lib) + mkOption + types + mkIf + mkForce + concatStringsSep + ; +in +{ + port = 9187; + extraOpts = { + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + dataSourceName = mkOption { + type = types.str; + default = "user=postgres database=postgres host=/run/postgresql sslmode=disable"; + example = "postgresql://username:password@localhost:5432/postgres?sslmode=disable"; + description = '' + Accepts PostgreSQL URI form and key=value form arguments. + ''; + }; + runAsLocalSuperUser = mkOption { + type = types.bool; + default = false; + description = '' + Whether to run the exporter as the local 'postgres' super user. + ''; + }; + + # TODO perhaps LoadCredential would be more appropriate + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/root/prometheus-postgres-exporter.env"; + description = '' + Environment file as defined in {manpage}`systemd.exec(5)`. + + Secrets may be passed to the service without adding them to the + world-readable Nix store, by specifying placeholder variables as + the option value in Nix and setting these variables accordingly in the + environment file. + + Environment variables from this file will be interpolated into the + config file using envsubst with this syntax: + `$ENVIRONMENT ''${VARIABLE}` + + The main use is to set the DATA_SOURCE_NAME that contains the + postgres password + + note that contents from this file will override dataSourceName + if you have set it from nix. + + ``` + # Content of the environment file + DATA_SOURCE_NAME=postgresql://username:password@localhost:5432/postgres?sslmode=disable + ``` + + Note that this file needs to be available on the host on which + this exporter is running. + ''; + }; + + }; + serviceOpts = { + environment.DATA_SOURCE_NAME = cfg.dataSourceName; + serviceConfig = { + DynamicUser = false; + User = mkIf cfg.runAsLocalSuperUser (mkForce "postgres"); + EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; + ExecStart = '' + ${pkgs.prometheus-postgres-exporter}/bin/postgres_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path ${cfg.telemetryPath} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + RestrictAddressFamilies = [ + # Need AF_UNIX to collect data + "AF_UNIX" + ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/process.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/process.nix new file mode 100644 index 000000000000..8e5eceee067c --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/process.nix @@ -0,0 +1,50 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.process; + inherit (lib) + mkOption + types + literalExpression + concatStringsSep + ; + configFile = pkgs.writeText "process-exporter.yaml" (builtins.toJSON cfg.settings); +in +{ + port = 9256; + extraOpts = { + settings.process_names = mkOption { + type = types.listOf types.anything; + default = []; + example = literalExpression '' + [ + # Remove nix store path from process name + { name = "{{.Matches.Wrapped}} {{ .Matches.Args }}"; cmdline = [ "^/nix/store[^ ]*/(?P<Wrapped>[^ /]*) (?P<Args>.*)" ]; } + ] + ''; + description = '' + All settings expressed as an Nix attrset. + + Check the official documentation for the corresponding YAML + settings that can all be used here: <https://github.com/ncabatoff/process-exporter> + ''; + }; + }; + serviceOpts = { + serviceConfig = { + DynamicUser = false; + ExecStart = '' + ${pkgs.prometheus-process-exporter}/bin/process-exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --config.path ${configFile} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + NoNewPrivileges = true; + ProtectHome = true; + ProtectSystem = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/pve.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/pve.nix new file mode 100644 index 000000000000..8928577b6953 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/pve.nix @@ -0,0 +1,140 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.pve; + inherit (lib) + mkOption + types + mkPackageOption + optionalString + optionalAttrs + ; + + # pve exporter requires a config file so create an empty one if configFile is not provided + emptyConfigFile = pkgs.writeTextFile { + name = "pve.yml"; + text = "default:"; + }; + + computedConfigFile = if cfg.configFile == null then emptyConfigFile else cfg.configFile; +in +{ + port = 9221; + extraOpts = { + package = mkPackageOption pkgs "prometheus-pve-exporter" { }; + + environmentFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/etc/prometheus-pve-exporter/pve.env"; + description = '' + Path to the service's environment file. This path can either be a computed path in /nix/store or a path in the local filesystem. + + The environment file should NOT be stored in /nix/store as it contains passwords and/or keys in plain text. + + Environment reference: https://github.com/prometheus-pve/prometheus-pve-exporter#authentication + ''; + }; + + configFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/etc/prometheus-pve-exporter/pve.yml"; + description = '' + Path to the service's config file. This path can either be a computed path in /nix/store or a path in the local filesystem. + + The config file should NOT be stored in /nix/store as it will contain passwords and/or keys in plain text. + + If both configFile and environmentFile are provided, the configFile option will be ignored. + + Configuration reference: https://github.com/prometheus-pve/prometheus-pve-exporter/#authentication + ''; + }; + + server = { + keyFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/var/lib/prometheus-pve-exporter/privkey.key"; + description = '' + Path to a SSL private key file for the server + ''; + }; + + certFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/var/lib/prometheus-pve-exporter/full-chain.pem"; + description = '' + Path to a SSL certificate file for the server + ''; + }; + }; + + collectors = { + status = mkOption { + type = types.bool; + default = true; + description = '' + Collect Node/VM/CT status + ''; + }; + version = mkOption { + type = types.bool; + default = true; + description = '' + Collect PVE version info + ''; + }; + node = mkOption { + type = types.bool; + default = true; + description = '' + Collect PVE node info + ''; + }; + cluster = mkOption { + type = types.bool; + default = true; + description = '' + Collect PVE cluster info + ''; + }; + resources = mkOption { + type = types.bool; + default = true; + description = '' + Collect PVE resources info + ''; + }; + config = mkOption { + type = types.bool; + default = true; + description = '' + Collect PVE onboot status + ''; + }; + }; + }; + serviceOpts = { + serviceConfig = { + DynamicUser = cfg.environmentFile == null; + LoadCredential = "configFile:${computedConfigFile}"; + ExecStart = '' + ${cfg.package}/bin/pve_exporter \ + --${optionalString (!cfg.collectors.status) "no-"}collector.status \ + --${optionalString (!cfg.collectors.version) "no-"}collector.version \ + --${optionalString (!cfg.collectors.node) "no-"}collector.node \ + --${optionalString (!cfg.collectors.cluster) "no-"}collector.cluster \ + --${optionalString (!cfg.collectors.resources) "no-"}collector.resources \ + --${optionalString (!cfg.collectors.config) "no-"}collector.config \ + ${optionalString (cfg.server.keyFile != null) "--server.keyfile ${cfg.server.keyFile}"} \ + ${optionalString (cfg.server.certFile != null) "--server.certfile ${cfg.server.certFile}"} \ + --config.file %d/configFile \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} + ''; + } // optionalAttrs (cfg.environmentFile != null) { + EnvironmentFile = cfg.environmentFile; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix new file mode 100644 index 000000000000..d788ce363d61 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/py-air-control.nix @@ -0,0 +1,52 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.py-air-control; + inherit (lib) mkOption types; + + workingDir = "/var/lib/${cfg.stateDir}"; + +in +{ + port = 9896; + extraOpts = { + deviceHostname = mkOption { + type = types.str; + example = "192.168.1.123"; + description = '' + The hostname of the air purification device from which to scrape the metrics. + ''; + }; + protocol = mkOption { + type = types.str; + default = "http"; + description = '' + The protocol to use when communicating with the air purification device. + Available: [http, coap, plain_coap] + ''; + }; + stateDir = mkOption { + type = types.str; + default = "prometheus-py-air-control-exporter"; + description = '' + Directory below `/var/lib` to store runtime data. + This directory will be created automatically using systemd's StateDirectory mechanism. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + DynamicUser = false; + StateDirectory = cfg.stateDir; + WorkingDirectory = workingDir; + ExecStart = '' + ${pkgs.python3Packages.py-air-control-exporter}/bin/py-air-control-exporter \ + --host ${cfg.deviceHostname} \ + --protocol ${cfg.protocol} \ + --listen-port ${toString cfg.port} \ + --listen-address ${cfg.listenAddress} + ''; + Environment = [ "HOME=${workingDir}" ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/redis.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/redis.nix new file mode 100644 index 000000000000..672e3dfe7b05 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/redis.nix @@ -0,0 +1,19 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.redis; + inherit (lib) concatStringsSep; +in +{ + port = 9121; + serviceOpts = { + serviceConfig = { + RestrictAddressFamilies = [ "AF_UNIX" ]; + ExecStart = '' + ${pkgs.prometheus-redis-exporter}/bin/redis_exporter \ + -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/restic.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/restic.nix new file mode 100644 index 000000000000..ef44803ba053 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/restic.nix @@ -0,0 +1,141 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.restic; + inherit (lib) + mkOption + types + concatStringsSep + mkIf + mapAttrs' + splitString + toUpper + optionalAttrs + nameValuePair + ; +in +{ + port = 9753; + extraOpts = { + repository = mkOption { + type = types.str; + description = '' + URI pointing to the repository to monitor. + ''; + example = "sftp:backup@192.168.1.100:/backups/example"; + }; + + passwordFile = mkOption { + type = types.path; + description = '' + File containing the password to the repository. + ''; + example = "/etc/nixos/restic-password"; + }; + + environmentFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + File containing the credentials to access the repository, in the + format of an EnvironmentFile as described by systemd.exec(5) + ''; + }; + + refreshInterval = mkOption { + type = types.ints.unsigned; + default = 60; + description = '' + Refresh interval for the metrics in seconds. + Computing the metrics is an expensive task, keep this value as high as possible. + ''; + }; + + rcloneOptions = mkOption { + type = with types; attrsOf (oneOf [ str bool ]); + default = { }; + description = '' + Options to pass to rclone to control its behavior. + See <https://rclone.org/docs/#options> for + available options. When specifying option names, strip the + leading `--`. To set a flag such as + `--drive-use-trash`, which does not take a value, + set the value to the Boolean `true`. + ''; + }; + + rcloneConfig = mkOption { + type = with types; attrsOf (oneOf [ str bool ]); + default = { }; + description = '' + Configuration for the rclone remote being used for backup. + See the remote's specific options under rclone's docs at + <https://rclone.org/docs/>. When specifying + option names, use the "config" name specified in the docs. + For example, to set `--b2-hard-delete` for a B2 + remote, use `hard_delete = true` in the + attribute set. + + ::: {.warning} + Secrets set in here will be world-readable in the Nix + store! Consider using the {option}`rcloneConfigFile` + option instead to specify secret values separately. Note that + options set here will override those set in the config file. + ::: + ''; + }; + + rcloneConfigFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + Path to the file containing rclone configuration. This file + must contain configuration for the remote specified in this backup + set and also must be readable by root. + + ::: {.caution} + Options set in `rcloneConfig` will override those set in this + file. + ::: + ''; + }; + }; + + serviceOpts = { + script = '' + export RESTIC_PASSWORD_FILE=$CREDENTIALS_DIRECTORY/RESTIC_PASSWORD_FILE + ${pkgs.prometheus-restic-exporter}/bin/restic-exporter.py \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + serviceConfig = { + EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; + LoadCredential = [ "RESTIC_PASSWORD_FILE:${cfg.passwordFile}" ]; + }; + environment = + let + rcloneRemoteName = builtins.elemAt (splitString ":" cfg.repository) 1; + rcloneAttrToOpt = v: "RCLONE_" + toUpper (builtins.replaceStrings [ "-" ] [ "_" ] v); + rcloneAttrToConf = v: "RCLONE_CONFIG_" + toUpper (rcloneRemoteName + "_" + v); + toRcloneVal = v: if lib.isBool v then lib.boolToString v else v; + in + { + RESTIC_REPOSITORY = cfg.repository; + LISTEN_ADDRESS = cfg.listenAddress; + LISTEN_PORT = toString cfg.port; + REFRESH_INTERVAL = toString cfg.refreshInterval; + } + // (mapAttrs' + (name: value: + nameValuePair (rcloneAttrToOpt name) (toRcloneVal value) + ) + cfg.rcloneOptions) + // optionalAttrs (cfg.rcloneConfigFile != null) { + RCLONE_CONFIG = cfg.rcloneConfigFile; + } + // (mapAttrs' + (name: value: + nameValuePair (rcloneAttrToConf name) (toRcloneVal value) + ) + cfg.rcloneConfig); + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix new file mode 100644 index 000000000000..8993aee5d248 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix @@ -0,0 +1,104 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.rspamd; + inherit (lib) + mkOption + types + replaceStrings + mkRemovedOptionModule + recursiveUpdate + concatStringsSep + literalExpression + ; + + mkFile = conf: + pkgs.writeText "rspamd-exporter-config.yml" (builtins.toJSON conf); + + generateConfig = extraLabels: { + modules.default.metrics = (map (path: { + name = "rspamd_${replaceStrings [ "[" "." " " "]" "\\" "'" ] [ "_" "_" "_" "" "" "" ] path}"; + path = "{ .${path} }"; + labels = extraLabels; + }) [ + "actions['add\\ header']" + "actions['no\\ action']" + "actions['rewrite\\ subject']" + "actions['soft\\ reject']" + "actions.greylist" + "actions.reject" + "bytes_allocated" + "chunks_allocated" + "chunks_freed" + "chunks_oversized" + "connections" + "control_connections" + "ham_count" + "learned" + "pools_allocated" + "pools_freed" + "read_only" + "scanned" + "shared_chunks_allocated" + "spam_count" + "total_learns" + ]) ++ [{ + name = "rspamd_statfiles"; + type = "object"; + path = "{.statfiles[*]}"; + labels = recursiveUpdate { + symbol = "{.symbol}"; + type = "{.type}"; + } extraLabels; + values = { + revision = "{.revision}"; + size = "{.size}"; + total = "{.total}"; + used = "{.used}"; + languages = "{.languages}"; + users = "{.users}"; + }; + }]; + }; +in +{ + port = 7980; + extraOpts = { + extraLabels = mkOption { + type = types.attrsOf types.str; + default = { + host = config.networking.hostName; + }; + defaultText = literalExpression "{ host = config.networking.hostName; }"; + example = literalExpression '' + { + host = config.networking.hostName; + custom_label = "some_value"; + } + ''; + description = "Set of labels added to each metric."; + }; + }; + serviceOpts.serviceConfig.ExecStart = '' + ${pkgs.prometheus-json-exporter}/bin/json_exporter \ + --config.file ${mkFile (generateConfig cfg.extraLabels)} \ + --web.listen-address "${cfg.listenAddress}:${toString cfg.port}" \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + + imports = [ + (mkRemovedOptionModule [ "url" ] '' + This option was removed. The URL of the rspamd metrics endpoint + must now be provided to the exporter by prometheus via the url + parameter `target'. + + In prometheus a scrape URL would look like this: + + http://some.rspamd-exporter.host:7980/probe?target=http://some.rspamd.host:11334/stat + + For more information, take a look at the official documentation + (https://github.com/prometheus-community/json_exporter) of the json_exporter. + '') + ({ options.warnings = options.warnings; options.assertions = options.assertions; }) + ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix new file mode 100644 index 000000000000..02624a0e80e7 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/rtl_433.nix @@ -0,0 +1,83 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.rtl_433; +in +{ + port = 9550; + + extraOpts = let + mkMatcherOptionType = field: description: with lib.types; + listOf (submodule { + options = { + name = lib.mkOption { + type = str; + description = "Name to match."; + }; + "${field}" = lib.mkOption { + type = int; + description = description; + }; + location = lib.mkOption { + type = str; + description = "Location to match."; + }; + }; + }); + in + { + rtl433Flags = lib.mkOption { + type = lib.types.str; + default = "-C si"; + example = "-C si -R 19"; + description = '' + Flags passed verbatim to rtl_433 binary. + Having `-C si` (the default) is recommended since only Celsius temperatures are parsed. + ''; + }; + channels = lib.mkOption { + type = mkMatcherOptionType "channel" "Channel to match."; + default = []; + example = [ + { name = "Acurite"; channel = 6543; location = "Kitchen"; } + ]; + description = '' + List of channel matchers to export. + ''; + }; + ids = lib.mkOption { + type = mkMatcherOptionType "id" "ID to match."; + default = []; + example = [ + { name = "Nexus"; id = 1; location = "Bedroom"; } + ]; + description = '' + List of ID matchers to export. + ''; + }; + }; + + serviceOpts = { + serviceConfig = { + # rtl-sdr udev rules make supported USB devices +rw by plugdev. + SupplementaryGroups = "plugdev"; + # rtl_433 needs rw access to the USB radio. + PrivateDevices = lib.mkForce false; + DeviceAllow = lib.mkForce "char-usb_device rw"; + RestrictAddressFamilies = [ "AF_NETLINK" ]; + + ExecStart = let + matchers = (map (m: + "--channel_matcher '${m.name},${toString m.channel},${m.location}'" + ) cfg.channels) ++ (map (m: + "--id_matcher '${m.name},${toString m.id},${m.location}'" + ) cfg.ids); in '' + ${pkgs.prometheus-rtl_433-exporter}/bin/rtl_433_prometheus \ + -listen ${cfg.listenAddress}:${toString cfg.port} \ + -subprocess "${pkgs.rtl_433}/bin/rtl_433 -F json ${cfg.rtl433Flags}" \ + ${lib.concatStringsSep " \\\n " matchers} \ + ${lib.concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/sabnzbd.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/sabnzbd.nix new file mode 100644 index 000000000000..0d937ac6673f --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/sabnzbd.nix @@ -0,0 +1,57 @@ +{ config, lib, pkgs, options, ... }: + +let + inherit (lib) mkOption types; + cfg = config.services.prometheus.exporters.sabnzbd; +in +{ + port = 9387; + + extraOpts = { + servers = mkOption { + description = "List of sabnzbd servers to connect to."; + type = types.listOf (types.submodule { + options = { + baseUrl = mkOption { + type = types.str; + description = "Base URL of the sabnzbd server."; + example = "http://localhost:8080/sabnzbd"; + }; + apiKeyFile = mkOption { + type = types.str; + description = '' + The path to a file containing the API key. + The file is securely passed to the service by leveraging systemd credentials. + No special permissions need to be set on this file. + ''; + example = "/run/secrets/sabnzbd_apikey"; + }; + }; + }); + }; + }; + + serviceOpts = + let + servers = lib.zipAttrs cfg.servers; + credentials = lib.imap0 (i: v: { name = "apikey-${toString i}"; path = v; }) servers.apiKeyFile; + in + { + serviceConfig.LoadCredential = builtins.map ({ name, path }: "${name}:${path}") credentials; + + environment = { + METRICS_PORT = toString cfg.port; + METRICS_ADDR = cfg.listenAddress; + SABNZBD_BASEURLS = lib.concatStringsSep "," servers.baseUrl; + }; + + script = + let + apiKeys = lib.concatStringsSep "," (builtins.map (cred: "$(< $CREDENTIALS_DIRECTORY/${cred.name})") credentials); + in + '' + export SABNZBD_APIKEYS="${apiKeys}" + exec ${lib.getExe pkgs.prometheus-sabnzbd-exporter} + ''; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/scaphandre.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/scaphandre.nix new file mode 100644 index 000000000000..fbee2850db74 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/scaphandre.nix @@ -0,0 +1,34 @@ +{ config +, lib +, pkgs +, options +, ... +}: + +let + logPrefix = "services.prometheus.exporter.scaphandre"; + cfg = config.services.prometheus.exporters.scaphandre; +in { + port = 8080; + extraOpts = { + telemetryPath = lib.mkOption { + type = lib.types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + }; + + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.scaphandre}/bin/scaphandre prometheus \ + --address ${cfg.listenAddress} \ + --port ${toString cfg.port} \ + --suffix ${cfg.telemetryPath} \ + ${lib.concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/script.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/script.nix new file mode 100644 index 000000000000..0967ce236a62 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/script.nix @@ -0,0 +1,68 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.script; + inherit (lib) + mkOption + types + literalExpression + concatStringsSep + ; + configFile = pkgs.writeText "script-exporter.yaml" (builtins.toJSON cfg.settings); +in +{ + port = 9172; + extraOpts = { + settings.scripts = mkOption { + type = with types; listOf (submodule { + options = { + name = mkOption { + type = str; + example = "sleep"; + description = "Name of the script."; + }; + script = mkOption { + type = str; + example = "sleep 5"; + description = "Shell script to execute when metrics are requested."; + }; + timeout = mkOption { + type = nullOr int; + default = null; + example = 60; + description = "Optional timeout for the script in seconds."; + }; + }; + }); + example = literalExpression '' + { + scripts = [ + { name = "sleep"; script = "sleep 5"; } + ]; + } + ''; + description = '' + All settings expressed as an Nix attrset. + + Check the official documentation for the corresponding YAML + settings that can all be used here: <https://github.com/adhocteam/script_exporter#sample-configuration> + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-script-exporter}/bin/script_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --config.file ${configFile} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + NoNewPrivileges = true; + ProtectHome = true; + ProtectSystem = "strict"; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/shelly.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/shelly.nix new file mode 100644 index 000000000000..be3e483c6ee1 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/shelly.nix @@ -0,0 +1,26 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.shelly; + inherit (lib) mkOption types; +in +{ + port = 9784; + extraOpts = { + metrics-file = mkOption { + type = types.path; + description = '' + Path to the JSON file with the metric definitions + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-shelly-exporter}/bin/shelly_exporter \ + -metrics-file ${cfg.metrics-file} \ + -listen-address ${cfg.listenAddress}:${toString cfg.port} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix new file mode 100644 index 000000000000..8aadd87abbed --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix @@ -0,0 +1,63 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.smartctl; + inherit (lib) mkOption types literalExpression; + args = lib.escapeShellArgs ([ + "--web.listen-address=${cfg.listenAddress}:${toString cfg.port}" + "--smartctl.path=${pkgs.smartmontools}/bin/smartctl" + "--smartctl.interval=${cfg.maxInterval}" + ] ++ map (device: "--smartctl.device=${device}") cfg.devices + ++ cfg.extraFlags); +in { + port = 9633; + + extraOpts = { + devices = mkOption { + type = types.listOf types.str; + default = []; + example = literalExpression '' + [ "/dev/sda", "/dev/nvme0n1" ]; + ''; + description = '' + Paths to the disks that will be monitored. Will autodiscover + all disks if none given. + ''; + }; + maxInterval = mkOption { + type = types.str; + default = "60s"; + example = "2m"; + description = '' + Interval that limits how often a disk can be queried. + ''; + }; + }; + + serviceOpts = { + serviceConfig = { + AmbientCapabilities = [ + "CAP_SYS_RAWIO" + "CAP_SYS_ADMIN" + ]; + CapabilityBoundingSet = [ + "CAP_SYS_RAWIO" + "CAP_SYS_ADMIN" + ]; + DevicePolicy = "closed"; + DeviceAllow = lib.mkOverride 50 [ + "block-blkext rw" + "block-sd rw" + "char-nvme rw" + ]; + ExecStart = '' + ${pkgs.prometheus-smartctl-exporter}/bin/smartctl_exporter ${args} + ''; + PrivateDevices = lib.mkForce false; + ProtectProc = "invisible"; + ProcSubset = "pid"; + SupplementaryGroups = [ "disk" ]; + SystemCallFilter = [ "@system-service" "~@privileged" ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix new file mode 100644 index 000000000000..c3baed150376 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/smokeping.nix @@ -0,0 +1,60 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.smokeping; + inherit (lib) mkOption types concatStringsSep; + goDuration = types.mkOptionType { + name = "goDuration"; + description = "Go duration (https://golang.org/pkg/time/#ParseDuration)"; + check = x: types.str.check x && builtins.match "(-?[0-9]+(\.[0-9]+)?(ns|us|µs|ms|s|m|h))+" x != null; + inherit (types.str) merge; + }; +in +{ + port = 9374; + extraOpts = { + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + pingInterval = mkOption { + type = goDuration; + default = "1s"; + description = '' + Interval between pings. + ''; + }; + buckets = mkOption { + type = types.commas; + default = "5e-05,0.0001,0.0002,0.0004,0.0008,0.0016,0.0032,0.0064,0.0128,0.0256,0.0512,0.1024,0.2048,0.4096,0.8192,1.6384,3.2768,6.5536,13.1072,26.2144"; + description = '' + List of buckets to use for the response duration histogram. + ''; + }; + hosts = mkOption { + type = with types; listOf str; + description = '' + List of endpoints to probe. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + AmbientCapabilities = [ "CAP_NET_RAW" ]; + CapabilityBoundingSet = [ "CAP_NET_RAW" ]; + ExecStart = '' + ${pkgs.prometheus-smokeping-prober}/bin/smokeping_prober \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path ${cfg.telemetryPath} \ + --buckets ${cfg.buckets} \ + --ping.interval ${cfg.pingInterval} \ + --privileged \ + ${concatStringsSep " \\\n " cfg.extraFlags} \ + ${concatStringsSep " " cfg.hosts} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/snmp.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/snmp.nix new file mode 100644 index 000000000000..dc10a9a2f92e --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/snmp.nix @@ -0,0 +1,105 @@ +{ config, lib, pkgs, options, ... }: + +let + logPrefix = "services.prometheus.exporters.snmp"; + cfg = config.services.prometheus.exporters.snmp; + inherit (lib) + mkOption + types + literalExpression + escapeShellArg + concatStringsSep + ; + + # This ensures that we can deal with string paths, path types and + # store-path strings with context. + coerceConfigFile = file: + if (builtins.isPath file) || (lib.isStorePath file) then + file + else + (lib.warn '' + ${logPrefix}: configuration file "${file}" is being copied to the nix-store. + If you would like to avoid that, please set enableConfigCheck to false. + '' /. + file); + + checkConfig = file: + pkgs.runCommandLocal "checked-snmp-exporter-config.yml" { + nativeBuildInputs = [ pkgs.buildPackages.prometheus-snmp-exporter ]; + } '' + ln -s ${coerceConfigFile file} $out + snmp_exporter --dry-run --config.file $out + ''; +in +{ + port = 9116; + extraOpts = { + configurationPath = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Path to a snmp exporter configuration file. Mutually exclusive with 'configuration' option. + ''; + example = literalExpression "./snmp.yml"; + }; + + configuration = mkOption { + type = types.nullOr types.attrs; + default = null; + description = '' + Snmp exporter configuration as nix attribute set. Mutually exclusive with 'configurationPath' option. + ''; + example = { + auths.public_v2 = { + community = "public"; + version = 2; + }; + }; + }; + + enableConfigCheck = mkOption { + type = types.bool; + default = true; + description = '' + Whether to run a correctness check for the configuration file. This depends + on the configuration file residing in the nix-store. Paths passed as string will + be copied to the store. + ''; + }; + + logFormat = mkOption { + type = types.enum ["logfmt" "json"]; + default = "logfmt"; + description = '' + Output format of log messages. + ''; + }; + + logLevel = mkOption { + type = types.enum ["debug" "info" "warn" "error"]; + default = "info"; + description = '' + Only log messages with the given severity or above. + ''; + }; + }; + serviceOpts = let + uncheckedConfigFile = if cfg.configurationPath != null + then cfg.configurationPath + else "${pkgs.writeText "snmp-exporter-conf.yml" (builtins.toJSON cfg.configuration)}"; + configFile = if cfg.enableConfigCheck then + checkConfig uncheckedConfigFile + else + uncheckedConfigFile; + in { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-snmp-exporter}/bin/snmp_exporter \ + --config.file=${escapeShellArg configFile} \ + --log.format=${escapeShellArg cfg.logFormat} \ + --log.level=${cfg.logLevel} \ + --web.listen-address=${cfg.listenAddress}:${toString cfg.port} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/sql.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/sql.nix new file mode 100644 index 000000000000..59715f5d33e2 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/sql.nix @@ -0,0 +1,114 @@ +{ config, lib, pkgs, options, ... }: +let + cfg = config.services.prometheus.exporters.sql; + inherit (lib) + mkOption + types + mapAttrs + mapAttrsToList + concatStringsSep + ; + cfgOptions = { + options = with types; { + jobs = mkOption { + type = attrsOf (submodule jobOptions); + default = { }; + description = "An attrset of metrics scraping jobs to run."; + }; + }; + }; + jobOptions = { + options = with types; { + interval = mkOption { + type = str; + description = '' + How often to run this job, specified in + [Go duration](https://golang.org/pkg/time/#ParseDuration) format. + ''; + }; + connections = mkOption { + type = listOf str; + description = "A list of connection strings of the SQL servers to scrape metrics from"; + }; + startupSql = mkOption { + type = listOf str; + default = []; + description = "A list of SQL statements to execute once after making a connection."; + }; + queries = mkOption { + type = attrsOf (submodule queryOptions); + description = "SQL queries to run."; + }; + }; + }; + queryOptions = { + options = with types; { + help = mkOption { + type = nullOr str; + default = null; + description = "A human-readable description of this metric."; + }; + labels = mkOption { + type = listOf str; + default = [ ]; + description = "A set of columns that will be used as Prometheus labels."; + }; + query = mkOption { + type = str; + description = "The SQL query to run."; + }; + values = mkOption { + type = listOf str; + description = "A set of columns that will be used as values of this metric."; + }; + }; + }; + + configFile = + if cfg.configFile != null + then cfg.configFile + else + let + nameInline = mapAttrsToList (k: v: v // { name = k; }); + renameStartupSql = j: removeAttrs (j // { startup_sql = j.startupSql; }) [ "startupSql" ]; + configuration = { + jobs = map renameStartupSql + (nameInline (mapAttrs (k: v: (v // { queries = nameInline v.queries; })) cfg.configuration.jobs)); + }; + in + builtins.toFile "config.yaml" (builtins.toJSON configuration); +in +{ + extraOpts = { + configFile = mkOption { + type = with types; nullOr path; + default = null; + description = '' + Path to configuration file. + ''; + }; + configuration = mkOption { + type = with types; nullOr (submodule cfgOptions); + default = null; + description = '' + Exporter configuration as nix attribute set. Mutually exclusive with 'configFile' option. + ''; + }; + }; + + port = 9237; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-sql-exporter}/bin/sql_exporter \ + -web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + -config.file ${configFile} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + RestrictAddressFamilies = [ + # Need AF_UNIX to collect data + "AF_UNIX" + ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/statsd.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/statsd.nix new file mode 100644 index 000000000000..b1bc65c9a492 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/statsd.nix @@ -0,0 +1,18 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.statsd; + inherit (lib) concatStringsSep; +in +{ + port = 9102; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-statsd-exporter}/bin/statsd_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/surfboard.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/surfboard.nix new file mode 100644 index 000000000000..d848e263a3b6 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/surfboard.nix @@ -0,0 +1,30 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.surfboard; + inherit (lib) mkOption types concatStringsSep; +in +{ + port = 9239; + extraOpts = { + modemAddress = mkOption { + type = types.str; + default = "192.168.100.1"; + description = '' + The hostname or IP of the cable modem. + ''; + }; + }; + serviceOpts = { + description = "Prometheus exporter for surfboard cable modem"; + unitConfig.Documentation = "https://github.com/ipstatic/surfboard_exporter"; + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-surfboard-exporter}/bin/surfboard_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --modem-address ${cfg.modemAddress} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix new file mode 100644 index 000000000000..52bad81ed7d5 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/systemd.nix @@ -0,0 +1,21 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.services.prometheus.exporters.systemd; + inherit (lib) concatStringsSep; +in { + port = 9558; + + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-systemd-exporter}/bin/systemd_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} ${concatStringsSep " " cfg.extraFlags} + ''; + RestrictAddressFamilies = [ + # Need AF_UNIX to collect data + "AF_UNIX" + ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/tor.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/tor.nix new file mode 100644 index 000000000000..d39112d0c283 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/tor.nix @@ -0,0 +1,43 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.tor; + inherit (lib) mkOption types concatStringsSep; +in +{ + port = 9130; + extraOpts = { + torControlAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + description = '' + Tor control IP address or hostname. + ''; + }; + + torControlPort = mkOption { + type = types.port; + default = 9051; + description = '' + Tor control port. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-tor-exporter}/bin/prometheus-tor-exporter \ + -b ${cfg.listenAddress} \ + -p ${toString cfg.port} \ + -a ${cfg.torControlAddress} \ + -c ${toString cfg.torControlPort} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + + # CPython requires a process to either have $HOME defined or run as a UID + # defined in /etc/passwd. The latter is false with DynamicUser, so define a + # dummy $HOME. https://bugs.python.org/issue10496 + environment = { HOME = "/var/empty"; }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix new file mode 100644 index 000000000000..df6011e2434b --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/unbound.nix @@ -0,0 +1,103 @@ +{ config +, lib +, pkgs +, options +, ... +}: + +let + cfg = config.services.prometheus.exporters.unbound; + inherit (lib) + mkOption + types + mkRemovedOptionModule + optionalAttrs + optionalString + mkMerge + mkIf + ; +in +{ + imports = [ + (mkRemovedOptionModule [ "controlInterface" ] "This option was removed, use the `unbound.host` option instead.") + (mkRemovedOptionModule [ "fetchType" ] "This option was removed, use the `unbound.host` option instead.") + ({ options.warnings = options.warnings; options.assertions = options.assertions; }) + ]; + + port = 9167; + extraOpts = { + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + + unbound = { + ca = mkOption { + type = types.nullOr types.path; + default = "/var/lib/unbound/unbound_server.pem"; + example = null; + description = '' + Path to the Unbound server certificate authority + ''; + }; + + certificate = mkOption { + type = types.nullOr types.path; + default = "/var/lib/unbound/unbound_control.pem"; + example = null; + description = '' + Path to the Unbound control socket certificate + ''; + }; + + key = mkOption { + type = types.nullOr types.path; + default = "/var/lib/unbound/unbound_control.key"; + example = null; + description = '' + Path to the Unbound control socket key. + ''; + }; + + host = mkOption { + type = types.str; + default = "tcp://127.0.0.1:8953"; + example = "unix:///run/unbound/unbound.socket"; + description = '' + Path to the unbound control socket. Supports unix domain sockets, as well as the TCP interface. + ''; + }; + }; + }; + + serviceOpts = mkMerge ([{ + serviceConfig = { + User = "unbound"; # to access the unbound_control.key + ExecStart = '' + ${pkgs.prometheus-unbound-exporter}/bin/unbound_exporter \ + --unbound.host "${cfg.unbound.host}" \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path ${cfg.telemetryPath} \ + ${optionalString (cfg.unbound.ca != null) "--unbound.ca ${cfg.unbound.ca}"} \ + ${optionalString (cfg.unbound.certificate != null) "--unbound.cert ${cfg.unbound.certificate}"} \ + ${optionalString (cfg.unbound.key != null) "--unbound.key ${cfg.unbound.key}"} \ + ${toString cfg.extraFlags} + ''; + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; + } // optionalAttrs (!config.services.unbound.enable) { + DynamicUser = true; + }; + }] ++ [ + (mkIf config.services.unbound.enable { + after = [ "unbound.service" ]; + requires = [ "unbound.service" ]; + }) + ]); +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/unifi.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/unifi.nix new file mode 100644 index 000000000000..07d177251f40 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/unifi.nix @@ -0,0 +1,71 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.unifi; + inherit (lib) + mkOption + types + escapeShellArg + optionalString + concatStringsSep + ; +in +{ + port = 9130; + extraOpts = { + unifiAddress = mkOption { + type = types.str; + example = "https://10.0.0.1:8443"; + description = '' + URL of the UniFi Controller API. + ''; + }; + + unifiInsecure = mkOption { + type = types.bool; + default = false; + description = '' + If enabled skip the verification of the TLS certificate of the UniFi Controller API. + Use with caution. + ''; + }; + + unifiUsername = mkOption { + type = types.str; + example = "ReadOnlyUser"; + description = '' + username for authentication against UniFi Controller API. + ''; + }; + + unifiPassword = mkOption { + type = types.str; + description = '' + Password for authentication against UniFi Controller API. + ''; + }; + + unifiTimeout = mkOption { + type = types.str; + default = "5s"; + example = "2m"; + description = '' + Timeout including unit for UniFi Controller API requests. + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-unifi-exporter}/bin/unifi_exporter \ + -telemetry.addr ${cfg.listenAddress}:${toString cfg.port} \ + -unifi.addr ${cfg.unifiAddress} \ + -unifi.username ${escapeShellArg cfg.unifiUsername} \ + -unifi.password ${escapeShellArg cfg.unifiPassword} \ + -unifi.timeout ${cfg.unifiTimeout} \ + ${optionalString cfg.unifiInsecure "-unifi.insecure" } \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix new file mode 100644 index 000000000000..7b9ba4c5d1ed --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix @@ -0,0 +1,36 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.unpoller; + inherit (lib) mkEnableOption generators; + + configFile = pkgs.writeText "prometheus-unpoller-exporter.json" (generators.toJSON {} { + poller = { inherit (cfg.log) debug quiet; }; + unifi = { inherit (cfg) controllers; }; + influxdb.disable = true; + datadog.disable = true; # workaround for https://github.com/unpoller/unpoller/issues/442 + prometheus = { + http_listen = "${cfg.listenAddress}:${toString cfg.port}"; + report_errors = cfg.log.prometheusErrors; + }; + inherit (cfg) loki; + }); + +in { + port = 9130; + + extraOpts = { + inherit (options.services.unpoller.unifi) controllers; + inherit (options.services.unpoller) loki; + 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.unpoller}/bin/unpoller --config ${configFile}"; + DynamicUser = false; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/v2ray.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/v2ray.nix new file mode 100644 index 000000000000..4fda15c9ee4e --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/v2ray.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.v2ray; + inherit (lib) mkOption types concatStringsSep; +in +{ + port = 9299; + extraOpts = { + v2rayEndpoint = mkOption { + type = types.str; + default = "127.0.0.1:54321"; + description = '' + v2ray grpc api endpoint + ''; + }; + }; + serviceOpts = { + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-v2ray-exporter}/bin/v2ray-exporter \ + --v2ray-endpoint ${cfg.v2rayEndpoint} \ + --listen ${cfg.listenAddress}:${toString cfg.port} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix new file mode 100644 index 000000000000..e94c513ae84f --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/varnish.nix @@ -0,0 +1,95 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.varnish; + inherit (lib) + mkOption + types + mkDefault + optional + escapeShellArg + concatStringsSep + ; +in +{ + port = 9131; + extraOpts = { + noExit = mkOption { + type = types.bool; + default = false; + description = '' + Do not exit server on Varnish scrape errors. + ''; + }; + withGoMetrics = mkOption { + type = types.bool; + default = false; + description = '' + Export go runtime and http handler metrics. + ''; + }; + verbose = mkOption { + type = types.bool; + default = false; + description = '' + Enable verbose logging. + ''; + }; + raw = mkOption { + type = types.bool; + default = false; + description = '' + Enable raw stdout logging without timestamps. + ''; + }; + varnishStatPath = mkOption { + type = types.str; + default = "varnishstat"; + description = '' + Path to varnishstat. + ''; + }; + instance = mkOption { + type = types.nullOr types.str; + default = config.services.varnish.stateDir; + defaultText = lib.literalExpression "config.services.varnish.stateDir"; + description = '' + varnishstat -n value. + ''; + }; + healthPath = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Path under which to expose healthcheck. Disabled unless configured. + ''; + }; + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + }; + serviceOpts = { + path = [ config.services.varnish.package ]; + serviceConfig = { + RestartSec = mkDefault 1; + DynamicUser = false; + ExecStart = '' + ${pkgs.prometheus-varnish-exporter}/bin/prometheus_varnish_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path ${cfg.telemetryPath} \ + --varnishstat-path ${escapeShellArg cfg.varnishStatPath} \ + ${concatStringsSep " \\\n " (cfg.extraFlags + ++ optional (cfg.healthPath != null) "--web.health-path ${cfg.healthPath}" + ++ optional (cfg.instance != null) "-n ${escapeShellArg cfg.instance}" + ++ optional cfg.noExit "--no-exit" + ++ optional cfg.withGoMetrics "--with-go-metrics" + ++ optional cfg.verbose "--verbose" + ++ optional cfg.raw "--raw")} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix new file mode 100644 index 000000000000..7a48c836425f --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/wireguard.nix @@ -0,0 +1,77 @@ +{ config, lib, pkgs, options, ... }: + +let + cfg = config.services.prometheus.exporters.wireguard; + inherit (lib) + mkOption + types + mkRenamedOptionModule + mkEnableOption + optionalString + escapeShellArg + ; +in { + port = 9586; + imports = [ + (mkRenamedOptionModule [ "addr" ] [ "listenAddress" ]) + ({ options.warnings = options.warnings; options.assertions = options.assertions; }) + ]; + extraOpts = { + verbose = mkEnableOption "verbose logging mode for prometheus-wireguard-exporter"; + + wireguardConfig = mkOption { + type = with types; nullOr (either path str); + default = null; + + description = '' + Path to the Wireguard Config to + [add the peer's name to the stats of a peer](https://github.com/MindFlavor/prometheus_wireguard_exporter/tree/2.0.0#usage). + + Please note that `networking.wg-quick` is required for this feature + as `networking.wireguard` uses + {manpage}`wg(8)` + to set the peers up. + ''; + }; + + singleSubnetPerField = mkOption { + type = types.bool; + default = false; + description = '' + By default, all allowed IPs and subnets are comma-separated in the + `allowed_ips` field. With this option enabled, + a single IP and subnet will be listed in fields like `allowed_ip_0`, + `allowed_ip_1` and so on. + ''; + }; + + withRemoteIp = mkOption { + type = types.bool; + default = false; + description = '' + Whether or not the remote IP of a WireGuard peer should be exposed via prometheus. + ''; + }; + }; + serviceOpts = { + path = [ pkgs.wireguard-tools ]; + + serviceConfig = { + AmbientCapabilities = [ "CAP_NET_ADMIN" ]; + CapabilityBoundingSet = [ "CAP_NET_ADMIN" ]; + ExecStart = '' + ${pkgs.prometheus-wireguard-exporter}/bin/prometheus_wireguard_exporter \ + -p ${toString cfg.port} \ + -l ${cfg.listenAddress} \ + ${optionalString cfg.verbose "-v true"} \ + ${optionalString cfg.singleSubnetPerField "-s true"} \ + ${optionalString cfg.withRemoteIp "-r true"} \ + ${optionalString (cfg.wireguardConfig != null) "-n ${escapeShellArg cfg.wireguardConfig}"} + ''; + RestrictAddressFamilies = [ + # Need AF_NETLINK to collect data + "AF_NETLINK" + ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/zfs.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/zfs.nix new file mode 100644 index 000000000000..a685b94b827f --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/exporters/zfs.nix @@ -0,0 +1,49 @@ +{ config, lib, pkgs, options, ... }: + + +let + cfg = config.services.prometheus.exporters.zfs; + inherit (lib) + mkOption + types + concatStringsSep + concatMapStringsSep + ; +in +{ + port = 9134; + + extraOpts = { + telemetryPath = mkOption { + type = types.str; + default = "/metrics"; + description = '' + Path under which to expose metrics. + ''; + }; + + pools = mkOption { + type = with types; nullOr (listOf str); + default = [ ]; + description = '' + Name of the pool(s) to collect, repeat for multiple pools (default: all pools). + ''; + }; + }; + + serviceOpts = { + # needs zpool + path = [ config.boot.zfs.package ]; + serviceConfig = { + ExecStart = '' + ${pkgs.prometheus-zfs-exporter}/bin/zfs_exporter \ + --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \ + --web.telemetry-path ${cfg.telemetryPath} \ + ${concatMapStringsSep " " (x: "--pool=${x}") cfg.pools} \ + ${concatStringsSep " \\\n " cfg.extraFlags} + ''; + ProtectClock = false; + PrivateDevices = false; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/pushgateway.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/pushgateway.nix new file mode 100644 index 000000000000..80e2339f5925 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/pushgateway.nix @@ -0,0 +1,159 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.prometheus.pushgateway; + + cmdlineArgs = + opt "web.listen-address" cfg.web.listen-address + ++ opt "web.telemetry-path" cfg.web.telemetry-path + ++ opt "web.external-url" cfg.web.external-url + ++ opt "web.route-prefix" cfg.web.route-prefix + ++ optional cfg.persistMetrics ''--persistence.file="/var/lib/${cfg.stateDir}/metrics"'' + ++ opt "persistence.interval" cfg.persistence.interval + ++ opt "log.level" cfg.log.level + ++ opt "log.format" cfg.log.format + ++ cfg.extraFlags; + + opt = k : v : optional (v != null) ''--${k}="${v}"''; + +in { + options = { + services.prometheus.pushgateway = { + enable = mkEnableOption "Prometheus Pushgateway"; + + package = mkPackageOption pkgs "prometheus-pushgateway" { }; + + web.listen-address = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Address to listen on for the web interface, API and telemetry. + + `null` will default to `:9091`. + ''; + }; + + web.telemetry-path = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Path under which to expose metrics. + + `null` will default to `/metrics`. + ''; + }; + + web.external-url = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The URL under which Pushgateway is externally reachable. + ''; + }; + + web.route-prefix = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Prefix for the internal routes of web endpoints. + + Defaults to the path of + {option}`services.prometheus.pushgateway.web.external-url`. + ''; + }; + + persistence.interval = mkOption { + type = types.nullOr types.str; + default = null; + example = "10m"; + description = '' + The minimum interval at which to write out the persistence file. + + `null` will default to `5m`. + ''; + }; + + log.level = mkOption { + type = types.nullOr (types.enum ["debug" "info" "warn" "error" "fatal"]); + default = null; + description = '' + Only log messages with the given severity or above. + + `null` will default to `info`. + ''; + }; + + log.format = mkOption { + type = types.nullOr types.str; + default = null; + example = "logger:syslog?appname=bob&local=7"; + description = '' + Set the log target and format. + + `null` will default to `logger:stderr`. + ''; + }; + + extraFlags = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Extra commandline options when launching the Pushgateway. + ''; + }; + + persistMetrics = mkOption { + type = types.bool; + default = false; + description = '' + Whether to persist metrics to a file. + + When enabled metrics will be saved to a file called + `metrics` in the directory + `/var/lib/pushgateway`. The directory below + `/var/lib` can be set using + {option}`services.prometheus.pushgateway.stateDir`. + ''; + }; + + stateDir = mkOption { + type = types.str; + default = "pushgateway"; + description = '' + Directory below `/var/lib` to store metrics. + + This directory will be created automatically using systemd's + StateDirectory mechanism when + {option}`services.prometheus.pushgateway.persistMetrics` + is enabled. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = !hasPrefix "/" cfg.stateDir; + message = + "The option services.prometheus.pushgateway.stateDir" + + " shouldn't be an absolute directory." + + " It should be a directory relative to /var/lib."; + } + ]; + systemd.services.pushgateway = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + Restart = "always"; + DynamicUser = true; + ExecStart = "${cfg.package}/bin/pushgateway" + + optionalString (length cmdlineArgs != 0) (" \\\n " + + concatStringsSep " \\\n " cmdlineArgs); + StateDirectory = if cfg.persistMetrics then cfg.stateDir else null; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/sachet.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/sachet.nix new file mode 100644 index 000000000000..3deb29aeb222 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/sachet.nix @@ -0,0 +1,88 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.prometheus.sachet; + configFile = pkgs.writeText "sachet.yml" (builtins.toJSON cfg.configuration); +in +{ + options = { + services.prometheus.sachet = { + enable = mkEnableOption "Sachet, an SMS alerting tool for the Prometheus Alertmanager"; + + configuration = mkOption { + type = types.nullOr types.attrs; + default = null; + example = literalExpression '' + { + providers = { + twilio = { + # environment variables gets expanded at runtime + account_sid = "$TWILIO_ACCOUNT"; + auth_token = "$TWILIO_TOKEN"; + }; + }; + templates = [ ./some-template.tmpl ]; + receivers = [{ + name = "pager"; + provider = "twilio"; + to = [ "+33123456789" ]; + text = "{{ template \"message\" . }}"; + }]; + } + ''; + description = '' + Sachet's configuration as a nix attribute set. + ''; + }; + + address = mkOption { + type = types.str; + default = "localhost"; + description = '' + The address Sachet will listen to. + ''; + }; + + port = mkOption { + type = types.port; + default = 9876; + description = '' + The port Sachet will listen to. + ''; + }; + + }; + }; + + config = mkIf cfg.enable { + assertions = singleton { + assertion = cfg.configuration != null; + message = "Cannot enable Sachet without a configuration."; + }; + + systemd.services.sachet = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "network-online.target" ]; + script = '' + ${pkgs.envsubst}/bin/envsubst -i "${configFile}" > /tmp/sachet.yaml + exec ${pkgs.prometheus-sachet}/bin/sachet -config /tmp/sachet.yaml -listen-address ${cfg.address}:${builtins.toString cfg.port} + ''; + + serviceConfig = { + Restart = "always"; + + ProtectSystem = "strict"; + ProtectHome = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + + DynamicUser = true; + PrivateTmp = true; + WorkingDirectory = "/tmp/"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix b/nixpkgs/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix new file mode 100644 index 000000000000..f3f553cd8642 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/prometheus/xmpp-alerts.nix @@ -0,0 +1,55 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.prometheus.xmpp-alerts; + settingsFormat = pkgs.formats.yaml {}; + configFile = settingsFormat.generate "prometheus-xmpp-alerts.yml" cfg.settings; +in +{ + imports = [ + (mkRenamedOptionModule + [ "services" "prometheus" "xmpp-alerts" "configuration" ] + [ "services" "prometheus" "xmpp-alerts" "settings" ]) + ]; + + options.services.prometheus.xmpp-alerts = { + enable = mkEnableOption "XMPP Web hook service for Alertmanager"; + + settings = mkOption { + type = settingsFormat.type; + default = {}; + + description = '' + Configuration for prometheus xmpp-alerts, see + <https://github.com/jelmer/prometheus-xmpp-alerts/blob/master/xmpp-alerts.yml.example> + for supported values. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.services.prometheus-xmpp-alerts = { + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + ExecStart = "${pkgs.prometheus-xmpp-alerts}/bin/prometheus-xmpp-alerts --config ${configFile}"; + Restart = "on-failure"; + DynamicUser = true; + PrivateTmp = true; + PrivateDevices = true; + ProtectHome = true; + ProtectSystem = "strict"; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + NoNewPrivileges = true; + SystemCallArchitectures = "native"; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + SystemCallFilter = [ "@system-service" ]; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/riemann-dash.nix b/nixpkgs/nixos/modules/services/monitoring/riemann-dash.nix new file mode 100644 index 000000000000..243d0edb3aae --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/riemann-dash.nix @@ -0,0 +1,82 @@ +{ config, pkgs, lib, ... }: + +with pkgs; +with lib; + +let + + cfg = config.services.riemann-dash; + + conf = writeText "config.rb" '' + riemann_base = "${cfg.dataDir}" + config.store[:ws_config] = "#{riemann_base}/config/config.json" + ${cfg.config} + ''; + + launcher = writeScriptBin "riemann-dash" '' + #!/bin/sh + exec ${pkgs.riemann-dash}/bin/riemann-dash ${conf} + ''; + +in { + + options = { + + services.riemann-dash = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Enable the riemann-dash dashboard daemon. + ''; + }; + config = mkOption { + type = types.lines; + description = '' + Contents added to the end of the riemann-dash configuration file. + ''; + }; + dataDir = mkOption { + type = types.str; + default = "/var/riemann-dash"; + description = '' + Location of the riemann-base dir. The dashboard configuration file is + is stored to this directory. The directory is created automatically on + service start, and owner is set to the riemanndash user. + ''; + }; + }; + + }; + + config = mkIf cfg.enable { + + users.groups.riemanndash.gid = config.ids.gids.riemanndash; + + users.users.riemanndash = { + description = "riemann-dash daemon user"; + uid = config.ids.uids.riemanndash; + group = "riemanndash"; + }; + + systemd.tmpfiles.settings."10-riemanndash".${cfg.dataDir}.d = { + user = "riemanndash"; + group = "riemanndash"; + }; + + systemd.services.riemann-dash = { + wantedBy = [ "multi-user.target" ]; + wants = [ "riemann.service" ]; + after = [ "riemann.service" ]; + preStart = '' + mkdir -p '${cfg.dataDir}/config' + ''; + serviceConfig = { + User = "riemanndash"; + ExecStart = "${launcher}/bin/riemann-dash"; + }; + }; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/riemann-tools.nix b/nixpkgs/nixos/modules/services/monitoring/riemann-tools.nix new file mode 100644 index 000000000000..86a11694e7b4 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/riemann-tools.nix @@ -0,0 +1,70 @@ +{ config, pkgs, lib, ... }: + +with pkgs; +with lib; + +let + + cfg = config.services.riemann-tools; + + riemannHost = "${cfg.riemannHost}"; + + healthLauncher = writeScriptBin "riemann-health" '' + #!/bin/sh + exec ${pkgs.riemann-tools}/bin/riemann-health ${builtins.concatStringsSep " " cfg.extraArgs} --host ${riemannHost} + ''; + + +in { + + options = { + + services.riemann-tools = { + enableHealth = mkOption { + type = types.bool; + default = false; + description = '' + Enable the riemann-health daemon. + ''; + }; + riemannHost = mkOption { + type = types.str; + default = "127.0.0.1"; + description = '' + Address of the host riemann node. Defaults to localhost. + ''; + }; + extraArgs = mkOption { + type = types.listOf types.str; + default = []; + description = '' + A list of commandline-switches forwarded to a riemann-tool. + See for example `riemann-health --help` for available options. + ''; + example = ["-p 5555" "--timeout=30" "--attribute=myattribute=42"]; + }; + }; + }; + + config = mkIf cfg.enableHealth { + + users.groups.riemanntools.gid = config.ids.gids.riemanntools; + + users.users.riemanntools = { + description = "riemann-tools daemon user"; + uid = config.ids.uids.riemanntools; + group = "riemanntools"; + }; + + systemd.services.riemann-health = { + wantedBy = [ "multi-user.target" ]; + path = [ procps ]; + serviceConfig = { + User = "riemanntools"; + ExecStart = "${healthLauncher}/bin/riemann-health"; + }; + }; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/riemann.nix b/nixpkgs/nixos/modules/services/monitoring/riemann.nix new file mode 100644 index 000000000000..fd625e34e013 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/riemann.nix @@ -0,0 +1,100 @@ +{ config, pkgs, lib, ... }: + +with pkgs; +with lib; + +let + + cfg = config.services.riemann; + + classpath = concatStringsSep ":" ( + cfg.extraClasspathEntries ++ [ "${riemann}/share/java/riemann.jar" ] + ); + + riemannConfig = concatStringsSep "\n" ( + [cfg.config] ++ (map (f: ''(load-file "${f}")'') cfg.configFiles) + ); + + launcher = writeScriptBin "riemann" '' + #!/bin/sh + exec ${jdk}/bin/java ${concatStringsSep " " cfg.extraJavaOpts} \ + -cp ${classpath} \ + riemann.bin ${cfg.configFile} + ''; + +in { + + options = { + + services.riemann = { + enable = mkEnableOption "Riemann network monitoring daemon"; + + config = mkOption { + type = types.lines; + description = '' + Contents of the Riemann configuration file. For more complicated + config you should use configFile. + ''; + }; + configFiles = mkOption { + type = with types; listOf path; + default = []; + description = '' + Extra files containing Riemann configuration. These files will be + loaded at runtime by Riemann (with Clojure's + `load-file` function) at the end of the + configuration if you use the config option, this is ignored if you + use configFile. + ''; + }; + configFile = mkOption { + type = types.str; + description = '' + A Riemann config file. Any files in the same directory as this file + will be added to the classpath by Riemann. + ''; + }; + extraClasspathEntries = mkOption { + type = with types; listOf str; + default = []; + description = '' + Extra entries added to the Java classpath when running Riemann. + ''; + }; + extraJavaOpts = mkOption { + type = with types; listOf str; + default = []; + description = '' + Extra Java options used when launching Riemann. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + + users.groups.riemann.gid = config.ids.gids.riemann; + + users.users.riemann = { + description = "riemann daemon user"; + uid = config.ids.uids.riemann; + group = "riemann"; + }; + + services.riemann.configFile = mkDefault ( + writeText "riemann-config.clj" riemannConfig + ); + + systemd.services.riemann = { + wantedBy = [ "multi-user.target" ]; + path = [ inetutils ]; + serviceConfig = { + User = "riemann"; + ExecStart = "${launcher}/bin/riemann"; + }; + serviceConfig.LimitNOFILE = 65536; + }; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/rustdesk-server.nix b/nixpkgs/nixos/modules/services/monitoring/rustdesk-server.nix new file mode 100644 index 000000000000..21e6128c7226 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/rustdesk-server.nix @@ -0,0 +1,109 @@ +{ lib, pkgs, config, ... }: +let + TCPPorts = [21115 21116 21117 21118 21119]; + UDPPorts = [21116]; +in { + options.services.rustdesk-server = with lib; with types; { + enable = mkEnableOption "RustDesk, a remote access and remote control software, allowing maintenance of computers and other devices."; + + package = mkPackageOption pkgs "rustdesk-server" {}; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = '' + Open the connection ports. + TCP (${lib.concatStringsSep ", " (map toString TCPPorts)}) + UDP (${lib.concatStringsSep ", " (map toString UDPPorts)}) + ''; + }; + + relayIP = mkOption { + type = str; + description = '' + The public facing IP of the RustDesk relay. + ''; + }; + + extraSignalArgs = mkOption { + type = listOf str; + default = []; + example = [ "-k" "_" ]; + description = '' + A list of extra command line arguments to pass to the `hbbs` process. + ''; + }; + + extraRelayArgs = mkOption { + type = listOf str; + default = []; + example = [ "-k" "_" ]; + description = '' + A list of extra command line arguments to pass to the `hbbr` process. + ''; + }; + }; + + config = let + cfg = config.services.rustdesk-server; + serviceDefaults = { + enable = true; + requiredBy = [ "rustdesk.target" ]; + serviceConfig = { + Slice = "system-rustdesk.slice"; + User = "rustdesk"; + Group = "rustdesk"; + DynamicUser = "yes"; + Environment = []; + WorkingDirectory = "/var/lib/rustdesk"; + StateDirectory = "rustdesk"; + StateDirectoryMode = "0750"; + LockPersonality = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RestrictNamespaces = true; + }; + }; + in lib.mkIf cfg.enable { + users.users.rustdesk = { + description = "System user for RustDesk"; + isSystemUser = true; + group = "rustdesk"; + }; + users.groups.rustdesk = {}; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall TCPPorts; + networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall UDPPorts; + + systemd.slices.system-rustdesk = { + enable = true; + description = "Slice designed to contain RustDesk Signal & RustDesk Relay"; + }; + + systemd.targets.rustdesk = { + enable = true; + description = "Target designed to group RustDesk Signal & RustDesk Relay"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + }; + + systemd.services.rustdesk-signal = lib.mkMerge [ serviceDefaults { + serviceConfig.ExecStart = "${cfg.package}/bin/hbbs -r ${cfg.relayIP} ${lib.escapeShellArgs cfg.extraSignalArgs}"; + } ]; + + systemd.services.rustdesk-relay = lib.mkMerge [ serviceDefaults { + serviceConfig.ExecStart = "${cfg.package}/bin/hbbr ${lib.escapeShellArgs cfg.extraRelayArgs}"; + } ]; + }; + + meta.maintainers = with lib.maintainers; [ ppom ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/scollector.nix b/nixpkgs/nixos/modules/services/monitoring/scollector.nix new file mode 100644 index 000000000000..49c3788e086f --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/scollector.nix @@ -0,0 +1,127 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.scollector; + + collectors = pkgs.runCommand "collectors" { preferLocalBuild = true; } + '' + mkdir -p $out + ${lib.concatStringsSep + "\n" + (lib.mapAttrsToList + (frequency: binaries: + "mkdir -p $out/${frequency}\n" + + (lib.concatStringsSep + "\n" + (map (path: "ln -s ${path} $out/${frequency}/$(basename ${path})") + binaries))) + cfg.collectors)} + ''; + + conf = pkgs.writeText "scollector.toml" '' + Host = "${cfg.bosunHost}" + ColDir = "${collectors}" + ${cfg.extraConfig} + ''; + +in { + + options = { + + services.scollector = { + + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to run scollector. + ''; + }; + + package = mkPackageOption pkgs "scollector" { }; + + user = mkOption { + type = types.str; + default = "scollector"; + description = '' + User account under which scollector runs. + ''; + }; + + group = mkOption { + type = types.str; + default = "scollector"; + description = '' + Group account under which scollector runs. + ''; + }; + + bosunHost = mkOption { + type = types.str; + default = "localhost:8070"; + description = '' + Host and port of the bosun server that will store the collected + data. + ''; + }; + + collectors = mkOption { + type = with types; attrsOf (listOf path); + default = {}; + example = literalExpression ''{ "0" = [ "''${postgresStats}/bin/collect-stats" ]; }''; + description = '' + An attribute set mapping the frequency of collection to a list of + binaries that should be executed at that frequency. You can use "0" + to run a binary forever. + ''; + }; + + extraOpts = mkOption { + type = with types; listOf str; + default = []; + example = [ "-d" ]; + description = '' + Extra scollector command line options + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra scollector configuration added to the end of scollector.toml + ''; + }; + + }; + + }; + + config = mkIf config.services.scollector.enable { + + systemd.services.scollector = { + description = "scollector metrics collector (part of Bosun)"; + wantedBy = [ "multi-user.target" ]; + + path = [ pkgs.coreutils pkgs.iproute2 ]; + + serviceConfig = { + User = cfg.user; + Group = cfg.group; + ExecStart = "${cfg.package}/bin/scollector -conf=${conf} ${lib.concatStringsSep " " cfg.extraOpts}"; + }; + }; + + users.users.scollector = { + description = "scollector user"; + group = "scollector"; + uid = config.ids.uids.scollector; + }; + + users.groups.scollector.gid = config.ids.gids.scollector; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/scrutiny.nix b/nixpkgs/nixos/modules/services/monitoring/scrutiny.nix new file mode 100644 index 000000000000..031f5a30cada --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/scrutiny.nix @@ -0,0 +1,217 @@ +{ config, lib, pkgs, ... }: +let + inherit (lib) maintainers; + inherit (lib.meta) getExe; + inherit (lib.modules) mkIf mkMerge; + inherit (lib.options) literalExpression mkEnableOption mkOption mkPackageOption; + inherit (lib.types) bool enum nullOr port str submodule; + + cfg = config.services.scrutiny; + # Define the settings format used for this program + settingsFormat = pkgs.formats.yaml { }; +in +{ + options = { + services.scrutiny = { + enable = mkEnableOption "Scrutiny, a web application for drive monitoring"; + + package = mkPackageOption pkgs "scrutiny" { }; + + openFirewall = mkEnableOption "opening the default ports in the firewall for Scrutiny"; + + influxdb.enable = mkOption { + type = bool; + default = true; + description = '' + Enables InfluxDB on the host system using the `services.influxdb2` NixOS module + with default options. + + If you already have InfluxDB configured, or wish to connect to an external InfluxDB + instance, disable this option. + ''; + }; + + settings = mkOption { + description = '' + Scrutiny settings to be rendered into the configuration file. + + See https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml. + ''; + default = { }; + type = submodule { + freeformType = settingsFormat.type; + + options.web.listen.port = mkOption { + type = port; + default = 8080; + description = "Port for web application to listen on."; + }; + + options.web.listen.host = mkOption { + type = str; + default = "0.0.0.0"; + description = "Interface address for web application to bind to."; + }; + + options.web.listen.basepath = mkOption { + type = str; + default = ""; + example = "/scrutiny"; + description = '' + If Scrutiny will be behind a path prefixed reverse proxy, you can override this + value to serve Scrutiny on a subpath. + ''; + }; + + options.log.level = mkOption { + type = enum [ "INFO" "DEBUG" ]; + default = "INFO"; + description = "Log level for Scrutiny."; + }; + + options.web.influxdb.scheme = mkOption { + type = str; + default = "http"; + description = "URL scheme to use when connecting to InfluxDB."; + }; + + options.web.influxdb.host = mkOption { + type = str; + default = "0.0.0.0"; + description = "IP or hostname of the InfluxDB instance."; + }; + + options.web.influxdb.port = mkOption { + type = port; + default = 8086; + description = "The port of the InfluxDB instance."; + }; + + options.web.influxdb.tls.insecure_skip_verify = mkEnableOption "skipping TLS verification when connecting to InfluxDB"; + + options.web.influxdb.token = mkOption { + type = nullOr str; + default = null; + description = "Authentication token for connecting to InfluxDB."; + }; + + options.web.influxdb.org = mkOption { + type = nullOr str; + default = null; + description = "InfluxDB organisation under which to store data."; + }; + + options.web.influxdb.bucket = mkOption { + type = nullOr str; + default = null; + description = "InfluxDB bucket in which to store data."; + }; + }; + }; + + collector = { + enable = mkEnableOption "the Scrutiny metrics collector"; + + package = mkPackageOption pkgs "scrutiny-collector" { }; + + schedule = mkOption { + type = str; + default = "*:0/15"; + description = '' + How often to run the collector in systemd calendar format. + ''; + }; + + settings = mkOption { + description = '' + Collector settings to be rendered into the collector configuration file. + + See https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml. + ''; + default = { }; + type = submodule { + freeformType = settingsFormat.type; + + options.host.id = mkOption { + type = nullOr str; + default = null; + description = "Host ID for identifying/labelling groups of disks"; + }; + + options.api.endpoint = mkOption { + type = str; + default = "http://localhost:${toString cfg.settings.web.listen.port}"; + defaultText = literalExpression ''"http://localhost:''${config.services.scrutiny.settings.web.listen.port}"''; + description = "Scrutiny app API endpoint for sending metrics to."; + }; + + options.log.level = mkOption { + type = enum [ "INFO" "DEBUG" ]; + default = "INFO"; + description = "Log level for Scrutiny collector."; + }; + }; + }; + }; + }; + }; + + config = mkMerge [ + (mkIf cfg.enable { + services.influxdb2.enable = cfg.influxdb.enable; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.settings.web.listen.port ]; + }; + + systemd.services.scrutiny = { + description = "Hard Drive S.M.A.R.T Monitoring, Historical Trends & Real World Failure Thresholds"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ] ++ lib.optional cfg.influxdb.enable "influxdb2.service"; + wants = lib.optional cfg.influxdb.enable "influxdb2.service"; + environment = { + SCRUTINY_VERSION = "1"; + SCRUTINY_WEB_DATABASE_LOCATION = "/var/lib/scrutiny/scrutiny.db"; + SCRUTINY_WEB_SRC_FRONTEND_PATH = "${cfg.package}/share/scrutiny"; + }; + serviceConfig = { + DynamicUser = true; + ExecStart = "${getExe cfg.package} start --config ${settingsFormat.generate "scrutiny.yaml" cfg.settings}"; + Restart = "always"; + StateDirectory = "scrutiny"; + StateDirectoryMode = "0750"; + }; + }; + }) + (mkIf cfg.collector.enable { + services.smartd = { + enable = true; + extraOptions = [ + "-A /var/log/smartd/" + "--interval=600" + ]; + }; + + systemd = { + services.scrutiny-collector = { + description = "Scrutiny Collector Service"; + after = lib.optional cfg.enable "scrutiny.service"; + wants = lib.optional cfg.enable "scrutiny.service"; + environment = { + COLLECTOR_VERSION = "1"; + COLLECTOR_API_ENDPOINT = cfg.collector.settings.api.endpoint; + }; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${getExe cfg.collector.package} run --config ${settingsFormat.generate "scrutiny-collector.yaml" cfg.collector.settings}"; + }; + startAt = cfg.collector.schedule; + }; + + timers.scrutiny-collector.timerConfig.Persistent = true; + }; + }) + ]; + + meta.maintainers = [ maintainers.jnsgruk ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/smartd.nix b/nixpkgs/nixos/modules/services/monitoring/smartd.nix new file mode 100644 index 000000000000..2c05eaad25ac --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/smartd.nix @@ -0,0 +1,252 @@ +{ config, lib, options, pkgs, ... }: + +with lib; + +let + + host = config.networking.fqdnOrHostName; + + cfg = config.services.smartd; + opt = options.services.smartd; + + nm = cfg.notifications.mail; + nw = cfg.notifications.wall; + nx = cfg.notifications.x11; + + smartdNotify = pkgs.writeScript "smartd-notify.sh" '' + #! ${pkgs.runtimeShell} + ${optionalString nm.enable '' + { + ${pkgs.coreutils}/bin/cat << EOF + From: smartd on ${host} <${nm.sender}> + To: ${nm.recipient} + Subject: $SMARTD_SUBJECT + + $SMARTD_FULLMESSAGE + EOF + + ${pkgs.smartmontools}/sbin/smartctl -a -d "$SMARTD_DEVICETYPE" "$SMARTD_DEVICE" + } | ${nm.mailer} -i "${nm.recipient}" + ''} + ${optionalString nw.enable '' + { + ${pkgs.coreutils}/bin/cat << EOF + Problem detected with disk: $SMARTD_DEVICESTRING + Warning message from smartd is: + + $SMARTD_MESSAGE + EOF + } | ${pkgs.util-linux}/bin/wall 2>/dev/null + ''} + ${optionalString nx.enable '' + export DISPLAY=${nx.display} + { + ${pkgs.coreutils}/bin/cat << EOF + Problem detected with disk: $SMARTD_DEVICESTRING + Warning message from smartd is: + + $SMARTD_FULLMESSAGE + EOF + } | ${pkgs.xorg.xmessage}/bin/xmessage -file - 2>/dev/null & + ''} + ''; + + notifyOpts = optionalString (nm.enable || nw.enable || nx.enable) + ("-m <nomailer> -M exec ${smartdNotify} " + optionalString cfg.notifications.test "-M test "); + + smartdConf = pkgs.writeText "smartd.conf" '' + # Autogenerated smartd startup config file + DEFAULT ${notifyOpts}${cfg.defaults.monitored} + + ${concatMapStringsSep "\n" (d: "${d.device} ${d.options}") cfg.devices} + + ${optionalString cfg.autodetect + "DEVICESCAN ${notifyOpts}${cfg.defaults.autodetected}"} + ''; + + smartdDeviceOpts = { ... }: { + + options = { + + device = mkOption { + example = "/dev/sda"; + type = types.str; + description = "Location of the device."; + }; + + options = mkOption { + default = ""; + example = "-d sat"; + type = types.separatedString " "; + description = "Options that determine how smartd monitors the device."; + }; + + }; + + }; + +in + +{ + ###### interface + + options = { + + services.smartd = { + + enable = mkEnableOption "smartd daemon from `smartmontools` package"; + + autodetect = mkOption { + default = true; + type = types.bool; + description = '' + Whenever smartd should monitor all devices connected to the + machine at the time it's being started (the default). + + Set to false to monitor the devices listed in + {option}`services.smartd.devices` only. + ''; + }; + + extraOptions = mkOption { + default = []; + type = types.listOf types.str; + example = ["-A /var/log/smartd/" "--interval=3600"]; + description = '' + Extra command-line options passed to the `smartd` + daemon on startup. + + (See `man 8 smartd`.) + ''; + }; + + notifications = { + + mail = { + enable = mkOption { + default = config.services.mail.sendmailSetuidWrapper != null; + defaultText = literalExpression "config.services.mail.sendmailSetuidWrapper != null"; + type = types.bool; + description = "Whenever to send e-mail notifications."; + }; + + sender = mkOption { + default = "root"; + example = "example@domain.tld"; + type = types.str; + description = '' + Sender of the notification messages. + Acts as the value of `email` in the emails' `From: ...` field. + ''; + }; + + recipient = mkOption { + default = "root"; + type = types.str; + description = "Recipient of the notification messages."; + }; + + mailer = mkOption { + default = "/run/wrappers/bin/sendmail"; + type = types.path; + description = '' + Sendmail-compatible binary to be used to send the messages. + + You should probably enable + {option}`services.postfix` or some other MTA for + this to work. + ''; + }; + }; + + wall = { + enable = mkOption { + default = true; + type = types.bool; + description = "Whenever to send wall notifications to all users."; + }; + }; + + x11 = { + enable = mkOption { + default = config.services.xserver.enable; + defaultText = literalExpression "config.services.xserver.enable"; + type = types.bool; + description = "Whenever to send X11 xmessage notifications."; + }; + + display = mkOption { + default = ":${toString config.services.xserver.display}"; + defaultText = literalExpression ''":''${toString config.services.xserver.display}"''; + type = types.str; + description = "DISPLAY to send X11 notifications to."; + }; + }; + + test = mkOption { + default = false; + type = types.bool; + description = "Whenever to send a test notification on startup."; + }; + + }; + + defaults = { + monitored = mkOption { + default = "-a"; + type = types.separatedString " "; + example = "-a -o on -s (S/../.././02|L/../../7/04)"; + description = '' + Common default options for explicitly monitored (listed in + {option}`services.smartd.devices`) devices. + + The default value turns on monitoring of all the things (see + `man 5 smartd.conf`). + + The example also turns on SMART Automatic Offline Testing on + startup, and schedules short self-tests daily, and long + self-tests weekly. + ''; + }; + + autodetected = mkOption { + default = cfg.defaults.monitored; + defaultText = literalExpression "config.${opt.defaults.monitored}"; + type = types.separatedString " "; + description = '' + Like {option}`services.smartd.defaults.monitored`, but for the + autodetected devices. + ''; + }; + }; + + devices = mkOption { + default = []; + example = [ { device = "/dev/sda"; } { device = "/dev/sdb"; options = "-d sat"; } ]; + type = with types; listOf (submodule smartdDeviceOpts); + description = "List of devices to monitor."; + }; + + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + + assertions = [ { + assertion = cfg.autodetect || cfg.devices != []; + message = "smartd can't run with both disabled autodetect and an empty list of devices to monitor."; + } ]; + + systemd.services.smartd = { + description = "S.M.A.R.T. Daemon"; + wantedBy = [ "multi-user.target" ]; + serviceConfig.ExecStart = "${pkgs.smartmontools}/sbin/smartd ${lib.concatStringsSep " " cfg.extraOptions} --no-fork --configfile=${smartdConf}"; + }; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/snmpd.nix b/nixpkgs/nixos/modules/services/monitoring/snmpd.nix new file mode 100644 index 000000000000..bff9ce3fbc88 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/snmpd.nix @@ -0,0 +1,83 @@ +{ pkgs, config, lib, ... }: + +let + cfg = config.services.snmpd; + configFile = if cfg.configText != "" then + pkgs.writeText "snmpd.cfg" '' + ${cfg.configText} + '' else null; +in { + options.services.snmpd = { + enable = lib.mkEnableOption "snmpd"; + + package = lib.mkPackageOption pkgs "net-snmp" {}; + + listenAddress = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = '' + The address to listen on for SNMP and AgentX messages. + ''; + example = "127.0.0.1"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 161; + description = '' + The port to listen on for SNMP and AgentX messages. + ''; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Open port in firewall for snmpd. + ''; + }; + + configText = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + The contents of the snmpd.conf. If the {option}`configFile` option + is set, this value will be ignored. + + Note that the contents of this option will be added to the Nix + store as world-readable plain text, {option}`configFile` can be used in + addition to a secret management tool to protect sensitive data. + ''; + }; + + configFile = lib.mkOption { + type = lib.types.path; + default = configFile; + defaultText = lib.literalMD "The value of {option}`configText`."; + description = '' + Path to the snmpd.conf file. By default, if {option}`configText` is set, + a config file will be automatically generated. + ''; + }; + + }; + + config = lib.mkIf cfg.enable { + systemd.services."snmpd" = { + description = "Simple Network Management Protocol (SNMP) daemon."; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${lib.getExe' cfg.package "snmpd"} -f -Lo -c ${cfg.configFile} ${cfg.listenAddress}:${toString cfg.port}"; + }; + }; + + networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [ + cfg.port + ]; + }; + + meta.maintainers = [ lib.maintainers.eliandoran ]; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/statsd.nix b/nixpkgs/nixos/modules/services/monitoring/statsd.nix new file mode 100644 index 000000000000..30b2916a9928 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/statsd.nix @@ -0,0 +1,149 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.statsd; + + isBuiltinBackend = name: + builtins.elem name [ "graphite" "console" "repeater" ]; + + backendsToPackages = let + mkMap = list: name: + if isBuiltinBackend name then list + else list ++ [ pkgs.nodePackages.${name} ]; + in foldl mkMap []; + + configFile = pkgs.writeText "statsd.conf" '' + { + address: "${cfg.listenAddress}", + port: "${toString cfg.port}", + mgmt_address: "${cfg.mgmt_address}", + mgmt_port: "${toString cfg.mgmt_port}", + backends: [${ + concatMapStringsSep "," (name: + if (isBuiltinBackend name) + then ''"./backends/${name}"'' + else ''"${name}"'' + ) cfg.backends}], + ${optionalString (cfg.graphiteHost!=null) ''graphiteHost: "${cfg.graphiteHost}",''} + ${optionalString (cfg.graphitePort!=null) ''graphitePort: "${toString cfg.graphitePort}",''} + console: { + prettyprint: false + }, + log: { + backend: "stdout" + }, + automaticConfigReload: false${optionalString (cfg.extraConfig != null) ","} + ${cfg.extraConfig} + } + ''; + + deps = pkgs.buildEnv { + name = "statsd-runtime-deps"; + pathsToLink = [ "/lib" ]; + ignoreCollisions = true; + + paths = backendsToPackages cfg.backends; + }; + +in + +{ + + ###### interface + + options.services.statsd = { + + enable = mkEnableOption "statsd"; + + listenAddress = mkOption { + description = "Address that statsd listens on over UDP"; + default = "127.0.0.1"; + type = types.str; + }; + + port = mkOption { + description = "Port that stats listens for messages on over UDP"; + default = 8125; + type = types.int; + }; + + mgmt_address = mkOption { + description = "Address to run management TCP interface on"; + default = "127.0.0.1"; + type = types.str; + }; + + mgmt_port = mkOption { + description = "Port to run the management TCP interface on"; + default = 8126; + type = types.int; + }; + + backends = mkOption { + description = "List of backends statsd will use for data persistence"; + default = []; + example = [ + "graphite" + "console" + "repeater" + "statsd-librato-backend" + "stackdriver-statsd-backend" + "statsd-influxdb-backend" + ]; + type = types.listOf types.str; + }; + + graphiteHost = mkOption { + description = "Hostname or IP of Graphite server"; + default = null; + type = types.nullOr types.str; + }; + + graphitePort = mkOption { + description = "Port of Graphite server (i.e. carbon-cache)."; + default = null; + type = types.nullOr types.int; + }; + + extraConfig = mkOption { + description = "Extra configuration options for statsd"; + default = ""; + type = types.nullOr types.str; + }; + + }; + + ###### implementation + + config = mkIf cfg.enable { + + assertions = map (backend: { + assertion = !isBuiltinBackend backend -> hasAttrByPath [ backend ] pkgs.nodePackages; + message = "Only builtin backends (graphite, console, repeater) or backends enumerated in `pkgs.nodePackages` are allowed!"; + }) cfg.backends; + + users.users.statsd = { + uid = config.ids.uids.statsd; + description = "Statsd daemon user"; + }; + + systemd.services.statsd = { + description = "Statsd Server"; + wantedBy = [ "multi-user.target" ]; + environment = { + NODE_PATH = "${deps}/lib/node_modules"; + }; + serviceConfig = { + ExecStart = "${pkgs.statsd}/bin/statsd ${configFile}"; + User = "statsd"; + }; + }; + + environment.systemPackages = [ pkgs.statsd ]; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/sysstat.nix b/nixpkgs/nixos/modules/services/monitoring/sysstat.nix new file mode 100644 index 000000000000..ca2cff827232 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/sysstat.nix @@ -0,0 +1,76 @@ +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.services.sysstat; +in { + options = { + services.sysstat = { + enable = mkEnableOption "sar system activity collection"; + + collect-frequency = mkOption { + type = types.str; + default = "*:00/10"; + description = '' + OnCalendar specification for sysstat-collect + ''; + }; + + collect-args = mkOption { + type = types.str; + default = "1 1"; + description = '' + Arguments to pass sa1 when collecting statistics + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.sysstat = { + description = "Resets System Activity Logs"; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + User = "root"; + RemainAfterExit = true; + Type = "oneshot"; + ExecStart = "${pkgs.sysstat}/lib/sa/sa1 --boot"; + LogsDirectory = "sa"; + }; + }; + + systemd.services.sysstat-collect = { + description = "system activity accounting tool"; + unitConfig.Documentation = "man:sa1(8)"; + + serviceConfig = { + Type = "oneshot"; + User = "root"; + ExecStart = "${pkgs.sysstat}/lib/sa/sa1 ${cfg.collect-args}"; + }; + }; + + systemd.timers.sysstat-collect = { + description = "Run system activity accounting tool on a regular basis"; + wantedBy = [ "timers.target" ]; + timerConfig.OnCalendar = cfg.collect-frequency; + }; + + systemd.services.sysstat-summary = { + description = "Generate a daily summary of process accounting"; + unitConfig.Documentation = "man:sa2(8)"; + + serviceConfig = { + Type = "oneshot"; + User = "root"; + ExecStart = "${pkgs.sysstat}/lib/sa/sa2 -A"; + }; + }; + + systemd.timers.sysstat-summary = { + description = "Generate summary of yesterday's process accounting"; + wantedBy = [ "timers.target" ]; + timerConfig.OnCalendar = "00:07:00"; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/teamviewer.nix b/nixpkgs/nixos/modules/services/monitoring/teamviewer.nix new file mode 100644 index 000000000000..360cdd1c6b6a --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/teamviewer.nix @@ -0,0 +1,50 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.teamviewer; + +in + +{ + + ###### interface + + options = { + + services.teamviewer.enable = mkEnableOption "TeamViewer daemon"; + + }; + + ###### implementation + + config = mkIf (cfg.enable) { + + environment.systemPackages = [ pkgs.teamviewer ]; + + services.dbus.packages = [ pkgs.teamviewer ]; + + systemd.services.teamviewerd = { + description = "TeamViewer remote control daemon"; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" "network.target" "dbus.service" ]; + requires = [ "dbus.service" ]; + preStart = "mkdir -pv /var/lib/teamviewer /var/log/teamviewer"; + + startLimitIntervalSec = 60; + startLimitBurst = 10; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.teamviewer}/bin/teamviewerd -f"; + PIDFile = "/run/teamviewerd.pid"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + Restart = "on-abort"; + }; + }; + }; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/telegraf.nix b/nixpkgs/nixos/modules/services/monitoring/telegraf.nix new file mode 100644 index 000000000000..8c51a2838e05 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/telegraf.nix @@ -0,0 +1,91 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.telegraf; + + settingsFormat = pkgs.formats.toml {}; + configFile = settingsFormat.generate "config.toml" cfg.extraConfig; +in { + ###### interface + options = { + services.telegraf = { + enable = mkEnableOption "telegraf server"; + + package = mkPackageOption pkgs "telegraf" { }; + + environmentFiles = mkOption { + type = types.listOf types.path; + default = []; + example = [ "/run/keys/telegraf.env" ]; + description = '' + File to load as environment file. Environment variables from this file + will be interpolated into the config file using envsubst with this + syntax: `$ENVIRONMENT` or `''${VARIABLE}`. + This is useful to avoid putting secrets into the nix store. + ''; + }; + + extraConfig = mkOption { + default = {}; + description = "Extra configuration options for telegraf"; + type = settingsFormat.type; + example = { + outputs.influxdb = { + urls = ["http://localhost:8086"]; + database = "telegraf"; + }; + inputs.statsd = { + service_address = ":8125"; + delete_timings = true; + }; + }; + }; + }; + }; + + + ###### implementation + config = mkIf config.services.telegraf.enable { + services.telegraf.extraConfig = { + inputs = {}; + outputs = {}; + }; + systemd.services.telegraf = let + finalConfigFile = if config.services.telegraf.environmentFiles == [] + then configFile + else "/var/run/telegraf/config.toml"; + in { + description = "Telegraf Agent"; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + path = lib.optional (config.services.telegraf.extraConfig.inputs ? procstat) pkgs.procps; + serviceConfig = { + EnvironmentFile = config.services.telegraf.environmentFiles; + ExecStartPre = lib.optional (config.services.telegraf.environmentFiles != []) + (pkgs.writeShellScript "pre-start" '' + umask 077 + ${pkgs.envsubst}/bin/envsubst -i "${configFile}" > /var/run/telegraf/config.toml + ''); + ExecStart="${cfg.package}/bin/telegraf -config ${finalConfigFile}"; + ExecReload="${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + RuntimeDirectory = "telegraf"; + User = "telegraf"; + Group = "telegraf"; + Restart = "on-failure"; + # for ping probes + AmbientCapabilities = [ "CAP_NET_RAW" ]; + }; + }; + + users.users.telegraf = { + uid = config.ids.uids.telegraf; + group = "telegraf"; + description = "telegraf daemon user"; + }; + + users.groups.telegraf = {}; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/thanos.nix b/nixpkgs/nixos/modules/services/monitoring/thanos.nix new file mode 100644 index 000000000000..f4cec0a545cb --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/thanos.nix @@ -0,0 +1,881 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) + collect + concatLists + concatStringsSep + flip + getAttrFromPath + hasPrefix + isList + length + literalExpression + literalMD + mapAttrsRecursiveCond + mapAttrsToList + mkEnableOption + mkIf + mkMerge + mkOption + mkPackageOption + optional + optionalAttrs + optionalString + types + ; + + cfg = config.services.thanos; + + nullOpt = type: description: mkOption { + type = types.nullOr type; + default = null; + description = description; + }; + + optionToArgs = opt: v : optional (v != null) ''--${opt}="${toString v}"''; + flagToArgs = opt: v : optional v "--${opt}"; + listToArgs = opt: vs : map (v: ''--${opt}="${v}"'') vs; + attrsToArgs = opt: kvs: mapAttrsToList (k: v: ''--${opt}=${k}=\"${v}\"'') kvs; + + mkParamDef = type: default: description: mkParam type (description + '' + + Defaults to `${toString default}` in Thanos + when set to `null`. + ''); + + mkParam = type: description: { + toArgs = optionToArgs; + option = nullOpt type description; + }; + + mkFlagParam = description: { + toArgs = flagToArgs; + option = mkOption { + type = types.bool; + default = false; + description = description; + }; + }; + + mkListParam = opt: description: { + toArgs = _opt: listToArgs opt; + option = mkOption { + type = types.listOf types.str; + default = []; + description = description; + }; + }; + + mkAttrsParam = opt: description: { + toArgs = _opt: attrsToArgs opt; + option = mkOption { + type = types.attrsOf types.str; + default = {}; + description = description; + }; + }; + + mkStateDirParam = opt: default: description: { + toArgs = _opt: stateDir: optionToArgs opt "/var/lib/${stateDir}"; + option = mkOption { + type = types.str; + inherit default; + description = description; + }; + }; + + toYAML = name: attrs: pkgs.runCommand name { + preferLocalBuild = true; + json = builtins.toFile "${name}.json" (builtins.toJSON attrs); + nativeBuildInputs = [ pkgs.remarshal ]; + } "json2yaml -i $json -o $out"; + + thanos = cmd: "${cfg.package}/bin/thanos ${cmd}" + + (let args = cfg.${cmd}.arguments; + in optionalString (length args != 0) (" \\\n " + + concatStringsSep " \\\n " args)); + + argumentsOf = cmd: concatLists (collect isList + (flip mapParamsRecursive params.${cmd} (path: param: + let opt = concatStringsSep "." path; + v = getAttrFromPath path cfg.${cmd}; + in param.toArgs opt v))); + + mkArgumentsOption = cmd: mkOption { + type = types.listOf types.str; + default = argumentsOf cmd; + defaultText = literalMD '' + calculated from `config.services.thanos.${cmd}` + ''; + description = '' + Arguments to the `thanos ${cmd}` command. + + Defaults to a list of arguments formed by converting the structured + options of {option}`services.thanos.${cmd}` to a list of arguments. + + Overriding this option will cause none of the structured options to have + any effect. So only set this if you know what you're doing! + ''; + }; + + mapParamsRecursive = + let noParam = attr: !(attr ? toArgs && attr ? option); + in mapAttrsRecursiveCond noParam; + + paramsToOptions = mapParamsRecursive (_path: param: param.option); + + params = { + + log = { + + log.level = mkParamDef (types.enum ["debug" "info" "warn" "error" "fatal"]) "info" '' + Log filtering level. + ''; + + log.format = mkParam types.str '' + Log format to use. + ''; + }; + + tracing = cfg: { + tracing.config-file = { + toArgs = _opt: path: optionToArgs "tracing.config-file" path; + option = mkOption { + type = with types; nullOr str; + default = if cfg.tracing.config == null then null + else toString (toYAML "tracing.yaml" cfg.tracing.config); + defaultText = literalExpression '' + if config.services.thanos.<cmd>.tracing.config == null then null + else toString (toYAML "tracing.yaml" config.services.thanos.<cmd>.tracing.config); + ''; + description = '' + Path to YAML file that contains tracing configuration. + + See format details: <https://thanos.io/tip/thanos/tracing.md/#configuration> + ''; + }; + }; + + tracing.config = + { + toArgs = _opt: _attrs: []; + option = nullOpt types.attrs '' + Tracing configuration. + + When not `null` the attribute set gets converted to + a YAML file and stored in the Nix store. The option + {option}`tracing.config-file` will default to its path. + + If {option}`tracing.config-file` is set this option has no effect. + + See format details: <https://thanos.io/tip/thanos/tracing.md/#configuration> + ''; + }; + }; + + common = cfg: params.log // params.tracing cfg // { + + http-address = mkParamDef types.str "0.0.0.0:10902" '' + Listen `host:port` for HTTP endpoints. + ''; + + grpc-address = mkParamDef types.str "0.0.0.0:10901" '' + Listen `ip:port` address for gRPC endpoints (StoreAPI). + + Make sure this address is routable from other components. + ''; + + grpc-server-tls-cert = mkParam types.str '' + TLS Certificate for gRPC server, leave blank to disable TLS + ''; + + grpc-server-tls-key = mkParam types.str '' + TLS Key for the gRPC server, leave blank to disable TLS + ''; + + grpc-server-tls-client-ca = mkParam types.str '' + TLS CA to verify clients against. + + If no client CA is specified, there is no client verification on server side. + (tls.NoClientCert) + ''; + }; + + objstore = cfg: { + + objstore.config-file = { + toArgs = _opt: path: optionToArgs "objstore.config-file" path; + option = mkOption { + type = with types; nullOr str; + default = if cfg.objstore.config == null then null + else toString (toYAML "objstore.yaml" cfg.objstore.config); + defaultText = literalExpression '' + if config.services.thanos.<cmd>.objstore.config == null then null + else toString (toYAML "objstore.yaml" config.services.thanos.<cmd>.objstore.config); + ''; + description = '' + Path to YAML file that contains object store configuration. + + See format details: <https://thanos.io/tip/thanos/storage.md/#configuring-access-to-object-storage> + ''; + }; + }; + + objstore.config = + { + toArgs = _opt: _attrs: []; + option = nullOpt types.attrs '' + Object store configuration. + + When not `null` the attribute set gets converted to + a YAML file and stored in the Nix store. The option + {option}`objstore.config-file` will default to its path. + + If {option}`objstore.config-file` is set this option has no effect. + + See format details: <https://thanos.io/tip/thanos/storage.md/#configuring-access-to-object-storage> + ''; + }; + }; + + sidecar = params.common cfg.sidecar // params.objstore cfg.sidecar // { + + prometheus.url = mkParamDef types.str "http://localhost:9090" '' + URL at which to reach Prometheus's API. + + For better performance use local network. + ''; + + tsdb.path = { + toArgs = optionToArgs; + option = mkOption { + type = types.str; + default = "/var/lib/${config.services.prometheus.stateDir}/data"; + defaultText = literalExpression ''"/var/lib/''${config.services.prometheus.stateDir}/data"''; + description = '' + Data directory of TSDB. + ''; + }; + }; + + reloader.config-file = mkParam types.str '' + Config file watched by the reloader. + ''; + + reloader.config-envsubst-file = mkParam types.str '' + Output file for environment variable substituted config file. + ''; + + reloader.rule-dirs = mkListParam "reloader.rule-dir" '' + Rule directories for the reloader to refresh. + ''; + + }; + + store = params.common cfg.store // params.objstore cfg.store // { + + stateDir = mkStateDirParam "data-dir" "thanos-store" '' + Data directory relative to `/var/lib` + in which to cache remote blocks. + ''; + + index-cache-size = mkParamDef types.str "250MB" '' + Maximum size of items held in the index cache. + ''; + + chunk-pool-size = mkParamDef types.str "2GB" '' + Maximum size of concurrently allocatable bytes for chunks. + ''; + + store.limits.request-samples = mkParamDef types.int 0 '' + The maximum samples allowed for a single Series request. + The Series call fails if this limit is exceeded. + + `0` means no limit. + + NOTE: For efficiency the limit is internally implemented as 'chunks limit' + considering each chunk contains a maximum of 120 samples. + ''; + + store.grpc.series-max-concurrency = mkParamDef types.int 20 '' + Maximum number of concurrent Series calls. + ''; + + sync-block-duration = mkParamDef types.str "3m" '' + Repeat interval for syncing the blocks between local and remote view. + ''; + + block-sync-concurrency = mkParamDef types.int 20 '' + Number of goroutines to use when syncing blocks from object storage. + ''; + + min-time = mkParamDef types.str "0000-01-01T00:00:00Z" '' + Start of time range limit to serve. + + Thanos Store serves only metrics, which happened later than this + value. Option can be a constant time in RFC3339 format or time duration + relative to current time, such as -1d or 2h45m. Valid duration units are + ms, s, m, h, d, w, y. + ''; + + max-time = mkParamDef types.str "9999-12-31T23:59:59Z" '' + End of time range limit to serve. + + Thanos Store serves only blocks, which happened earlier than this + value. Option can be a constant time in RFC3339 format or time duration + relative to current time, such as -1d or 2h45m. Valid duration units are + ms, s, m, h, d, w, y. + ''; + }; + + query = params.common cfg.query // { + + grpc-client-tls-secure = mkFlagParam '' + Use TLS when talking to the gRPC server + ''; + + grpc-client-tls-cert = mkParam types.str '' + TLS Certificates to use to identify this client to the server + ''; + + grpc-client-tls-key = mkParam types.str '' + TLS Key for the client's certificate + ''; + + grpc-client-tls-ca = mkParam types.str '' + TLS CA Certificates to use to verify gRPC servers + ''; + + grpc-client-server-name = mkParam types.str '' + Server name to verify the hostname on the returned gRPC certificates. + See <https://tools.ietf.org/html/rfc4366#section-3.1> + ''; + + grpc-compression = mkParam types.str '' + Compression algorithm to use for gRPC requests to other clients. + ''; + + web.route-prefix = mkParam types.str '' + Prefix for API and UI endpoints. + + This allows thanos UI to be served on a sub-path. This option is + analogous to {option}`web.route-prefix` of Promethus. + ''; + + web.external-prefix = mkParam types.str '' + Static prefix for all HTML links and redirect URLs in the UI query web + interface. + + Actual endpoints are still served on / or the + {option}`web.route-prefix`. This allows thanos UI to be served + behind a reverse proxy that strips a URL sub-path. + ''; + + web.prefix-header = mkParam types.str '' + Name of HTTP request header used for dynamic prefixing of UI links and + redirects. + + This option is ignored if the option + `web.external-prefix` is set. + + Security risk: enable this option only if a reverse proxy in front of + thanos is resetting the header. + + The setting `web.prefix-header="X-Forwarded-Prefix"` + can be useful, for example, if Thanos UI is served via Traefik reverse + proxy with `PathPrefixStrip` option enabled, which + sends the stripped prefix value in `X-Forwarded-Prefix` + header. This allows thanos UI to be served on a sub-path. + ''; + + query.timeout = mkParamDef types.str "2m" '' + Maximum time to process query by query node. + ''; + + query.max-concurrent = mkParamDef types.int 20 '' + Maximum number of queries processed concurrently by query node. + ''; + + query.replica-labels = mkListParam "query.replica-label" '' + Labels to treat as a replica indicator along which data is + deduplicated. + + Still you will be able to query without deduplication using + 'dedup=false' parameter. Data includes time series, recording + rules, and alerting rules. + ''; + + selector-labels = mkAttrsParam "selector-label" '' + Query selector labels that will be exposed in info endpoint. + ''; + + endpoints = mkListParam "endpoint" '' + Addresses of statically configured Thanos API servers (repeatable). + + The scheme may be prefixed with 'dns+' or 'dnssrv+' to detect + Thanos API servers through respective DNS lookups. + ''; + + store.sd-files = mkListParam "store.sd-files" '' + Path to files that contain addresses of store API servers. The path + can be a glob pattern. + ''; + + store.sd-interval = mkParamDef types.str "5m" '' + Refresh interval to re-read file SD files. It is used as a resync fallback. + ''; + + store.sd-dns-interval = mkParamDef types.str "30s" '' + Interval between DNS resolutions. + ''; + + store.unhealthy-timeout = mkParamDef types.str "5m" '' + Timeout before an unhealthy store is cleaned from the store UI page. + ''; + + query.auto-downsampling = mkFlagParam '' + Enable automatic adjustment (step / 5) to what source of data should + be used in store gateways if no + `max_source_resolution` param is specified. + ''; + + query.partial-response = mkFlagParam '' + Enable partial response for queries if no + `partial_response` param is specified. + ''; + + query.default-evaluation-interval = mkParamDef types.str "1m" '' + Set default evaluation interval for sub queries. + ''; + + store.response-timeout = mkParamDef types.str "0ms" '' + If a Store doesn't send any data in this specified duration then a + Store will be ignored and partial data will be returned if it's + enabled. `0` disables timeout. + ''; + }; + + query-frontend = params.common cfg.query-frontend // { + query-frontend.downstream-url = mkParamDef types.str "http://localhost:9090" '' + URL of downstream Prometheus Query compatible API. + ''; + }; + + rule = params.common cfg.rule // params.objstore cfg.rule // { + + labels = mkAttrsParam "label" '' + Labels to be applied to all generated metrics. + + Similar to external labels for Prometheus, + used to identify ruler and its blocks as unique source. + ''; + + stateDir = mkStateDirParam "data-dir" "thanos-rule" '' + Data directory relative to `/var/lib`. + ''; + + rule-files = mkListParam "rule-file" '' + Rule files that should be used by rule manager. Can be in glob format. + ''; + + eval-interval = mkParamDef types.str "1m" '' + The default evaluation interval to use. + ''; + + tsdb.block-duration = mkParamDef types.str "2h" '' + Block duration for TSDB block. + ''; + + tsdb.retention = mkParamDef types.str "48h" '' + Block retention time on local disk. + ''; + + alertmanagers.urls = mkListParam "alertmanagers.url" '' + Alertmanager replica URLs to push firing alerts. + + Ruler claims success if push to at least one alertmanager from + discovered succeeds. The scheme may be prefixed with + `dns+` or `dnssrv+` to detect + Alertmanager IPs through respective DNS lookups. The port defaults to + `9093` or the SRV record's value. The URL path is + used as a prefix for the regular Alertmanager API path. + ''; + + alertmanagers.send-timeout = mkParamDef types.str "10s" '' + Timeout for sending alerts to alertmanager. + ''; + + alert.query-url = mkParam types.str '' + The external Thanos Query URL that would be set in all alerts 'Source' field. + ''; + + alert.label-drop = mkListParam "alert.label-drop" '' + Labels by name to drop before sending to alertmanager. + + This allows alert to be deduplicated on replica label. + + Similar Prometheus alert relabelling + ''; + + web.route-prefix = mkParam types.str '' + Prefix for API and UI endpoints. + + This allows thanos UI to be served on a sub-path. + + This option is analogous to `--web.route-prefix` of Promethus. + ''; + + web.external-prefix = mkParam types.str '' + Static prefix for all HTML links and redirect URLs in the UI query web + interface. + + Actual endpoints are still served on / or the + {option}`web.route-prefix`. This allows thanos UI to be served + behind a reverse proxy that strips a URL sub-path. + ''; + + web.prefix-header = mkParam types.str '' + Name of HTTP request header used for dynamic prefixing of UI links and + redirects. + + This option is ignored if the option + {option}`web.external-prefix` is set. + + Security risk: enable this option only if a reverse proxy in front of + thanos is resetting the header. + + The header `X-Forwarded-Prefix` can be useful, for + example, if Thanos UI is served via Traefik reverse proxy with + `PathPrefixStrip` option enabled, which sends the + stripped prefix value in `X-Forwarded-Prefix` + header. This allows thanos UI to be served on a sub-path. + ''; + + query.addresses = mkListParam "query" '' + Addresses of statically configured query API servers. + + The scheme may be prefixed with `dns+` or + `dnssrv+` to detect query API servers through + respective DNS lookups. + ''; + + query.sd-files = mkListParam "query.sd-files" '' + Path to file that contain addresses of query peers. + The path can be a glob pattern. + ''; + + query.sd-interval = mkParamDef types.str "5m" '' + Refresh interval to re-read file SD files. (used as a fallback) + ''; + + query.sd-dns-interval = mkParamDef types.str "30s" '' + Interval between DNS resolutions. + ''; + }; + + compact = params.log // params.tracing cfg.compact // params.objstore cfg.compact // { + + http-address = mkParamDef types.str "0.0.0.0:10902" '' + Listen `host:port` for HTTP endpoints. + ''; + + stateDir = mkStateDirParam "data-dir" "thanos-compact" '' + Data directory relative to `/var/lib` + in which to cache blocks and process compactions. + ''; + + consistency-delay = mkParamDef types.str "30m" '' + Minimum age of fresh (non-compacted) blocks before they are being + processed. Malformed blocks older than the maximum of consistency-delay + and 30m0s will be removed. + ''; + + retention.resolution-raw = mkParamDef types.str "0d" '' + How long to retain raw samples in bucket. + + `0d` - disables this retention + ''; + + retention.resolution-5m = mkParamDef types.str "0d" '' + How long to retain samples of resolution 1 (5 minutes) in bucket. + + `0d` - disables this retention + ''; + + retention.resolution-1h = mkParamDef types.str "0d" '' + How long to retain samples of resolution 2 (1 hour) in bucket. + + `0d` - disables this retention + ''; + + startAt = { + toArgs = _opt: startAt: flagToArgs "wait" (startAt == null); + option = nullOpt types.str '' + When this option is set to a `systemd.time` + specification the Thanos compactor will run at the specified period. + + When this option is `null` the Thanos compactor service + will run continuously. So it will not exit after all compactions have + been processed but wait for new work. + ''; + }; + + downsampling.disable = mkFlagParam '' + Disables downsampling. + + This is not recommended as querying long time ranges without + non-downsampled data is not efficient and useful e.g it is not possible + to render all samples for a human eye anyway + ''; + + compact.concurrency = mkParamDef types.int 1 '' + Number of goroutines to use when compacting groups. + ''; + }; + + downsample = params.log // params.tracing cfg.downsample // params.objstore cfg.downsample // { + + stateDir = mkStateDirParam "data-dir" "thanos-downsample" '' + Data directory relative to `/var/lib` + in which to cache blocks and process downsamplings. + ''; + + }; + + receive = params.common cfg.receive // params.objstore cfg.receive // { + + receive.grpc-compression = mkParam types.str '' + Compression algorithm to use for gRPC requests to other receivers. + ''; + + remote-write.address = mkParamDef types.str "0.0.0.0:19291" '' + Address to listen on for remote write requests. + ''; + + stateDir = mkStateDirParam "tsdb.path" "thanos-receive" '' + Data directory relative to `/var/lib` of TSDB. + ''; + + labels = mkAttrsParam "label" '' + External labels to announce. + + This flag will be removed in the future when handling multiple tsdb + instances is added. + ''; + + tsdb.retention = mkParamDef types.str "15d" '' + How long to retain raw samples on local storage. + + `0d` - disables this retention + ''; + }; + + }; + + assertRelativeStateDir = cmd: { + assertions = [ + { + assertion = !hasPrefix "/" cfg.${cmd}.stateDir; + message = + "The option services.thanos.${cmd}.stateDir should not be an absolute directory." + + " It should be a directory relative to /var/lib."; + } + ]; + }; + +in { + + options.services.thanos = { + + package = mkPackageOption pkgs "thanos" {}; + + sidecar = paramsToOptions params.sidecar // { + enable = mkEnableOption "the Thanos sidecar for Prometheus server"; + arguments = mkArgumentsOption "sidecar"; + }; + + store = paramsToOptions params.store // { + enable = mkEnableOption "the Thanos store node giving access to blocks in a bucket provider."; + arguments = mkArgumentsOption "store"; + }; + + query = paramsToOptions params.query // { + enable = mkEnableOption ("the Thanos query node exposing PromQL enabled Query API " + + "with data retrieved from multiple store nodes"); + arguments = mkArgumentsOption "query"; + }; + + query-frontend = paramsToOptions params.query-frontend // { + enable = mkEnableOption ("the Thanos query frontend implements a service deployed in front of queriers to + improve query parallelization and caching."); + arguments = mkArgumentsOption "query-frontend"; + }; + + rule = paramsToOptions params.rule // { + enable = mkEnableOption ("the Thanos ruler service which evaluates Prometheus rules against" + + " given Query nodes, exposing Store API and storing old blocks in bucket"); + arguments = mkArgumentsOption "rule"; + }; + + compact = paramsToOptions params.compact // { + enable = mkEnableOption "the Thanos compactor which continuously compacts blocks in an object store bucket"; + arguments = mkArgumentsOption "compact"; + }; + + downsample = paramsToOptions params.downsample // { + enable = mkEnableOption "the Thanos downsampler which continuously downsamples blocks in an object store bucket"; + arguments = mkArgumentsOption "downsample"; + }; + + receive = paramsToOptions params.receive // { + enable = mkEnableOption ("the Thanos receiver which accept Prometheus remote write API requests and write to local tsdb"); + arguments = mkArgumentsOption "receive"; + }; + }; + + config = mkMerge [ + + (mkIf cfg.sidecar.enable { + assertions = [ + { + assertion = config.services.prometheus.enable; + message = + "Please enable services.prometheus when enabling services.thanos.sidecar."; + } + { + assertion = !(config.services.prometheus.globalConfig.external_labels == null || + config.services.prometheus.globalConfig.external_labels == {}); + message = + "services.thanos.sidecar requires uniquely identifying external labels " + + "to be configured in the Prometheus server. " + + "Please set services.prometheus.globalConfig.external_labels."; + } + ]; + systemd.services.thanos-sidecar = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "prometheus.service" ]; + serviceConfig = { + User = "prometheus"; + Restart = "always"; + ExecStart = thanos "sidecar"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + }; + }) + + (mkIf cfg.store.enable (mkMerge [ + (assertRelativeStateDir "store") + { + systemd.services.thanos-store = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + DynamicUser = true; + StateDirectory = cfg.store.stateDir; + Restart = "always"; + ExecStart = thanos "store"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + }; + } + ])) + + (mkIf cfg.query.enable { + systemd.services.thanos-query = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + DynamicUser = true; + Restart = "always"; + ExecStart = thanos "query"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + }; + }) + + (mkIf cfg.query-frontend.enable { + systemd.services.thanos-query-frontend = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + DynamicUser = true; + Restart = "always"; + ExecStart = thanos "query-frontend"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + }; + }) + + (mkIf cfg.rule.enable (mkMerge [ + (assertRelativeStateDir "rule") + { + systemd.services.thanos-rule = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + DynamicUser = true; + StateDirectory = cfg.rule.stateDir; + Restart = "always"; + ExecStart = thanos "rule"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + }; + } + ])) + + (mkIf cfg.compact.enable (mkMerge [ + (assertRelativeStateDir "compact") + { + systemd.services.thanos-compact = + let wait = cfg.compact.startAt == null; in { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + Type = if wait then "simple" else "oneshot"; + Restart = if wait then "always" else "no"; + DynamicUser = true; + StateDirectory = cfg.compact.stateDir; + ExecStart = thanos "compact"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + } // optionalAttrs (!wait) { inherit (cfg.compact) startAt; }; + } + ])) + + (mkIf cfg.downsample.enable (mkMerge [ + (assertRelativeStateDir "downsample") + { + systemd.services.thanos-downsample = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + DynamicUser = true; + StateDirectory = cfg.downsample.stateDir; + Restart = "always"; + ExecStart = thanos "downsample"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + }; + } + ])) + + (mkIf cfg.receive.enable (mkMerge [ + (assertRelativeStateDir "receive") + { + systemd.services.thanos-receive = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + DynamicUser = true; + StateDirectory = cfg.receive.stateDir; + Restart = "always"; + ExecStart = thanos "receive"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + }; + } + ])) + + ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/tremor-rs.nix b/nixpkgs/nixos/modules/services/monitoring/tremor-rs.nix new file mode 100644 index 000000000000..c8a77ab93def --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/tremor-rs.nix @@ -0,0 +1,129 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + + cfg = config.services.tremor-rs; + + loggerSettingsFormat = pkgs.formats.yaml { }; + loggerConfigFile = loggerSettingsFormat.generate "logger.yaml" cfg.loggerSettings; +in { + + options = { + services.tremor-rs = { + enable = lib.mkEnableOption "Tremor event- or stream-processing system"; + + troyFileList = mkOption { + type = types.listOf types.path; + default = []; + description = "List of troy files to load."; + }; + + tremorLibDir = mkOption { + type = types.path; + default = ""; + description = "Directory where to find /lib containing tremor script files"; + }; + + host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "The host tremor should be listening on"; + }; + + port = mkOption { + type = types.port; + default = 9898; + description = "the port tremor should be listening on"; + }; + + loggerSettings = mkOption { + description = "Tremor logger configuration"; + default = {}; + type = loggerSettingsFormat.type; + + example = { + refresh_rate = "30 seconds"; + appenders.stdout.kind = "console"; + root = { + level = "warn"; + appenders = [ "stdout" ]; + }; + loggers = { + tremor_runtime = { + level = "debug"; + appenders = [ "stdout" ]; + additive = false; + }; + tremor = { + level = "debug"; + appenders = [ "stdout" ]; + additive = false; + }; + }; + }; + + defaultText = literalExpression '' + { + refresh_rate = "30 seconds"; + appenders.stdout.kind = "console"; + root = { + level = "warn"; + appenders = [ "stdout" ]; + }; + loggers = { + tremor_runtime = { + level = "debug"; + appenders = [ "stdout" ]; + additive = false; + }; + tremor = { + level = "debug"; + appenders = [ "stdout" ]; + additive = false; + }; + }; + } + ''; + + }; + }; + }; + + config = mkIf (cfg.enable) { + + environment.systemPackages = [ pkgs.tremor-rs ] ; + + systemd.services.tremor-rs = { + description = "Tremor event- or stream-processing system"; + wantedBy = [ "multi-user.target" ]; + requires = [ "network-online.target" ]; + after = [ "network-online.target" ]; + + environment.TREMOR_PATH = "${pkgs.tremor-rs}/lib:${cfg.tremorLibDir}"; + + serviceConfig = { + ExecStart = "${pkgs.tremor-rs}/bin/tremor --logger-config ${loggerConfigFile} server run ${concatStringsSep " " cfg.troyFileList} --api-host ${cfg.host}:${toString cfg.port}"; + DynamicUser = true; + Restart = "always"; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectHome = true; + ProtectClock = true; + ProtectProc = "noaccess"; + ProcSubset = "pid"; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectControlGroups = true; + ProtectHostname = true; + RestrictSUIDSGID = true; + RestrictRealtime = true; + RestrictNamespaces = true; + LockPersonality = true; + RemoveIPC = true; + SystemCallFilter = [ "@system-service" "~@privileged" ]; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/tuptime.nix b/nixpkgs/nixos/modules/services/monitoring/tuptime.nix new file mode 100644 index 000000000000..334f911a6c71 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/tuptime.nix @@ -0,0 +1,90 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.tuptime; + +in { + + options.services.tuptime = { + + enable = mkEnableOption "the total uptime service"; + + timer = { + enable = mkOption { + type = types.bool; + default = true; + description = "Whether to regularly log uptime to detect bad shutdowns."; + }; + + period = mkOption { + type = types.str; + default = "*:0/5"; + description = "systemd calendar event"; + }; + }; + }; + + + config = mkIf cfg.enable { + + environment.systemPackages = [ pkgs.tuptime ]; + + users = { + groups._tuptime.members = [ "_tuptime" ]; + users._tuptime = { + isSystemUser = true; + group = "_tuptime"; + description = "tuptime database owner"; + }; + }; + + systemd = { + services = { + + tuptime = { + description = "The total uptime service"; + documentation = [ "man:tuptime(1)" ]; + after = [ "time-sync.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + StateDirectory = "tuptime"; + Type = "oneshot"; + User = "_tuptime"; + RemainAfterExit = true; + ExecStart = "${pkgs.tuptime}/bin/tuptime -q"; + ExecStop = "${pkgs.tuptime}/bin/tuptime -qg"; + }; + }; + + tuptime-sync = mkIf cfg.timer.enable { + description = "Tuptime scheduled sync service"; + serviceConfig = { + Type = "oneshot"; + User = "_tuptime"; + ExecStart = "${pkgs.tuptime}/bin/tuptime -q"; + }; + }; + }; + + timers.tuptime-sync = mkIf cfg.timer.enable { + description = "Tuptime scheduled sync timer"; + # this timer should be started if the service is started + # even if the timer was previously stopped + wantedBy = [ "tuptime.service" "timers.target" ]; + # this timer should be stopped if the service is stopped + partOf = [ "tuptime.service" ]; + timerConfig = { + OnBootSec = "1min"; + OnCalendar = cfg.timer.period; + Unit = "tuptime-sync.service"; + }; + }; + }; + }; + + meta.maintainers = [ maintainers.evils ]; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/unpoller.nix b/nixpkgs/nixos/modules/services/monitoring/unpoller.nix new file mode 100644 index 000000000000..1b4acb5d938f --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/unpoller.nix @@ -0,0 +1,322 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.unpoller; + + configFile = pkgs.writeText "unpoller.json" (generators.toJSON {} { + inherit (cfg) poller influxdb loki prometheus unifi; + }); + +in { + imports = [ + (lib.mkRenamedOptionModule [ "services" "unifi-poller" ] [ "services" "unpoller" ]) + ]; + + options.services.unpoller = { + enable = mkEnableOption "unpoller"; + + 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 output 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 output 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 "unpoller-influxdb-default.password" "unifipoller"; + defaultText = literalExpression "unpoller-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. + ''; + }; + }; + + loki = { + url = mkOption { + type = types.str; + default = ""; + description = '' + URL of the Loki host. + ''; + }; + user = mkOption { + type = types.str; + default = ""; + description = '' + Username for Loki. + ''; + }; + pass = mkOption { + type = types.path; + default = pkgs.writeText "unpoller-loki-default.password" ""; + defaultText = "unpoller-influxdb-default.password"; + description = '' + Path of a file containing the password for Loki. + This file needs to be readable by the unifi-poller user. + ''; + apply = v: "file://${v}"; + }; + verify_ssl = mkOption { + type = types.bool; + default = false; + description = '' + Verify Loki's certificate. + ''; + }; + tenant_id = mkOption { + type = types.str; + default = ""; + description = '' + Tenant ID to use in Loki. + ''; + }; + interval = mkOption { + type = types.str; + default = "2m"; + description = '' + How often the events are polled and pushed to Loki. + ''; + }; + timeout = mkOption { + type = types.str; + default = "10s"; + description = '' + Should be increased in case of timeout errors. + ''; + }; + }; + + unifi = let + controllerOptions = { + user = mkOption { + type = types.str; + default = "unifi"; + description = '' + Unifi service user name. + ''; + }; + pass = mkOption { + type = types.path; + default = pkgs.writeText "unpoller-unifi-default.password" "unifi"; + defaultText = literalExpression "unpoller-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 and Loki. + ''; + }; + save_events = mkOption { + type = types.bool; + default = false; + description = '' + Collect and save data from UniFi events to influxdb and Loki. + ''; + }; + save_alarms = mkOption { + type = types.bool; + default = false; + description = '' + Collect and save data from UniFi alarms to influxdb and Loki. + ''; + }; + save_anomalies = mkOption { + type = types.bool; + default = false; + description = '' + Collect and save data from UniFi anomalies to influxdb and Loki. + ''; + }; + 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.unpoller}/bin/unpoller --config ${configFile}"; + Restart = "always"; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "full"; + DevicePolicy = "closed"; + NoNewPrivileges = true; + User = "unifi-poller"; + WorkingDirectory = "/tmp"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/ups.nix b/nixpkgs/nixos/modules/services/monitoring/ups.nix new file mode 100644 index 000000000000..0a0d5eadccd3 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/ups.nix @@ -0,0 +1,609 @@ +{ config, lib, pkgs, ... }: + +# TODO: This is not secure, have a look at the file docs/security.txt inside +# the project sources. +with lib; + +let + cfg = config.power.ups; + defaultPort = 3493; + + nutFormat = { + + type = with lib.types; let + + singleAtom = nullOr (oneOf [ + bool + int + float + str + ]) // { + description = "atom (null, bool, int, float or string)"; + }; + + in attrsOf (oneOf [ + singleAtom + (listOf (nonEmptyListOf singleAtom)) + ]); + + generate = name: value: + let + normalizedValue = + lib.mapAttrs (key: val: + if lib.isList val + then forEach val (elem: if lib.isList elem then elem else [elem]) + else + if val == null + then [] + else [[val]] + ) value; + + mkValueString = concatMapStringsSep " " (v: + let str = generators.mkValueStringDefault {} v; + in + # Quote the value if it has spaces and isn't already quoted. + if (hasInfix " " str) && !(hasPrefix "\"" str && hasSuffix "\"" str) + then "\"${str}\"" + else str + ); + + in pkgs.writeText name (lib.generators.toKeyValue { + mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " "; + listsAsDuplicateKeys = true; + } normalizedValue); + + }; + + installSecrets = source: target: secrets: + pkgs.writeShellScript "installSecrets.sh" '' + install -m0600 -D ${source} "${target}" + ${concatLines (forEach secrets (name: '' + ${pkgs.replace-secret}/bin/replace-secret \ + '@${name}@' \ + "$CREDENTIALS_DIRECTORY/${name}" \ + "${target}" + ''))} + chmod u-w "${target}" + ''; + + upsmonConf = nutFormat.generate "upsmon.conf" cfg.upsmon.settings; + + upsdUsers = pkgs.writeText "upsd.users" (let + # This looks like INI, but it's not quite because the + # 'upsmon' option lacks a '='. See: man upsd.users + userConfig = name: user: concatStringsSep "\n " (concatLists [ + [ + "[${name}]" + "password = \"@upsdusers_password_${name}@\"" + ] + (optional (user.upsmon != null) "upsmon ${user.upsmon}") + (forEach user.actions (action: "actions = ${action}")) + (forEach user.instcmds (instcmd: "instcmds = ${instcmd}")) + ]); + in concatStringsSep "\n\n" (mapAttrsToList userConfig cfg.users)); + + + upsOptions = {name, config, ...}: + { + options = { + # This can be inferred from the UPS model by looking at + # /nix/store/nut/share/driver.list + driver = mkOption { + type = types.str; + description = '' + Specify the program to run to talk to this UPS. apcsmart, + bestups, and sec are some examples. + ''; + }; + + port = mkOption { + type = types.str; + description = '' + The serial port to which your UPS is connected. /dev/ttyS0 is + usually the first port on Linux boxes, for example. + ''; + }; + + shutdownOrder = mkOption { + default = 0; + type = types.int; + description = '' + When you have multiple UPSes on your system, you usually need to + turn them off in a certain order. upsdrvctl shuts down all the + 0s, then the 1s, 2s, and so on. To exclude a UPS from the + shutdown sequence, set this to -1. + ''; + }; + + maxStartDelay = mkOption { + default = null; + type = types.uniq (types.nullOr types.int); + description = '' + This can be set as a global variable above your first UPS + definition and it can also be set in a UPS section. This value + controls how long upsdrvctl will wait for the driver to finish + starting. This keeps your system from getting stuck due to a + broken driver or UPS. + ''; + }; + + description = mkOption { + default = ""; + type = types.str; + description = '' + Description of the UPS. + ''; + }; + + directives = mkOption { + default = []; + type = types.listOf types.str; + description = '' + List of configuration directives for this UPS. + ''; + }; + + summary = mkOption { + default = ""; + type = types.lines; + description = '' + Lines which would be added inside ups.conf for handling this UPS. + ''; + }; + + }; + + config = { + directives = mkOrder 10 ([ + "driver = ${config.driver}" + "port = ${config.port}" + ''desc = "${config.description}"'' + "sdorder = ${toString config.shutdownOrder}" + ] ++ (optional (config.maxStartDelay != null) + "maxstartdelay = ${toString config.maxStartDelay}") + ); + + summary = + concatStringsSep "\n " + (["[${name}]"] ++ config.directives); + }; + }; + + listenOptions = { + options = { + address = mkOption { + type = types.str; + description = '' + Address of the interface for `upsd` to listen on. + See `man upsd.conf` for details. + ''; + }; + + port = mkOption { + type = types.port; + default = defaultPort; + description = '' + TCP port for `upsd` to listen on. + See `man upsd.conf` for details. + ''; + }; + }; + }; + + upsdOptions = { + options = { + enable = mkOption { + type = types.bool; + defaultText = literalMD "`true` if `mode` is one of `standalone`, `netserver`"; + description = "Whether to enable `upsd`."; + }; + + listen = mkOption { + type = with types; listOf (submodule listenOptions); + default = []; + example = [ + { + address = "192.168.50.1"; + } + { + address = "::1"; + port = 5923; + } + ]; + description = '' + Address of the interface for `upsd` to listen on. + See `man upsd` for details`. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Additional lines to add to `upsd.conf`. + ''; + }; + }; + + config = { + enable = mkDefault (elem cfg.mode [ "standalone" "netserver" ]); + }; + }; + + + monitorOptions = { name, config, ... }: { + options = { + system = mkOption { + type = types.str; + default = name; + description = '' + Identifier of the UPS to monitor, in this form: `<upsname>[@<hostname>[:<port>]]` + See `upsmon.conf` for details. + ''; + }; + + powerValue = mkOption { + type = types.int; + default = 1; + description = '' + Number of power supplies that the UPS feeds on this system. + See `upsmon.conf` for details. + ''; + }; + + user = mkOption { + type = types.str; + description = '' + Username from `upsd.users` for accessing this UPS. + See `upsmon.conf` for details. + ''; + }; + + passwordFile = mkOption { + type = types.str; + defaultText = literalMD "power.ups.users.\${user}.passwordFile"; + description = '' + The full path to a file containing the password from + `upsd.users` for accessing this UPS. The password file + is read on service start. + See `upsmon.conf` for details. + ''; + }; + + type = mkOption { + type = types.str; + default = "master"; + description = '' + The relationship with `upsd`. + See `upsmon.conf` for details. + ''; + }; + }; + + config = { + passwordFile = mkDefault cfg.users.${config.user}.passwordFile; + }; + }; + + upsmonOptions = { + options = { + enable = mkOption { + type = types.bool; + defaultText = literalMD "`true` if `mode` is one of `standalone`, `netserver`, `netclient`"; + description = "Whether to enable `upsmon`."; + }; + + monitor = mkOption { + type = with types; attrsOf (submodule monitorOptions); + default = {}; + description = '' + Set of UPS to monitor. See `man upsmon.conf` for details. + ''; + }; + + settings = mkOption { + type = nutFormat.type; + default = {}; + defaultText = literalMD '' + { + MINSUPPLIES = 1; + RUN_AS_USER = "root"; + NOTIFYCMD = "''${pkgs.nut}/bin/upssched"; + SHUTDOWNCMD = "''${pkgs.systemd}/bin/shutdown now"; + } + ''; + description = "Additional settings to add to `upsmon.conf`."; + example = literalMD '' + { + MINSUPPLIES = 2; + NOTIFYFLAG = [ + [ "ONLINE" "SYSLOG+EXEC" ] + [ "ONBATT" "SYSLOG+EXEC" ] + ]; + } + ''; + }; + }; + + config = { + enable = mkDefault (elem cfg.mode [ "standalone" "netserver" "netclient" ]); + settings = { + RUN_AS_USER = "root"; # TODO: replace 'root' by another username. + MINSUPPLIES = mkDefault 1; + NOTIFYCMD = mkDefault "${pkgs.nut}/bin/upssched"; + SHUTDOWNCMD = mkDefault "${pkgs.systemd}/bin/shutdown now"; + MONITOR = flip mapAttrsToList cfg.upsmon.monitor (name: monitor: with monitor; [ system powerValue user "\"@upsmon_password_${name}@\"" type ]); + }; + }; + }; + + userOptions = { + options = { + passwordFile = mkOption { + type = types.str; + description = '' + The full path to a file that contains the user's (clear text) + password. The password file is read on service start. + ''; + }; + + actions = mkOption { + type = with types; listOf str; + default = []; + description = '' + Allow the user to do certain things with upsd. + See `man upsd.users` for details. + ''; + }; + + instcmds = mkOption { + type = with types; listOf str; + default = []; + description = '' + Let the user initiate specific instant commands. Use "ALL" to grant all commands automatically. For the full list of what your UPS supports, use "upscmd -l". + See `man upsd.users` for details. + ''; + }; + + upsmon = mkOption { + type = with types; nullOr str; + default = null; + description = '' + Add the necessary actions for a upsmon process to work. + See `man upsd.users` for details. + ''; + }; + }; + }; + +in + + +{ + options = { + # powerManagement.powerDownCommands + + power.ups = { + enable = mkEnableOption '' + Enables support for Power Devices, such as Uninterruptible Power + Supplies, Power Distribution Units and Solar Controllers. + ''; + + mode = mkOption { + default = "standalone"; + type = types.enum [ "none" "standalone" "netserver" "netclient" ]; + description = '' + The MODE determines which part of the NUT is to be started, and + which configuration files must be modified. + + The values of MODE can be: + + - none: NUT is not configured, or use the Integrated Power + Management, or use some external system to startup NUT + components. So nothing is to be started. + + - standalone: This mode address a local only configuration, with 1 + UPS protecting the local system. This implies to start the 3 NUT + layers (driver, upsd and upsmon) and the matching configuration + files. This mode can also address UPS redundancy. + + - netserver: same as for the standalone configuration, but also + need some more ACLs and possibly a specific LISTEN directive in + upsd.conf. Since this MODE is opened to the network, a special + care should be applied to security concerns. + + - netclient: this mode only requires upsmon. + ''; + }; + + schedulerRules = mkOption { + example = "/etc/nixos/upssched.conf"; + type = types.str; + description = '' + File which contains the rules to handle UPS events. + ''; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = '' + Open ports in the firewall for `upsd`. + ''; + }; + + maxStartDelay = mkOption { + default = 45; + type = types.int; + description = '' + This can be set as a global variable above your first UPS + definition and it can also be set in a UPS section. This value + controls how long upsdrvctl will wait for the driver to finish + starting. This keeps your system from getting stuck due to a + broken driver or UPS. + ''; + }; + + upsmon = mkOption { + default = {}; + description = '' + Options for the `upsmon.conf` configuration file. + ''; + type = types.submodule upsmonOptions; + }; + + upsd = mkOption { + default = {}; + description = '' + Options for the `upsd.conf` configuration file. + ''; + type = types.submodule upsdOptions; + }; + + ups = mkOption { + default = {}; + # see nut/etc/ups.conf.sample + description = '' + This is where you configure all the UPSes that this system will be + monitoring directly. These are usually attached to serial ports, + but USB devices are also supported. + ''; + type = with types; attrsOf (submodule upsOptions); + }; + + users = mkOption { + default = {}; + description = '' + Users that can access upsd. See `man upsd.users`. + ''; + type = with types; attrsOf (submodule userOptions); + }; + + }; + }; + + config = mkIf cfg.enable { + + assertions = [ + (let + totalPowerValue = foldl' add 0 (map (monitor: monitor.powerValue) (attrValues cfg.upsmon.monitor)); + minSupplies = cfg.upsmon.settings.MINSUPPLIES; + in mkIf cfg.upsmon.enable { + assertion = totalPowerValue >= minSupplies; + message = '' + `power.ups.upsmon`: Total configured power value (${toString totalPowerValue}) must be at least MINSUPPLIES (${toString minSupplies}). + ''; + }) + ]; + + environment.systemPackages = [ pkgs.nut ]; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = + if cfg.upsd.listen == [] + then [ defaultPort ] + else unique (forEach cfg.upsd.listen (listen: listen.port)); + }; + + systemd.services.upsmon = let + secrets = mapAttrsToList (name: monitor: "upsmon_password_${name}") cfg.upsmon.monitor; + createUpsmonConf = installSecrets upsmonConf "/run/nut/upsmon.conf" secrets; + in { + enable = cfg.upsmon.enable; + description = "Uninterruptible Power Supplies (Monitor)"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "forking"; + ExecStartPre = "${createUpsmonConf}"; + ExecStart = "${pkgs.nut}/sbin/upsmon"; + ExecReload = "${pkgs.nut}/sbin/upsmon -c reload"; + LoadCredential = mapAttrsToList (name: monitor: "upsmon_password_${name}:${monitor.passwordFile}") cfg.upsmon.monitor; + }; + environment.NUT_CONFPATH = "/etc/nut"; + environment.NUT_STATEPATH = "/var/lib/nut"; + }; + + systemd.services.upsd = let + secrets = mapAttrsToList (name: user: "upsdusers_password_${name}") cfg.users; + createUpsdUsers = installSecrets upsdUsers "/run/nut/upsd.users" secrets; + in { + enable = cfg.upsd.enable; + description = "Uninterruptible Power Supplies (Daemon)"; + after = [ "network.target" "upsmon.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "forking"; + ExecStartPre = "${createUpsdUsers}"; + # TODO: replace 'root' by another username. + ExecStart = "${pkgs.nut}/sbin/upsd -u root"; + ExecReload = "${pkgs.nut}/sbin/upsd -c reload"; + LoadCredential = mapAttrsToList (name: user: "upsdusers_password_${name}:${user.passwordFile}") cfg.users; + }; + environment.NUT_CONFPATH = "/etc/nut"; + environment.NUT_STATEPATH = "/var/lib/nut"; + restartTriggers = [ + config.environment.etc."nut/upsd.conf".source + ]; + }; + + systemd.services.upsdrv = { + enable = cfg.upsd.enable; + description = "Uninterruptible Power Supplies (Register all UPS)"; + after = [ "upsd.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + # TODO: replace 'root' by another username. + ExecStart = "${pkgs.nut}/bin/upsdrvctl -u root start"; + }; + environment.NUT_CONFPATH = "/etc/nut"; + environment.NUT_STATEPATH = "/var/lib/nut"; + }; + + environment.etc = { + "nut/nut.conf".source = pkgs.writeText "nut.conf" + '' + MODE = ${cfg.mode} + ''; + "nut/ups.conf".source = pkgs.writeText "ups.conf" + '' + maxstartdelay = ${toString cfg.maxStartDelay} + + ${concatStringsSep "\n\n" (forEach (attrValues cfg.ups) (ups: ups.summary))} + ''; + "nut/upsd.conf".source = pkgs.writeText "upsd.conf" + '' + ${concatStringsSep "\n" (forEach cfg.upsd.listen (listen: "LISTEN ${listen.address} ${toString listen.port}"))} + ${cfg.upsd.extraConfig} + ''; + "nut/upssched.conf".source = cfg.schedulerRules; + "nut/upsd.users".source = "/run/nut/upsd.users"; + "nut/upsmon.conf".source = "/run/nut/upsmon.conf"; + }; + + power.ups.schedulerRules = mkDefault "${pkgs.nut}/etc/upssched.conf.sample"; + + systemd.tmpfiles.rules = [ + "d /var/state/ups -" + "d /var/lib/nut 700" + ]; + + services.udev.packages = [ pkgs.nut ]; + +/* + users.users.nut = + { uid = 84; + home = "/var/lib/nut"; + createHome = true; + group = "nut"; + description = "UPnP A/V Media Server user"; + }; + + users.groups."nut" = + { gid = 84; }; +*/ + + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/uptime-kuma.nix b/nixpkgs/nixos/modules/services/monitoring/uptime-kuma.nix new file mode 100644 index 000000000000..4c7dd900f52b --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/uptime-kuma.nix @@ -0,0 +1,76 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.uptime-kuma; +in +{ + + meta.maintainers = [ lib.maintainers.julienmalka ]; + + options = { + services.uptime-kuma = { + enable = mkEnableOption "Uptime Kuma, this assumes a reverse proxy to be set"; + + package = mkPackageOption pkgs "uptime-kuma" { }; + + appriseSupport = mkEnableOption "apprise support for notifications"; + + settings = lib.mkOption { + type = lib.types.submodule { freeformType = with lib.types; attrsOf str; }; + default = { }; + example = { + PORT = "4000"; + NODE_EXTRA_CA_CERTS = "/etc/ssl/certs/ca-certificates.crt"; + }; + description = '' + Additional configuration for Uptime Kuma, see + <https://github.com/louislam/uptime-kuma/wiki/Environment-Variables> + for supported values. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + + services.uptime-kuma.settings = { + DATA_DIR = "/var/lib/uptime-kuma/"; + NODE_ENV = mkDefault "production"; + HOST = mkDefault "127.0.0.1"; + PORT = mkDefault "3001"; + }; + + systemd.services.uptime-kuma = { + description = "Uptime Kuma"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = cfg.settings; + path = with pkgs; [ unixtools.ping ] ++ lib.optional cfg.appriseSupport apprise; + serviceConfig = { + Type = "simple"; + StateDirectory = "uptime-kuma"; + DynamicUser = true; + ExecStart = "${cfg.package}/bin/uptime-kuma-server"; + Restart = "on-failure"; + ProtectHome = true; + ProtectSystem = "strict"; + PrivateTmp = true; + PrivateDevices = true; + ProtectHostname = true; + ProtectClock = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + NoNewPrivileges = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + RemoveIPC = true; + PrivateMounts = true; + }; + }; + }; +} + diff --git a/nixpkgs/nixos/modules/services/monitoring/uptime.nix b/nixpkgs/nixos/modules/services/monitoring/uptime.nix new file mode 100644 index 000000000000..79b86be6cc71 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/uptime.nix @@ -0,0 +1,100 @@ +{ config, options, pkgs, lib, ... }: +let + inherit (lib) literalExpression mkOption mkEnableOption mkIf mkMerge types optional; + + cfg = config.services.uptime; + opt = options.services.uptime; + + configDir = pkgs.runCommand "config" { preferLocalBuild = true; } + (if cfg.configFile != null then '' + mkdir $out + ext=`echo ${cfg.configFile} | grep -o \\..*` + ln -sv ${cfg.configFile} $out/default$ext + ln -sv /var/lib/uptime/runtime.json $out/runtime.json + '' else '' + mkdir $out + cat ${pkgs.nodePackages.node-uptime}/lib/node_modules/node-uptime/config/default.yaml > $out/default.yaml + cat >> $out/default.yaml <<EOF + + autoStartMonitor: false + + mongodb: + connectionString: 'mongodb://localhost/uptime' + EOF + ln -sv /var/lib/uptime/runtime.json $out/runtime.json + ''); +in { + options.services.uptime = { + configFile = mkOption { + description = '' + The uptime configuration file + + If mongodb: server != localhost, please set usesRemoteMongo = true + + If you only want to run the monitor, please set enableWebService = false + and enableSeparateMonitoringService = true + + If autoStartMonitor: false (recommended) and you want to run both + services, please set enableSeparateMonitoringService = true + ''; + + type = types.nullOr types.path; + + default = null; + }; + + usesRemoteMongo = mkOption { + description = "Whether the configuration file specifies a remote mongo instance"; + + default = false; + + type = types.bool; + }; + + enableWebService = mkEnableOption "the uptime monitoring program web service"; + + enableSeparateMonitoringService = mkEnableOption "the uptime monitoring service" // { + default = cfg.enableWebService; + defaultText = literalExpression "config.${opt.enableWebService}"; + }; + + nodeEnv = mkOption { + description = "The node environment to run in (development, production, etc.)"; + + type = types.str; + + default = "production"; + }; + }; + + config = mkMerge [ (mkIf cfg.enableWebService { + systemd.services.uptime = { + description = "uptime web service"; + wantedBy = [ "multi-user.target" ]; + environment = { + NODE_CONFIG_DIR = configDir; + NODE_ENV = cfg.nodeEnv; + NODE_PATH = "${pkgs.nodePackages.node-uptime}/lib/node_modules/node-uptime/node_modules"; + }; + preStart = "mkdir -p /var/lib/uptime"; + serviceConfig.ExecStart = "${pkgs.nodejs}/bin/node ${pkgs.nodePackages.node-uptime}/lib/node_modules/node-uptime/app.js"; + }; + + services.mongodb.enable = mkIf (!cfg.usesRemoteMongo) true; + }) (mkIf cfg.enableSeparateMonitoringService { + systemd.services.uptime-monitor = { + description = "uptime monitoring service"; + wantedBy = [ "multi-user.target" ]; + requires = optional cfg.enableWebService "uptime.service"; + after = optional cfg.enableWebService "uptime.service"; + environment = { + NODE_CONFIG_DIR = configDir; + NODE_ENV = cfg.nodeEnv; + NODE_PATH = "${pkgs.nodePackages.node-uptime}/lib/node_modules/node-uptime/node_modules"; + }; + # Ugh, need to wait for web service to be up + preStart = if cfg.enableWebService then "sleep 1s" else "mkdir -p /var/lib/uptime"; + serviceConfig.ExecStart = "${pkgs.nodejs}/bin/node ${pkgs.nodePackages.node-uptime}/lib/node_modules/node-uptime/monitor.js"; + }; + }) ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/vmagent.nix b/nixpkgs/nixos/modules/services/monitoring/vmagent.nix new file mode 100644 index 000000000000..4838e0709d09 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/vmagent.nix @@ -0,0 +1,101 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.services.vmagent; + settingsFormat = pkgs.formats.json { }; +in { + imports = [ + (lib.mkRemovedOptionModule [ "services" "vmagent" "dataDir" ] "dataDir has been deprecated in favor of systemd provided CacheDirectory") + (lib.mkRemovedOptionModule [ "services" "vmagent" "user" ] "user has been deprecated in favor of systemd DynamicUser") + (lib.mkRemovedOptionModule [ "services" "vmagent" "group" ] "group has been deprecated in favor of systemd DynamicUser") + (lib.mkRenamedOptionModule [ "services" "vmagent" "remoteWriteUrl" ] [ "services" "vmagent" "remoteWrite" "url" ]) + ]; + + options.services.vmagent = { + enable = lib.mkEnableOption "vmagent"; + + package = lib.mkPackageOption pkgs "vmagent" { }; + + remoteWrite = { + url = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + description = '' + Endpoint for prometheus compatible remote_write + ''; + }; + basicAuthUsername = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + description = '' + Basic Auth username used to connect to remote_write endpoint + ''; + }; + basicAuthPasswordFile = lib.mkOption { + default = null; + type = lib.types.nullOr lib.types.str; + description = '' + File that contains the Basic Auth password used to connect to remote_write endpoint + ''; + }; + }; + + prometheusConfig = lib.mkOption { + type = lib.types.submodule { freeformType = settingsFormat.type; }; + description = '' + Config for prometheus style metrics + ''; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to open the firewall for the default ports. + ''; + }; + + extraArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = '' + Extra args to pass to `vmagent`. See the docs: + <https://docs.victoriametrics.com/vmagent.html#advanced-usage> + or {command}`vmagent -help` for more information. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ 8429 ]; + + systemd.services.vmagent = let + prometheusConfig = settingsFormat.generate "prometheusConfig.yaml" cfg.prometheusConfig; + startCommandLine = lib.concatStringsSep " " ([ + "${cfg.package}/bin/vmagent" + "-promscrape.config=${prometheusConfig}" + ] ++ cfg.extraArgs + ++ lib.optionals (cfg.remoteWrite.url != null) [ + "-remoteWrite.url=${cfg.remoteWrite.url}" + "-remoteWrite.tmpDataPath=%C/vmagent/remote_write_tmp" + ] ++ lib.optional (cfg.remoteWrite.basicAuthUsername != null) "-remoteWrite.basicAuth.username=${cfg.remoteWrite.basicAuthUsername}" + ++ lib.optional (cfg.remoteWrite.basicAuthPasswordFile != null) "-remoteWrite.basicAuth.passwordFile=\${CREDENTIALS_DIRECTORY}/remote_write_basic_auth_password"); + in { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + description = "vmagent system service"; + serviceConfig = { + DynamicUser = true; + User = "vmagent"; + Group = "vmagent"; + Type = "simple"; + Restart = "on-failure"; + CacheDirectory = "vmagent"; + ExecStart = startCommandLine; + LoadCredential = lib.optional (cfg.remoteWrite.basicAuthPasswordFile != null) [ + "remote_write_basic_auth_password:${cfg.remoteWrite.basicAuthPasswordFile}" + ]; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/vmalert.nix b/nixpkgs/nixos/modules/services/monitoring/vmalert.nix new file mode 100644 index 000000000000..65db6fab77db --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/vmalert.nix @@ -0,0 +1,129 @@ +{ config, pkgs, lib, ... }: with lib; +let + cfg = config.services.vmalert; + + format = pkgs.formats.yaml {}; + + confOpts = concatStringsSep " \\\n" (mapAttrsToList mkLine (filterAttrs (_: v: v != false) cfg.settings)); + confType = with types; + let + valueType = oneOf [ bool int path str ]; + in + attrsOf (either valueType (listOf valueType)); + + mkLine = key: value: + if value == true then "-${key}" + else if isList value then concatMapStringsSep " " (v: "-${key}=${escapeShellArg (toString v)}") value + else "-${key}=${escapeShellArg (toString value)}" + ; +in +{ + # interface + options.services.vmalert = { + enable = mkEnableOption "vmalert"; + + package = mkPackageOption pkgs "victoriametrics" { }; + + settings = mkOption { + type = types.submodule { + freeformType = confType; + options = { + + "datasource.url" = mkOption { + type = types.nonEmptyStr; + example = "http://localhost:8428"; + description = '' + Datasource compatible with Prometheus HTTP API. + ''; + }; + + "notifier.url" = mkOption { + type = with types; listOf nonEmptyStr; + default = []; + example = [ "http://127.0.0.1:9093" ]; + description = '' + Prometheus Alertmanager URL. List all Alertmanager URLs if it runs in the cluster mode to ensure high availability. + ''; + }; + + "rule" = mkOption { + type = with types; listOf path; + description = '' + Path to the files with alerting and/or recording rules. + + ::: {.note} + Consider using the {option}`services.vmalert.rules` option as a convenient alternative for declaring rules + directly in the `nix` language. + ::: + ''; + }; + + }; + }; + default = { }; + example = { + "datasource.url" = "http://localhost:8428"; + "datasource.disableKeepAlive" = true; + "datasource.showURL" = false; + "rule" = [ + "http://<some-server-addr>/path/to/rules" + "dir/*.yaml" + ]; + }; + description = '' + `vmalert` configuration, passed via command line flags. Refer to + <https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmalert/README.md#configuration> + for details on supported values. + ''; + }; + + rules = mkOption { + type = format.type; + default = {}; + example = { + group = [ + { name = "TestGroup"; + rules = [ + { alert = "ExampleAlertAlwaysFiring"; + expr = '' + sum by(job) + (up == 1) + ''; + } + ]; + } + ]; + }; + description = '' + A list of the given alerting or recording rules against configured `"datasource.url"` compatible with + Prometheus HTTP API for `vmalert` to execute. Refer to + <https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/app/vmalert/README.md#rules> + for details on supported values. + ''; + }; + }; + + # implementation + config = mkIf cfg.enable { + + environment.etc."vmalert/rules.yml".source = format.generate "rules.yml" cfg.rules; + + services.vmalert.settings.rule = [ + "/etc/vmalert/rules.yml" + ]; + + systemd.services.vmalert = { + description = "vmalert service"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + reloadTriggers = [ config.environment.etc."vmalert/rules.yml".source ]; + + serviceConfig = { + DynamicUser = true; + Restart = "on-failure"; + ExecStart = "${cfg.package}/bin/vmalert ${confOpts}"; + ExecReload = ''${pkgs.coreutils}/bin/kill -SIGHUP "$MAINPID"''; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/vnstat.nix b/nixpkgs/nixos/modules/services/monitoring/vnstat.nix new file mode 100644 index 000000000000..5e19c399568d --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/vnstat.nix @@ -0,0 +1,60 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.vnstat; +in { + options.services.vnstat = { + enable = mkEnableOption "update of network usage statistics via vnstatd"; + }; + + config = mkIf cfg.enable { + + environment.systemPackages = [ pkgs.vnstat ]; + + users = { + groups.vnstatd = {}; + + users.vnstatd = { + isSystemUser = true; + group = "vnstatd"; + description = "vnstat daemon user"; + }; + }; + + systemd.services.vnstat = { + description = "vnStat network traffic monitor"; + path = [ pkgs.coreutils ]; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + documentation = [ + "man:vnstatd(1)" + "man:vnstat(1)" + "man:vnstat.conf(5)" + ]; + serviceConfig = { + ExecStart = "${pkgs.vnstat}/bin/vnstatd -n"; + ExecReload = "${pkgs.procps}/bin/kill -HUP $MAINPID"; + + # Hardening (from upstream example service) + ProtectSystem = "strict"; + StateDirectory = "vnstat"; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectKernelModules = true; + PrivateTmp = true; + MemoryDenyWriteExecute = true; + RestrictRealtime = true; + RestrictNamespaces = true; + + User = "vnstatd"; + Group = "vnstatd"; + }; + }; + }; + + meta.maintainers = [ maintainers.evils ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/watchdogd.nix b/nixpkgs/nixos/modules/services/monitoring/watchdogd.nix new file mode 100644 index 000000000000..e8d104651c6a --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/watchdogd.nix @@ -0,0 +1,131 @@ +{ config, lib, pkgs, ... }: +with lib; +let + cfg = config.services.watchdogd; + + mkPluginOpts = plugin: defWarn: defCrit: { + enabled = mkEnableOption "watchdogd plugin ${plugin}"; + interval = mkOption { + type = types.ints.unsigned; + default = 300; + description = '' + Amount of seconds between every poll. + ''; + }; + logmark = mkOption { + type = types.bool; + default = false; + description = '' + Whether to log current stats every poll interval. + ''; + }; + warning = mkOption { + type = types.numbers.nonnegative; + default = defWarn; + description = '' + The high watermark level. Alert sent to log. + ''; + }; + critical = mkOption { + type = types.numbers.nonnegative; + default = defCrit; + description = '' + The critical watermark level. Alert sent to log, followed by reboot or script action. + ''; + }; + }; +in { + options.services.watchdogd = { + enable = mkEnableOption "watchdogd, an advanced system & process supervisor"; + package = mkPackageOption pkgs "watchdogd" { }; + + settings = mkOption { + type = with types; submodule { + freeformType = let + valueType = oneOf [ + bool + int + float + str + ]; + in attrsOf (either valueType (attrsOf valueType)); + + options = { + timeout = mkOption { + type = types.ints.unsigned; + default = 15; + description = '' + The WDT timeout before reset. + ''; + }; + interval = mkOption { + type = types.ints.unsigned; + default = 5; + description = '' + The kick interval, i.e. how often {manpage}`watchdogd(8)` should reset the WDT timer. + ''; + }; + + safe-exit = mkOption { + type = types.bool; + default = true; + description = '' + With {var}`safeExit` enabled, the daemon will ask the driver to disable the WDT before exiting. + However, some WDT drivers (or hardware) may not support this. + ''; + }; + + filenr = mkPluginOpts "filenr" 0.9 1.0; + + loadavg = mkPluginOpts "loadavg" 1.0 2.0; + + meminfo = mkPluginOpts "meminfo" 0.9 0.95; + }; + }; + default = { }; + description = '' + Configuration to put in {file}`watchdogd.conf`. + See {manpage}`watchdogd.conf(5)` for more details. + ''; + }; + }; + + config = let + toConfig = attrs: concatStringsSep "\n" (mapAttrsToList toValue attrs); + + toValue = name: value: + if isAttrs value + then pipe value [ + (mapAttrsToList toValue) + (map (s: " ${s}")) + (concatStringsSep "\n") + (s: "${name} {\n${s}\n}") + ] + else if isBool value + then "${name} = ${boolToString value}" + else if any (f: f value) [isString isInt isFloat] + then "${name} = ${toString value}" + else throw '' + Found invalid type in `services.watchdogd.settings`: '${typeOf value}' + ''; + + watchdogdConf = pkgs.writeText "watchdogd.conf" (toConfig cfg.settings); + in mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; + + systemd.services.watchdogd = { + documentation = [ + "man:watchdogd(8)" + "man:watchdogd.conf(5)" + ]; + wantedBy = [ "multi-user.target" ]; + description = "Advanced system & process supervisor"; + serviceConfig = { + Type = "simple"; + ExecStart = "${cfg.package}/bin/watchdogd -n -f ${watchdogdConf}"; + }; + }; + }; + + meta.maintainers = with maintainers; [ vifino ]; +} diff --git a/nixpkgs/nixos/modules/services/monitoring/zabbix-agent.nix b/nixpkgs/nixos/modules/services/monitoring/zabbix-agent.nix new file mode 100644 index 000000000000..b3850baa738b --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/zabbix-agent.nix @@ -0,0 +1,173 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.zabbixAgent; + + inherit (lib) mkDefault mkEnableOption mkPackageOption mkIf mkMerge mkOption; + inherit (lib) attrValues concatMapStringsSep literalExpression optionalString types; + inherit (lib.generators) toKeyValue; + + user = "zabbix-agent"; + group = "zabbix-agent"; + + moduleEnv = pkgs.symlinkJoin { + name = "zabbix-agent-module-env"; + paths = attrValues cfg.modules; + }; + + configFile = pkgs.writeText "zabbix_agent.conf" (toKeyValue { listsAsDuplicateKeys = true; } cfg.settings); + +in + +{ + imports = [ + (lib.mkRemovedOptionModule [ "services" "zabbixAgent" "extraConfig" ] "Use services.zabbixAgent.settings instead.") + ]; + + # interface + + options = { + + services.zabbixAgent = { + enable = mkEnableOption "the Zabbix Agent"; + + package = mkPackageOption pkgs [ "zabbix" "agent" ] { }; + + extraPackages = mkOption { + type = types.listOf types.package; + default = with pkgs; [ nettools ]; + defaultText = literalExpression "with pkgs; [ nettools ]"; + example = literalExpression "with pkgs; [ nettools mysql ]"; + description = '' + Packages to be added to the Zabbix {env}`PATH`. + Typically used to add executables for scripts, but can be anything. + ''; + }; + + modules = mkOption { + type = types.attrsOf types.package; + description = "A set of modules to load."; + default = {}; + example = literalExpression '' + { + "dummy.so" = pkgs.stdenv.mkDerivation { + name = "zabbix-dummy-module-''${cfg.package.version}"; + src = cfg.package.src; + buildInputs = [ cfg.package ]; + sourceRoot = "zabbix-''${cfg.package.version}/src/modules/dummy"; + installPhase = ''' + mkdir -p $out/lib + cp dummy.so $out/lib/ + '''; + }; + } + ''; + }; + + server = mkOption { + type = types.str; + description = '' + The IP address or hostname of the Zabbix server to connect to. + ''; + }; + + listen = { + ip = mkOption { + type = types.str; + default = "0.0.0.0"; + description = '' + List of comma delimited IP addresses that the agent should listen on. + ''; + }; + + port = mkOption { + type = types.port; + default = 10050; + description = '' + Agent will listen on this port for connections from the server. + ''; + }; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = '' + Open ports in the firewall for the Zabbix Agent. + ''; + }; + + settings = mkOption { + type = with types; attrsOf (oneOf [ int str (listOf str) ]); + default = {}; + description = '' + Zabbix Agent configuration. Refer to + <https://www.zabbix.com/documentation/current/manual/appendix/config/zabbix_agentd> + for details on supported values. + ''; + example = { + Hostname = "example.org"; + DebugLevel = 4; + }; + }; + + }; + + }; + + # implementation + + config = mkIf cfg.enable { + + services.zabbixAgent.settings = mkMerge [ + { + LogType = "console"; + Server = cfg.server; + ListenPort = cfg.listen.port; + } + (mkIf (cfg.modules != {}) { + LoadModule = builtins.attrNames cfg.modules; + LoadModulePath = "${moduleEnv}/lib"; + }) + + # the default value for "ListenIP" is 0.0.0.0 but zabbix agent 2 cannot accept configuration files which + # explicitly set "ListenIP" to the default value... + (mkIf (cfg.listen.ip != "0.0.0.0") { ListenIP = cfg.listen.ip; }) + ]; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.listen.port ]; + }; + + users.users.${user} = { + description = "Zabbix Agent daemon user"; + inherit group; + isSystemUser = true; + }; + + users.groups.${group} = { }; + + systemd.services.zabbix-agent = { + description = "Zabbix Agent"; + + wantedBy = [ "multi-user.target" ]; + + # https://www.zabbix.com/documentation/current/manual/config/items/userparameters + # > User parameters are commands executed by Zabbix agent. + # > /bin/sh is used as a command line interpreter under UNIX operating systems. + path = with pkgs; [ bash "/run/wrappers" ] ++ cfg.extraPackages; + + serviceConfig = { + ExecStart = "@${cfg.package}/sbin/zabbix_agentd zabbix_agentd -f --config ${configFile}"; + Restart = "always"; + RestartSec = 2; + + User = user; + Group = group; + PrivateTmp = true; + }; + }; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/zabbix-proxy.nix b/nixpkgs/nixos/modules/services/monitoring/zabbix-proxy.nix new file mode 100644 index 000000000000..7fa471b6404a --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/zabbix-proxy.nix @@ -0,0 +1,320 @@ +{ config, lib, options, pkgs, ... }: + +let + cfg = config.services.zabbixProxy; + opt = options.services.zabbixProxy; + pgsql = config.services.postgresql; + mysql = config.services.mysql; + + inherit (lib) mkAfter mkDefault mkEnableOption mkIf mkMerge mkOption; + inherit (lib) attrValues concatMapStringsSep getName literalExpression optional optionalAttrs optionalString types; + inherit (lib.generators) toKeyValue; + + user = "zabbix"; + group = "zabbix"; + runtimeDir = "/run/zabbix"; + stateDir = "/var/lib/zabbix"; + passwordFile = "${runtimeDir}/zabbix-dbpassword.conf"; + + moduleEnv = pkgs.symlinkJoin { + name = "zabbix-proxy-module-env"; + paths = attrValues cfg.modules; + }; + + configFile = pkgs.writeText "zabbix_proxy.conf" (toKeyValue { listsAsDuplicateKeys = true; } cfg.settings); + + mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql"; + pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql"; + +in + +{ + imports = [ + (lib.mkRemovedOptionModule [ "services" "zabbixProxy" "extraConfig" ] "Use services.zabbixProxy.settings instead.") + ]; + + # interface + + options = { + + services.zabbixProxy = { + enable = mkEnableOption "the Zabbix Proxy"; + + server = mkOption { + type = types.str; + description = '' + The IP address or hostname of the Zabbix server to connect to. + ''; + }; + + package = mkOption { + type = types.package; + default = + if cfg.database.type == "mysql" then pkgs.zabbix.proxy-mysql + else if cfg.database.type == "pgsql" then pkgs.zabbix.proxy-pgsql + else pkgs.zabbix.proxy-sqlite; + defaultText = literalExpression "pkgs.zabbix.proxy-pgsql"; + description = "The Zabbix package to use."; + }; + + extraPackages = mkOption { + type = types.listOf types.package; + default = with pkgs; [ nettools nmap traceroute ]; + defaultText = literalExpression "[ nettools nmap traceroute ]"; + description = '' + Packages to be added to the Zabbix {env}`PATH`. + Typically used to add executables for scripts, but can be anything. + ''; + }; + + modules = mkOption { + type = types.attrsOf types.package; + description = "A set of modules to load."; + default = {}; + example = literalExpression '' + { + "dummy.so" = pkgs.stdenv.mkDerivation { + name = "zabbix-dummy-module-''${cfg.package.version}"; + src = cfg.package.src; + buildInputs = [ cfg.package ]; + sourceRoot = "zabbix-''${cfg.package.version}/src/modules/dummy"; + installPhase = ''' + mkdir -p $out/lib + cp dummy.so $out/lib/ + '''; + }; + } + ''; + }; + + database = { + type = mkOption { + type = types.enum [ "mysql" "pgsql" "sqlite" ]; + example = "mysql"; + default = "pgsql"; + description = "Database engine to use."; + }; + + host = mkOption { + type = types.str; + default = "localhost"; + description = "Database host address."; + }; + + port = mkOption { + type = types.port; + default = if cfg.database.type == "mysql" then mysql.port else pgsql.services.port; + defaultText = literalExpression '' + if config.${opt.database.type} == "mysql" + then config.${options.services.mysql.port} + else config.services.postgresql.settings.port + ''; + description = "Database host port."; + }; + + name = mkOption { + type = types.str; + default = if cfg.database.type == "sqlite" then "${stateDir}/zabbix.db" else "zabbix"; + defaultText = literalExpression "zabbix"; + description = "Database name."; + }; + + user = mkOption { + type = types.str; + default = "zabbix"; + description = "Database user."; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/keys/zabbix-dbpassword"; + description = '' + A file containing the password corresponding to + {option}`database.user`. + ''; + }; + + socket = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/postgresql"; + description = "Path to the unix socket file to use for authentication."; + }; + + createLocally = mkOption { + type = types.bool; + default = true; + description = "Whether to create a local database automatically."; + }; + }; + + listen = { + ip = mkOption { + type = types.str; + default = "0.0.0.0"; + description = '' + List of comma delimited IP addresses that the trapper should listen on. + Trapper will listen on all network interfaces if this parameter is missing. + ''; + }; + + port = mkOption { + type = types.port; + default = 10051; + description = '' + Listen port for trapper. + ''; + }; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = '' + Open ports in the firewall for the Zabbix Proxy. + ''; + }; + + settings = mkOption { + type = with types; attrsOf (oneOf [ int str (listOf str) ]); + default = {}; + description = '' + Zabbix Proxy configuration. Refer to + <https://www.zabbix.com/documentation/current/manual/appendix/config/zabbix_proxy> + for details on supported values. + ''; + example = { + CacheSize = "1G"; + SSHKeyLocation = "/var/lib/zabbix/.ssh"; + StartPingers = 32; + }; + }; + + }; + + }; + + # implementation + + config = mkIf cfg.enable { + + assertions = [ + { assertion = !config.services.zabbixServer.enable; + message = "Please choose one of services.zabbixServer or services.zabbixProxy."; + } + { 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; + message = "a password cannot be specified if services.zabbixProxy.database.createLocally is set to true"; + } + ]; + + services.zabbixProxy.settings = mkMerge [ + { + LogType = "console"; + ListenIP = cfg.listen.ip; + ListenPort = cfg.listen.port; + Server = cfg.server; + # TODO: set to cfg.database.socket if database type is pgsql? + DBHost = optionalString (cfg.database.createLocally != true) cfg.database.host; + DBName = cfg.database.name; + DBUser = cfg.database.user; + SocketDir = runtimeDir; + FpingLocation = "/run/wrappers/bin/fping"; + LoadModule = builtins.attrNames cfg.modules; + } + (mkIf (cfg.database.createLocally != true) { DBPort = cfg.database.port; }) + (mkIf (cfg.database.passwordFile != null) { Include = [ "${passwordFile}" ]; }) + (mkIf (mysqlLocal && cfg.database.socket != null) { DBSocket = cfg.database.socket; }) + (mkIf (cfg.modules != {}) { LoadModulePath = "${moduleEnv}/lib"; }) + ]; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.listen.port ]; + }; + + services.mysql = optionalAttrs mysqlLocal { + enable = true; + package = mkDefault pkgs.mariadb; + }; + + systemd.services.mysql.postStart = mkAfter (optionalString mysqlLocal '' + ( echo "CREATE DATABASE IF NOT EXISTS \`${cfg.database.name}\` CHARACTER SET utf8 COLLATE utf8_bin;" + echo "CREATE USER IF NOT EXISTS '${cfg.database.user}'@'localhost' IDENTIFIED WITH ${if (getName config.services.mysql.package == getName pkgs.mariadb) then "unix_socket" else "auth_socket"};" + echo "GRANT ALL PRIVILEGES ON \`${cfg.database.name}\`.* TO '${cfg.database.user}'@'localhost';" + ) | ${config.services.mysql.package}/bin/mysql -N + ''); + + services.postgresql = optionalAttrs pgsqlLocal { + enable = true; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { name = cfg.database.user; + ensureDBOwnership = true; + } + ]; + }; + + users.users.${user} = { + description = "Zabbix daemon user"; + uid = config.ids.uids.zabbix; + inherit group; + }; + + users.groups.${group} = { + gid = config.ids.gids.zabbix; + }; + + security.wrappers = { + fping = + { setuid = true; + owner = "root"; + group = "root"; + source = "${pkgs.fping}/bin/fping"; + }; + }; + + systemd.services.zabbix-proxy = { + description = "Zabbix Proxy"; + + wantedBy = [ "multi-user.target" ]; + after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service"; + + path = [ "/run/wrappers" ] ++ cfg.extraPackages; + preStart = optionalString pgsqlLocal '' + if ! test -e "${stateDir}/db-created"; then + cat ${cfg.package}/share/zabbix/database/postgresql/schema.sql | ${pgsql.package}/bin/psql ${cfg.database.name} + touch "${stateDir}/db-created" + fi + '' + optionalString mysqlLocal '' + if ! test -e "${stateDir}/db-created"; then + cat ${cfg.package}/share/zabbix/database/mysql/schema.sql | ${mysql.package}/bin/mysql ${cfg.database.name} + touch "${stateDir}/db-created" + fi + '' + optionalString (cfg.database.type == "sqlite") '' + if ! test -e "${cfg.database.name}"; then + ${pkgs.sqlite}/bin/sqlite3 "${cfg.database.name}" < ${cfg.package}/share/zabbix/database/sqlite3/schema.sql + fi + '' + optionalString (cfg.database.passwordFile != null) '' + # create a copy of the supplied password file in a format zabbix can consume + install -m 0600 <(echo "DBPassword = $(cat ${cfg.database.passwordFile})") ${passwordFile} + ''; + + serviceConfig = { + ExecStart = "@${cfg.package}/sbin/zabbix_proxy zabbix_proxy -f --config ${configFile}"; + Restart = "always"; + RestartSec = 2; + + User = user; + Group = group; + RuntimeDirectory = "zabbix"; + StateDirectory = "zabbix"; + PrivateTmp = true; + }; + }; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/monitoring/zabbix-server.nix b/nixpkgs/nixos/modules/services/monitoring/zabbix-server.nix new file mode 100644 index 000000000000..3c6f60b9d722 --- /dev/null +++ b/nixpkgs/nixos/modules/services/monitoring/zabbix-server.nix @@ -0,0 +1,317 @@ +{ config, lib, options, pkgs, ... }: + +let + cfg = config.services.zabbixServer; + opt = options.services.zabbixServer; + pgsql = config.services.postgresql; + mysql = config.services.mysql; + + inherit (lib) mkAfter mkDefault mkEnableOption mkIf mkMerge mkOption; + inherit (lib) attrValues concatMapStringsSep getName literalExpression optional optionalAttrs optionalString types; + inherit (lib.generators) toKeyValue; + + user = "zabbix"; + group = "zabbix"; + runtimeDir = "/run/zabbix"; + stateDir = "/var/lib/zabbix"; + passwordFile = "${runtimeDir}/zabbix-dbpassword.conf"; + + moduleEnv = pkgs.symlinkJoin { + name = "zabbix-server-module-env"; + paths = attrValues cfg.modules; + }; + + configFile = pkgs.writeText "zabbix_server.conf" (toKeyValue { listsAsDuplicateKeys = true; } cfg.settings); + + mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql"; + pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql"; + +in + +{ + imports = [ + (lib.mkRenamedOptionModule [ "services" "zabbixServer" "dbServer" ] [ "services" "zabbixServer" "database" "host" ]) + (lib.mkRemovedOptionModule [ "services" "zabbixServer" "dbPassword" ] "Use services.zabbixServer.database.passwordFile instead.") + (lib.mkRemovedOptionModule [ "services" "zabbixServer" "extraConfig" ] "Use services.zabbixServer.settings instead.") + ]; + + # interface + + options = { + + services.zabbixServer = { + enable = mkEnableOption "the Zabbix Server"; + + package = mkOption { + type = types.package; + default = if cfg.database.type == "mysql" then pkgs.zabbix.server-mysql else pkgs.zabbix.server-pgsql; + defaultText = literalExpression "pkgs.zabbix.server-pgsql"; + description = "The Zabbix package to use."; + }; + + extraPackages = mkOption { + type = types.listOf types.package; + default = with pkgs; [ nettools nmap traceroute ]; + defaultText = literalExpression "[ nettools nmap traceroute ]"; + description = '' + Packages to be added to the Zabbix {env}`PATH`. + Typically used to add executables for scripts, but can be anything. + ''; + }; + + modules = mkOption { + type = types.attrsOf types.package; + description = "A set of modules to load."; + default = {}; + example = literalExpression '' + { + "dummy.so" = pkgs.stdenv.mkDerivation { + name = "zabbix-dummy-module-''${cfg.package.version}"; + src = cfg.package.src; + buildInputs = [ cfg.package ]; + sourceRoot = "zabbix-''${cfg.package.version}/src/modules/dummy"; + installPhase = ''' + mkdir -p $out/lib + cp dummy.so $out/lib/ + '''; + }; + } + ''; + }; + + database = { + type = mkOption { + type = types.enum [ "mysql" "pgsql" ]; + example = "mysql"; + default = "pgsql"; + description = "Database engine to use."; + }; + + host = mkOption { + type = types.str; + default = "localhost"; + description = "Database host address."; + }; + + port = mkOption { + type = types.port; + default = if cfg.database.type == "mysql" then mysql.port else pgsql.settings.port; + defaultText = literalExpression '' + if config.${opt.database.type} == "mysql" + then config.${options.services.mysql.port} + else config.services.postgresql.settings.port + ''; + description = "Database host port."; + }; + + name = mkOption { + type = types.str; + default = "zabbix"; + description = "Database name."; + }; + + user = mkOption { + type = types.str; + default = "zabbix"; + description = "Database user."; + }; + + passwordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/keys/zabbix-dbpassword"; + description = '' + A file containing the password corresponding to + {option}`database.user`. + ''; + }; + + socket = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/postgresql"; + description = "Path to the unix socket file to use for authentication."; + }; + + createLocally = mkOption { + type = types.bool; + default = true; + description = "Whether to create a local database automatically."; + }; + }; + + listen = { + ip = mkOption { + type = types.str; + default = "0.0.0.0"; + description = '' + List of comma delimited IP addresses that the trapper should listen on. + Trapper will listen on all network interfaces if this parameter is missing. + ''; + }; + + port = mkOption { + type = types.port; + default = 10051; + description = '' + Listen port for trapper. + ''; + }; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = '' + Open ports in the firewall for the Zabbix Server. + ''; + }; + + settings = mkOption { + type = with types; attrsOf (oneOf [ int str (listOf str) ]); + default = {}; + description = '' + Zabbix Server configuration. Refer to + <https://www.zabbix.com/documentation/current/manual/appendix/config/zabbix_server> + for details on supported values. + ''; + example = { + CacheSize = "1G"; + SSHKeyLocation = "/var/lib/zabbix/.ssh"; + StartPingers = 32; + }; + }; + + }; + + }; + + # implementation + + config = mkIf cfg.enable { + + assertions = [ + { 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; + message = "a password cannot be specified if services.zabbixServer.database.createLocally is set to true"; + } + ]; + + services.zabbixServer.settings = mkMerge [ + { + LogType = "console"; + ListenIP = cfg.listen.ip; + ListenPort = cfg.listen.port; + # TODO: set to cfg.database.socket if database type is pgsql? + DBHost = optionalString (cfg.database.createLocally != true) cfg.database.host; + DBName = cfg.database.name; + DBUser = cfg.database.user; + PidFile = "${runtimeDir}/zabbix_server.pid"; + SocketDir = runtimeDir; + FpingLocation = "/run/wrappers/bin/fping"; + LoadModule = builtins.attrNames cfg.modules; + } + (mkIf (cfg.database.createLocally != true) { DBPort = cfg.database.port; }) + (mkIf (cfg.database.passwordFile != null) { Include = [ "${passwordFile}" ]; }) + (mkIf (mysqlLocal && cfg.database.socket != null) { DBSocket = cfg.database.socket; }) + (mkIf (cfg.modules != {}) { LoadModulePath = "${moduleEnv}/lib"; }) + ]; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.listen.port ]; + }; + + services.mysql = optionalAttrs mysqlLocal { + enable = true; + package = mkDefault pkgs.mariadb; + }; + + systemd.services.mysql.postStart = mkAfter (optionalString mysqlLocal '' + ( echo "CREATE DATABASE IF NOT EXISTS \`${cfg.database.name}\` CHARACTER SET utf8 COLLATE utf8_bin;" + echo "CREATE USER IF NOT EXISTS '${cfg.database.user}'@'localhost' IDENTIFIED WITH ${if (getName config.services.mysql.package == getName pkgs.mariadb) then "unix_socket" else "auth_socket"};" + echo "GRANT ALL PRIVILEGES ON \`${cfg.database.name}\`.* TO '${cfg.database.user}'@'localhost';" + ) | ${config.services.mysql.package}/bin/mysql -N + ''); + + services.postgresql = optionalAttrs pgsqlLocal { + enable = true; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { name = cfg.database.user; + ensureDBOwnership = true; + } + ]; + }; + + users.users.${user} = { + description = "Zabbix daemon user"; + uid = config.ids.uids.zabbix; + inherit group; + }; + + users.groups.${group} = { + gid = config.ids.gids.zabbix; + }; + + security.wrappers = { + fping = + { setuid = true; + owner = "root"; + group = "root"; + source = "${pkgs.fping}/bin/fping"; + }; + }; + + systemd.services.zabbix-server = { + description = "Zabbix Server"; + + wantedBy = [ "multi-user.target" ]; + after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service"; + + path = [ "/run/wrappers" ] ++ cfg.extraPackages; + preStart = '' + # pre 19.09 compatibility + if test -e "${runtimeDir}/db-created"; then + mv "${runtimeDir}/db-created" "${stateDir}/" + fi + '' + optionalString pgsqlLocal '' + if ! test -e "${stateDir}/db-created"; then + cat ${cfg.package}/share/zabbix/database/postgresql/schema.sql | ${pgsql.package}/bin/psql ${cfg.database.name} + cat ${cfg.package}/share/zabbix/database/postgresql/images.sql | ${pgsql.package}/bin/psql ${cfg.database.name} + cat ${cfg.package}/share/zabbix/database/postgresql/data.sql | ${pgsql.package}/bin/psql ${cfg.database.name} + touch "${stateDir}/db-created" + fi + '' + optionalString mysqlLocal '' + if ! test -e "${stateDir}/db-created"; then + cat ${cfg.package}/share/zabbix/database/mysql/schema.sql | ${mysql.package}/bin/mysql ${cfg.database.name} + cat ${cfg.package}/share/zabbix/database/mysql/images.sql | ${mysql.package}/bin/mysql ${cfg.database.name} + cat ${cfg.package}/share/zabbix/database/mysql/data.sql | ${mysql.package}/bin/mysql ${cfg.database.name} + touch "${stateDir}/db-created" + fi + '' + optionalString (cfg.database.passwordFile != null) '' + # create a copy of the supplied password file in a format zabbix can consume + install -m 0600 <(echo "DBPassword = $(cat ${cfg.database.passwordFile})") ${passwordFile} + ''; + + serviceConfig = { + ExecStart = "@${cfg.package}/sbin/zabbix_server zabbix_server -f --config ${configFile}"; + Restart = "always"; + RestartSec = 2; + + User = user; + Group = group; + RuntimeDirectory = "zabbix"; + StateDirectory = "zabbix"; + PrivateTmp = true; + }; + }; + + systemd.services.httpd.after = + optional (config.services.zabbixWeb.enable && mysqlLocal) "mysql.service" ++ + optional (config.services.zabbixWeb.enable && pgsqlLocal) "postgresql.service"; + + }; + +} |