diff options
Diffstat (limited to 'nixos/modules/services/monitoring/librenms.nix')
-rw-r--r-- | nixos/modules/services/monitoring/librenms.nix | 624 |
1 files changed, 624 insertions, 0 deletions
diff --git a/nixos/modules/services/monitoring/librenms.nix b/nixos/modules/services/monitoring/librenms.nix new file mode 100644 index 00000000000..08a46754e0e --- /dev/null +++ b/nixos/modules/services/monitoring/librenms.nix @@ -0,0 +1,624 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.librenms; + settingsFormat = pkgs.formats.json {}; + configJson = settingsFormat.generate "librenms-config.json" cfg.settings; + + package = pkgs.librenms.override { + logDir = cfg.logDir; + dataDir = cfg.dataDir; + }; + + phpOptions = '' + log_errors = on + post_max_size = 100M + upload_max_filesize = 100M + date.timezone = "${config.time.timeZone}" + ''; + phpIni = pkgs.runCommand "php.ini" { + inherit (package) phpPackage; + inherit phpOptions; + preferLocalBuild = true; + passAsFile = [ "phpOptions" ]; + } '' + cat $phpPackage/etc/php.ini $phpOptionsPath > $out + ''; + + artisanWrapper = pkgs.writeShellScriptBin "librenms-artisan" '' + cd ${package} + sudo=exec + if [[ "$USER" != ${cfg.user} ]]; then + sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}' + fi + $sudo ${package}/artisan $* + ''; + + lnmsWrapper = pkgs.writeShellScriptBin "lnms" '' + cd ${package} + exec ${package}/lnms $* + ''; + + configFile = pkgs.writeText "config.php" '' + <?php + $new_config = json_decode(file_get_contents("${cfg.dataDir}/config.json"), true); + $config = ($config == null) ? $new_config : array_merge($config, $new_config); + + ${lib.optionalString (cfg.extraConfig != null) cfg.extraConfig} + ''; + +in { + options.services.librenms = with lib; { + enable = mkEnableOption "LibreNMS network monitoring system"; + + user = mkOption { + type = types.str; + default = "librenms"; + description = '' + Name of the LibreNMS user. + ''; + }; + + group = mkOption { + type = types.str; + default = "librenms"; + description = '' + Name of the LibreNMS group. + ''; + }; + + hostname = mkOption { + type = types.str; + default = config.networking.fqdnOrHostName; + defaultText = literalExpression "config.networking.fqdnOrHostName"; + description = '' + The hostname to serve LibreNMS on. + ''; + }; + + pollerThreads = mkOption { + type = types.int; + default = 16; + description = '' + Amount of threads of the cron-poller. + ''; + }; + + enableOneMinutePolling = mkOption { + type = types.bool; + default = false; + description = '' + Enables the [1-Minute Polling](https://docs.librenms.org/Support/1-Minute-Polling/). + Changing this option will automatically convert your existing rrd files. + ''; + }; + + useDistributedPollers = mkOption { + type = types.bool; + default = false; + description = '' + Enables (distributed pollers)[https://docs.librenms.org/Extensions/Distributed-Poller/] + for this LibreNMS instance. This will enable a local `rrdcached` and `memcached` server. + + To use this feature, make sure to configure your firewall that the distributed pollers + can reach the local `mysql`, `rrdcached` and `memcached` ports. + ''; + }; + + distributedPoller = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Configure this LibreNMS instance as a (distributed poller)[https://docs.librenms.org/Extensions/Distributed-Poller/]. + This will disable all web features and just configure the poller features. + Use the `mysql` database of your main LibreNMS instance in the database settings. + ''; + }; + + name = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Custom name of this poller. + ''; + }; + + group = mkOption { + type = types.str; + default = "0"; + example = "1,2"; + description = '' + Group(s) of this poller. + ''; + }; + + distributedBilling = mkOption { + type = types.bool; + default = false; + description = '' + Enable distributed billing on this poller. + ''; + }; + + memcachedHost = mkOption { + type = types.str; + description = '' + Hostname or IP of the `memcached` server. + ''; + }; + + memcachedPort = mkOption { + type = types.port; + default = 11211; + description = '' + Port of the `memcached` server. + ''; + }; + + rrdcachedHost = mkOption { + type = types.str; + description = '' + Hostname or IP of the `rrdcached` server. + ''; + }; + + rrdcachedPort = mkOption { + type = types.port; + default = 42217; + description = '' + Port of the `memcached` server. + ''; + }; + }; + + poolConfig = mkOption { + type = with types; attrsOf (oneOf [ str int bool ]); + default = { + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 4; + "pm.max_requests" = 500; + }; + description = '' + Options for the LibreNMS PHP pool. See the documentation on `php-fpm.conf` + for details on configuration directives. + ''; + }; + + nginx = mkOption { + type = types.submodule ( + recursiveUpdate + (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {} + ); + default = { }; + example = literalExpression '' + { + serverAliases = [ + "librenms.''${config.networking.domain}" + ]; + # To enable encryption and let let's encrypt take care of certificate + forceSSL = true; + enableACME = true; + # To set the LibreNMS virtualHost as the default virtualHost; + default = true; + } + ''; + description = '' + With this option, you can customize the nginx virtualHost settings. + ''; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/librenms"; + description = '' + Path of the LibreNMS state directory. + ''; + }; + + logDir = mkOption { + type = types.path; + default = "/var/log/librenms"; + description = '' + Path of the LibreNMS logging directory. + ''; + }; + + database = { + createLocally = mkOption { + type = types.bool; + default = false; + description = '' + Whether to create a local database automatically. + ''; + }; + + host = mkOption { + default = "localhost"; + description = '' + Hostname or IP of the MySQL/MariaDB server. + ''; + }; + + port = mkOption { + type = types.port; + default = 3306; + description = '' + Port of the MySQL/MariaDB server. + ''; + }; + + database = mkOption { + type = types.str; + default = "librenms"; + description = '' + Name of the database on the MySQL/MariaDB server. + ''; + }; + + username = mkOption { + type = types.str; + default = "librenms"; + description = '' + Name of the user on the MySQL/MariaDB server. + ''; + }; + + passwordFile = mkOption { + type = types.path; + example = "/run/secrets/mysql.pass"; + description = '' + A file containing the password for the user of the MySQL/MariaDB server. + Must be readable for the LibreNMS user. + ''; + }; + }; + + environmentFile = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + File containing env-vars to be substituted into the final config. Useful for secrets. + Does not apply to settings defined in `extraConfig`. + ''; + }; + + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = {}; + }; + description = '' + Attrset of the LibreNMS configuration. + See https://docs.librenms.org/Support/Configuration/ for reference. + All possible options are listed [here](https://github.com/librenms/librenms/blob/master/misc/config_definitions.json). + See https://docs.librenms.org/Extensions/Authentication/ for setting other authentication methods. + ''; + default = { }; + example = { + base_url = "/librenms/"; + top_devices = true; + top_ports = false; + }; + }; + + extraConfig = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Additional config for LibreNMS that will be appended to the `config.php`. See + https://github.com/librenms/librenms/blob/master/misc/config_definitions.json + for possible options. Useful if you want to use PHP-Functions in your config. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = config.time.timeZone != null; + message = "You must set `time.timeZone` to use the LibreNMS module."; + } + { + assertion = cfg.database.createLocally -> cfg.database.host == "localhost"; + message = "The database host must be \"localhost\" if services.librenms.database.createLocally is set to true."; + } + { + assertion = !(cfg.useDistributedPollers && cfg.distributedPoller.enable); + message = "The LibreNMS instance can't be a distributed poller and a full instance at the same time."; + } + ]; + + users.users.${cfg.user} = { + group = "${cfg.group}"; + isSystemUser = true; + }; + + users.groups.${cfg.group} = { }; + + services.librenms.settings = { + # basic configs + "user" = cfg.user; + "own_hostname" = cfg.hostname; + "base_url" = lib.mkDefault "/"; + "auth_mechanism" = lib.mkDefault "mysql"; + + # disable auto update function (won't work with NixOS) + "update" = false; + + # enable fast ping by default + "ping_rrd_step" = 60; + + # one minute polling + "rrd.step" = if cfg.enableOneMinutePolling then 60 else 300; + "rrd.heartbeat" = if cfg.enableOneMinutePolling then 120 else 600; + } // (lib.optionalAttrs cfg.distributedPoller.enable { + "distributed_poller" = true; + "distributed_poller_name" = lib.mkIf (cfg.distributedPoller.name != null) cfg.distributedPoller.name; + "distributed_poller_group" = cfg.distributedPoller.group; + "distributed_billing" = cfg.distributedPoller.distributedBilling; + "distributed_poller_memcached_host" = cfg.distributedPoller.memcachedHost; + "distributed_poller_memcached_port" = cfg.distributedPoller.memcachedPort; + "rrdcached" = "${cfg.distributedPoller.rrdcachedHost}:${toString cfg.distributedPoller.rrdcachedPort}"; + }) // (lib.optionalAttrs cfg.useDistributedPollers { + "distributed_poller" = true; + # still enable a local poller with distributed polling + "distributed_poller_group" = lib.mkDefault "0"; + "distributed_billing" = lib.mkDefault true; + "distributed_poller_memcached_host" = "localhost"; + "distributed_poller_memcached_port" = 11211; + "rrdcached" = "localhost:42217"; + }); + + services.memcached = lib.mkIf cfg.useDistributedPollers { + enable = true; + listen = "0.0.0.0"; + }; + + systemd.services.rrdcached = lib.mkIf cfg.useDistributedPollers { + description = "rrdcached"; + after = [ "librenms-setup.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "forking"; + User = cfg.user; + Group = cfg.group; + LimitNOFILE = 16384; + RuntimeDirectory = "rrdcached"; + PidFile = "/run/rrdcached/rrdcached.pid"; + # rrdcached params from https://docs.librenms.org/Extensions/Distributed-Poller/#config-sample + ExecStart = "${pkgs.rrdtool}/bin/rrdcached -l 0:42217 -R -j ${cfg.dataDir}/rrdcached-journal/ -F -b ${cfg.dataDir}/rrd -B -w 1800 -z 900 -p /run/rrdcached/rrdcached.pid"; + }; + }; + + services.mysql = lib.mkIf cfg.database.createLocally { + enable = true; + package = lib.mkDefault pkgs.mariadb; + settings.mysqld = { + innodb_file_per_table = 1; + lower_case_table_names = 0; + } // (lib.optionalAttrs cfg.useDistributedPollers { + bind-address = "0.0.0.0"; + }); + ensureDatabases = [ cfg.database.database ]; + ensureUsers = [ + { + name = cfg.database.username; + ensurePermissions = { + "${cfg.database.database}.*" = "ALL PRIVILEGES"; + }; + } + ]; + initialScript = lib.mkIf cfg.useDistributedPollers (pkgs.writeText "mysql-librenms-init" '' + CREATE USER IF NOT EXISTS '${cfg.database.username}'@'%'; + GRANT ALL PRIVILEGES ON ${cfg.database.database}.* TO '${cfg.database.username}'@'%'; + ''); + }; + + services.nginx = lib.mkIf (!cfg.distributedPoller.enable) { + enable = true; + virtualHosts."${cfg.hostname}" = lib.mkMerge [ + cfg.nginx + { + root = lib.mkForce "${package}/html"; + locations."/" = { + index = "index.php"; + tryFiles = "$uri $uri/ /index.php?$query_string"; + }; + locations."~ .php$".extraConfig = '' + fastcgi_pass unix:${config.services.phpfpm.pools."librenms".socket}; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + ''; + } + ]; + }; + + services.phpfpm.pools.librenms = lib.mkIf (!cfg.distributedPoller.enable) { + user = cfg.user; + group = cfg.group; + inherit (package) phpPackage; + inherit phpOptions; + settings = { + "listen.mode" = "0660"; + "listen.owner" = config.services.nginx.user; + "listen.group" = config.services.nginx.group; + } // cfg.poolConfig; + }; + + systemd.services.librenms-scheduler = { + description = "LibreNMS Scheduler"; + path = [ pkgs.unixtools.whereis ]; + serviceConfig = { + Type = "oneshot"; + WorkingDirectory = package; + User = cfg.user; + Group = cfg.group; + ExecStart = "${artisanWrapper}/bin/librenms-artisan schedule:run"; + }; + }; + + systemd.timers.librenms-scheduler = { + description = "LibreNMS Scheduler"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "minutely"; + AccuracySec = "1second"; + }; + }; + + systemd.services.librenms-setup = { + description = "Preparation tasks for LibreNMS"; + before = [ "phpfpm-librenms.service" ]; + after = [ "systemd-tmpfiles-setup.service" ] + ++ (lib.optional (cfg.database.host == "localhost") "mysql.service"); + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ package configFile ]; + path = [ pkgs.mariadb pkgs.unixtools.whereis pkgs.gnused ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; + User = cfg.user; + Group = cfg.group; + ExecStartPre = lib.mkIf cfg.database.createLocally [ "!${pkgs.writeShellScript "librenms-db-init" '' + DB_PASSWORD=$(cat ${cfg.database.passwordFile} | tr -d '\n') + echo "ALTER USER '${cfg.database.username}'@'localhost' IDENTIFIED BY '$DB_PASSWORD';" | ${pkgs.mariadb}/bin/mysql + ${lib.optionalString cfg.useDistributedPollers '' + echo "ALTER USER '${cfg.database.username}'@'%' IDENTIFIED BY '$DB_PASSWORD';" | ${pkgs.mariadb}/bin/mysql + ''} + ''}"]; + }; + script = '' + set -euo pipefail + + # config setup + ln -sf ${configFile} ${cfg.dataDir}/config.php + ${pkgs.envsubst}/bin/envsubst -i ${configJson} -o ${cfg.dataDir}/config.json + export PHPRC=${phpIni} + + if [[ ! -s ${cfg.dataDir}/.env ]]; then + # init .env file + echo "APP_KEY=" > ${cfg.dataDir}/.env + ${artisanWrapper}/bin/librenms-artisan key:generate --ansi + ${artisanWrapper}/bin/librenms-artisan webpush:vapid + echo "" >> ${cfg.dataDir}/.env + echo -n "NODE_ID=" >> ${cfg.dataDir}/.env + ${package.phpPackage}/bin/php -r "echo uniqid();" >> ${cfg.dataDir}/.env + echo "" >> ${cfg.dataDir}/.env + else + # .env file already exists --> only update database and cache config + ${pkgs.gnused}/bin/sed -i /^DB_/d ${cfg.dataDir}/.env + ${pkgs.gnused}/bin/sed -i /^CACHE_DRIVER/d ${cfg.dataDir}/.env + fi + ${lib.optionalString (cfg.useDistributedPollers || cfg.distributedPoller.enable) '' + echo "CACHE_DRIVER=memcached" >> ${cfg.dataDir}/.env + ''} + echo "DB_HOST=${cfg.database.host}" >> ${cfg.dataDir}/.env + echo "DB_PORT=${toString cfg.database.port}" >> ${cfg.dataDir}/.env + echo "DB_DATABASE=${cfg.database.database}" >> ${cfg.dataDir}/.env + echo "DB_USERNAME=${cfg.database.username}" >> ${cfg.dataDir}/.env + echo -n "DB_PASSWORD=" >> ${cfg.dataDir}/.env + cat ${cfg.database.passwordFile} >> ${cfg.dataDir}/.env + + # clear cache after update + OLD_VERSION=$(cat ${cfg.dataDir}/version) + if [[ $OLD_VERSION != "${package.version}" ]]; then + rm -r ${cfg.dataDir}/cache/* + echo "${package.version}" > ${cfg.dataDir}/version + fi + + # convert rrd files when the oneMinutePolling option is changed + OLD_ENABLED=$(cat ${cfg.dataDir}/one_minute_enabled) + if [[ $OLD_ENABLED != "${lib.boolToString cfg.enableOneMinutePolling}" ]]; then + ${package}/scripts/rrdstep.php -h all + echo "${lib.boolToString cfg.enableOneMinutePolling}" > ${cfg.dataDir}/one_minute_enabled + fi + + # migrate db + ${artisanWrapper}/bin/librenms-artisan migrate --force --no-interaction + ''; + }; + + programs.mtr.enable = true; + + services.logrotate = { + enable = true; + settings."${cfg.logDir}/librenms.log" = { + su = "${cfg.user} ${cfg.group}"; + create = "0640 ${cfg.user} ${cfg.group}"; + rotate = 6; + frequency = "weekly"; + compress = true; + delaycompress = true; + missingok = true; + notifempty = true; + }; + }; + + services.cron = { + enable = true; + systemCronJobs = let + env = "PHPRC=${phpIni}"; + in [ + # based on crontab provided by LibreNMS + "33 */6 * * * ${cfg.user} ${env} ${package}/cronic ${package}/discovery-wrapper.py 1" + "*/5 * * * * ${cfg.user} ${env} ${package}/discovery.php -h new >> /dev/null 2>&1" + + "${if cfg.enableOneMinutePolling then "*" else "*/5"} * * * * ${cfg.user} ${env} ${package}/cronic ${package}/poller-wrapper.py ${toString cfg.pollerThreads}" + "* * * * * ${cfg.user} ${env} ${package}/alerts.php >> /dev/null 2>&1" + + "*/5 * * * * ${cfg.user} ${env} ${package}/poll-billing.php >> /dev/null 2>&1" + "01 * * * * ${cfg.user} ${env} ${package}/billing-calculate.php >> /dev/null 2>&1" + "*/5 * * * * ${cfg.user} ${env} ${package}/check-services.php >> /dev/null 2>&1" + + # extra: fast ping + "* * * * * ${cfg.user} ${env} ${package}/ping.php >> /dev/null 2>&1" + + # daily.sh tasks are split to exclude update + "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh cleanup >> /dev/null 2>&1" + "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh notifications >> /dev/null 2>&1" + "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh peeringdb >> /dev/null 2>&1" + "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh mac_oui >> /dev/null 2>&1" + ]; + }; + + security.wrappers = { + fping = { + setuid = true; + owner = "root"; + group = "root"; + source = "${pkgs.fping}/bin/fping"; + }; + }; + + environment.systemPackages = [ artisanWrapper lnmsWrapper ]; + + systemd.tmpfiles.rules = [ + "d ${cfg.logDir} 0750 ${cfg.user} ${cfg.group} - -" + "f ${cfg.logDir}/librenms.log 0640 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -" + "f ${cfg.dataDir}/.env 0600 ${cfg.user} ${cfg.group} - -" + "f ${cfg.dataDir}/version 0600 ${cfg.user} ${cfg.group} - -" + "f ${cfg.dataDir}/one_minute_enabled 0600 ${cfg.user} ${cfg.group} - -" + "f ${cfg.dataDir}/config.json 0600 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/app 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/debugbar 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/framework 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/framework/cache 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/framework/sessions 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/framework/views 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/storage/logs 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/rrd 0700 ${cfg.user} ${cfg.group} - -" + "d ${cfg.dataDir}/cache 0700 ${cfg.user} ${cfg.group} - -" + ] ++ lib.optionals cfg.useDistributedPollers [ + "d ${cfg.dataDir}/rrdcached-journal 0700 ${cfg.user} ${cfg.group} - -" + ]; + + }; + + meta.maintainers = lib.teams.wdz.members; +} |