Files
nixpkgs/nixos/modules/services/monitoring/librenms.nix
Sam Grimee dc6c6e42e4 nixos/librenms: add socket auth for mysql
This allows librenms to use socket authentication to the mysql package out of the box if installed under
the same username, avoiding complex DB password initialization steps.
2024-08-07 11:32:04 +02:00

658 lines
22 KiB
Nix

{ 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.
Ignored if 'socket' is defined.
'';
};
port = mkOption {
type = types.port;
default = 3306;
description = ''
Port of the MySQL/MariaDB server.
Ignored if 'socket' is defined.
'';
};
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.
Ignored if 'socket' is defined.
'';
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
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.
Ignored if 'socket' is defined, mandatory otherwise.
'';
};
socket = mkOption {
type = types.nullOr types.str;
default = null;
example = "/run/mysqld/mysqld.sock";
description = ''
A unix socket to mysql, accessible by the librenms user.
Useful when mysql is on the localhost.
'';
};
};
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_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
''
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 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;
}