{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.librenms;
settingsFormat = pkgs.formats.json { };
configJson = settingsFormat.generate "librenms-config.json" cfg.settings;
package = cfg.package.override {
logDir = cfg.logDir;
dataDir = cfg.dataDir;
};
phpOptions = ''
log_errors = on
post_max_size = 100M
upload_max_filesize = 100M
memory_limit = ${toString cfg.settings.php_memory_limit}M
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}
sudo=exec
if [[ "$USER" != ${cfg.user} ]]; then
sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}'
fi
$sudo ${package}/lnms "$@"
'';
configFile = pkgs.writeText "config.php" ''
for reference.
All possible options are listed [here](https://github.com/librenms/librenms/blob/master/misc/config_definitions.json).
See 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
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;
# set default memory limit to 1G
"php_memory_limit" = lib.mkDefault 1024;
# 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"
"network.target"
] ++ (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" (
if !isNull cfg.database.socket then
''
echo "ALTER USER '${cfg.database.username}'@'localhost' IDENTIFIED VIA unix_socket;" | ${pkgs.mariadb}/bin/mysql --socket='${cfg.database.socket}'
${lib.optionalString cfg.useDistributedPollers ''
echo "ALTER USER '${cfg.database.username}'@'%' IDENTIFIED VIA unix_socket;" | ${pkgs.mariadb}/bin/mysql --socket='${cfg.database.socket}'
''}
''
else
''
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}
INIT=false
if [[ ! -s ${cfg.dataDir}/.env ]]; then
INIT=true
# 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_DATABASE=${cfg.database.database}" >> ${cfg.dataDir}/.env
''
+ (
if !isNull cfg.database.socket then
''
# use socket connection
echo "DB_SOCKET=${cfg.database.socket}" >> ${cfg.dataDir}/.env
echo "DB_PASSWORD=null" >> ${cfg.dataDir}/.env
''
else
''
# use TCP connection
echo "DB_HOST=${cfg.database.host}" >> ${cfg.dataDir}/.env
echo "DB_PORT=${toString cfg.database.port}" >> ${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 if package has changed (cache may contain cached paths
# to the old package)
OLD_PACKAGE=$(cat ${cfg.dataDir}/package)
if [[ $OLD_PACKAGE != "${package}" ]]; then
rm -r ${cfg.dataDir}/cache/*
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 if package version has changed
# not necessary for every package change
OLD_VERSION=$(cat ${cfg.dataDir}/version)
if [[ $OLD_VERSION != "${package.version}" ]]; then
${artisanWrapper}/bin/librenms-artisan migrate --force --no-interaction
echo "${package.version}" > ${cfg.dataDir}/version
fi
if [[ $INIT == "true" ]]; then
${artisanWrapper}/bin/librenms-artisan db:seed --force --no-interaction
fi
# regenerate cache if package has changed
if [[ $OLD_PACKAGE != "${package}" ]]; then
${artisanWrapper}/bin/librenms-artisan view:clear
${artisanWrapper}/bin/librenms-artisan optimize:clear
${artisanWrapper}/bin/librenms-artisan view:cache
${artisanWrapper}/bin/librenms-artisan optimize
echo "${package}" > ${cfg.dataDir}/package
fi
'';
};
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}/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"
]
++ lib.optionals cfg.enableLocalBilling [
"*/5 * * * * ${cfg.user} ${env} ${package}/poll-billing.php >> /dev/null 2>&1"
"01 * * * * ${cfg.user} ${env} ${package}/billing-calculate.php >> /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}/package 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 = with lib.maintainers; [ netali ] ++ lib.teams.wdz.members;
}