# SPDX-FileCopyrightText: 2024 Tom Hubrecht # # SPDX-License-Identifier: EUPL-1.2 { config, lib, pkgs, modulesPath, ... }: let inherit (lib) literalExpression mkEnableOption mkOption recursiveUpdate types ; cfg = config.services.librenms; settingsFormat = pkgs.formats.json { }; configJson = settingsFormat.generate "librenms-config.json" cfg.settings; inherit (cfg) package; 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" '' 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"; }); memcached = lib.mkIf cfg.useDistributedPollers { enable = true; listen = "0.0.0.0"; }; 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}'@'%'; '' ); }; 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)(/.+)$; ''; } ]; }; phpfpm.pools.librenms = lib.mkIf (!cfg.distributedPoller.enable) { inherit (cfg) group user; inherit (package) phpPackage; inherit phpOptions; settings = { "listen.mode" = "0660"; "listen.owner" = config.services.nginx.user; "listen.group" = config.services.nginx.group; } // cfg.poolConfig; }; 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; }; }; 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" ]; }; }; 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"; }; }; 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"; }; }; 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 ''; }; }; timers.librenms-scheduler = { description = "LibreNMS Scheduler"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = "minutely"; AccuracySec = "1second"; }; }; 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} - -" ]; }; programs.mtr.enable = true; security.wrappers = { fping = { setuid = true; owner = "root"; group = "root"; source = "${pkgs.fping}/bin/fping"; }; }; environment.systemPackages = [ artisanWrapper lnmsWrapper ]; }; meta.maintainers = lib.teams.wdz.members; }