1 { config, lib, pkgs, ... }:
4 cfg = config.services.librenms;
5 settingsFormat = pkgs.formats.json { };
6 configJson = settingsFormat.generate "librenms-config.json" cfg.settings;
8 package = pkgs.librenms.override {
10 dataDir = cfg.dataDir;
16 upload_max_filesize = 100M
17 date.timezone = "${config.time.timeZone}"
19 phpIni = pkgs.runCommand "php.ini"
21 inherit (package) phpPackage;
23 preferLocalBuild = true;
24 passAsFile = [ "phpOptions" ];
26 cat $phpPackage/etc/php.ini $phpOptionsPath > $out
29 artisanWrapper = pkgs.writeShellScriptBin "librenms-artisan" ''
32 if [[ "$USER" != ${cfg.user} ]]; then
33 sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}'
35 $sudo ${package}/artisan "$@"
38 lnmsWrapper = pkgs.writeShellScriptBin "lnms" ''
41 if [[ "$USER" != ${cfg.user} ]]; then
42 sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}'
44 $sudo ${package}/lnms "$@"
49 configFile = pkgs.writeText "config.php" ''
51 $new_config = json_decode(file_get_contents("${cfg.dataDir}/config.json"), true);
52 $config = ($config == null) ? $new_config : array_merge($config, $new_config);
54 ${lib.optionalString (cfg.extraConfig != null) cfg.extraConfig}
59 options.services.librenms = with lib; {
60 enable = mkEnableOption "LibreNMS network monitoring system";
66 Name of the LibreNMS user.
74 Name of the LibreNMS group.
80 default = config.networking.fqdnOrHostName;
81 defaultText = literalExpression "config.networking.fqdnOrHostName";
83 The hostname to serve LibreNMS on.
87 pollerThreads = mkOption {
91 Amount of threads of the cron-poller.
95 enableOneMinutePolling = mkOption {
99 Enables the [1-Minute Polling](https://docs.librenms.org/Support/1-Minute-Polling/).
100 Changing this option will automatically convert your existing rrd files.
104 useDistributedPollers = mkOption {
108 Enables (distributed pollers)[https://docs.librenms.org/Extensions/Distributed-Poller/]
109 for this LibreNMS instance. This will enable a local `rrdcached` and `memcached` server.
111 To use this feature, make sure to configure your firewall that the distributed pollers
112 can reach the local `mysql`, `rrdcached` and `memcached` ports.
116 distributedPoller = {
121 Configure this LibreNMS instance as a (distributed poller)[https://docs.librenms.org/Extensions/Distributed-Poller/].
122 This will disable all web features and just configure the poller features.
123 Use the `mysql` database of your main LibreNMS instance in the database settings.
128 type = types.nullOr types.str;
131 Custom name of this poller.
140 Group(s) of this poller.
144 distributedBilling = mkOption {
148 Enable distributed billing on this poller.
152 memcachedHost = mkOption {
155 Hostname or IP of the `memcached` server.
159 memcachedPort = mkOption {
163 Port of the `memcached` server.
167 rrdcachedHost = mkOption {
170 Hostname or IP of the `rrdcached` server.
174 rrdcachedPort = mkOption {
178 Port of the `memcached` server.
183 poolConfig = mkOption {
184 type = with types; attrsOf (oneOf [ str int bool ]);
187 "pm.max_children" = 32;
188 "pm.start_servers" = 2;
189 "pm.min_spare_servers" = 2;
190 "pm.max_spare_servers" = 4;
191 "pm.max_requests" = 500;
194 Options for the LibreNMS PHP pool. See the documentation on `php-fpm.conf`
195 for details on configuration directives.
200 type = types.submodule (
202 (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
206 example = literalExpression ''
209 "librenms.''${config.networking.domain}"
211 # To enable encryption and let let's encrypt take care of certificate
214 # To set the LibreNMS virtualHost as the default virtualHost;
219 With this option, you can customize the nginx virtualHost settings.
225 default = "/var/lib/librenms";
227 Path of the LibreNMS state directory.
233 default = "/var/log/librenms";
235 Path of the LibreNMS logging directory.
240 createLocally = mkOption {
244 Whether to create a local database automatically.
249 default = "localhost";
251 Hostname or IP of the MySQL/MariaDB server.
252 Ignored if 'socket' is defined.
260 Port of the MySQL/MariaDB server.
261 Ignored if 'socket' is defined.
265 database = mkOption {
267 default = "librenms";
269 Name of the database on the MySQL/MariaDB server.
273 username = mkOption {
275 default = "librenms";
277 Name of the user on the MySQL/MariaDB server.
278 Ignored if 'socket' is defined.
282 passwordFile = mkOption {
283 type = types.nullOr types.path;
285 example = "/run/secrets/mysql.pass";
287 A file containing the password for the user of the MySQL/MariaDB server.
288 Must be readable for the LibreNMS user.
289 Ignored if 'socket' is defined, mandatory otherwise.
294 type = types.nullOr types.str;
296 example = "/run/mysqld/mysqld.sock";
298 A unix socket to mysql, accessible by the librenms user.
299 Useful when mysql is on the localhost.
304 environmentFile = mkOption {
305 type = types.nullOr types.str;
308 File containing env-vars to be substituted into the final config. Useful for secrets.
309 Does not apply to settings defined in `extraConfig`.
313 settings = mkOption {
314 type = types.submodule {
315 freeformType = settingsFormat.type;
319 Attrset of the LibreNMS configuration.
320 See https://docs.librenms.org/Support/Configuration/ for reference.
321 All possible options are listed [here](https://github.com/librenms/librenms/blob/master/misc/config_definitions.json).
322 See https://docs.librenms.org/Extensions/Authentication/ for setting other authentication methods.
326 base_url = "/librenms/";
332 extraConfig = mkOption {
333 type = types.nullOr types.str;
336 Additional config for LibreNMS that will be appended to the `config.php`. See
337 https://github.com/librenms/librenms/blob/master/misc/config_definitions.json
338 for possible options. Useful if you want to use PHP-Functions in your config.
343 config = lib.mkIf cfg.enable {
346 assertion = config.time.timeZone != null;
347 message = "You must set `time.timeZone` to use the LibreNMS module.";
350 assertion = cfg.database.createLocally -> cfg.database.host == "localhost";
351 message = "The database host must be \"localhost\" if services.librenms.database.createLocally is set to true.";
354 assertion = !(cfg.useDistributedPollers && cfg.distributedPoller.enable);
355 message = "The LibreNMS instance can't be a distributed poller and a full instance at the same time.";
359 users.users.${cfg.user} = {
360 group = "${cfg.group}";
364 users.groups.${cfg.group} = { };
366 services.librenms.settings = {
369 "own_hostname" = cfg.hostname;
370 "base_url" = lib.mkDefault "/";
371 "auth_mechanism" = lib.mkDefault "mysql";
373 # disable auto update function (won't work with NixOS)
376 # enable fast ping by default
377 "ping_rrd_step" = 60;
380 "rrd.step" = if cfg.enableOneMinutePolling then 60 else 300;
381 "rrd.heartbeat" = if cfg.enableOneMinutePolling then 120 else 600;
382 } // (lib.optionalAttrs cfg.distributedPoller.enable {
383 "distributed_poller" = true;
384 "distributed_poller_name" = lib.mkIf (cfg.distributedPoller.name != null) cfg.distributedPoller.name;
385 "distributed_poller_group" = cfg.distributedPoller.group;
386 "distributed_billing" = cfg.distributedPoller.distributedBilling;
387 "distributed_poller_memcached_host" = cfg.distributedPoller.memcachedHost;
388 "distributed_poller_memcached_port" = cfg.distributedPoller.memcachedPort;
389 "rrdcached" = "${cfg.distributedPoller.rrdcachedHost}:${toString cfg.distributedPoller.rrdcachedPort}";
390 }) // (lib.optionalAttrs cfg.useDistributedPollers {
391 "distributed_poller" = true;
392 # still enable a local poller with distributed polling
393 "distributed_poller_group" = lib.mkDefault "0";
394 "distributed_billing" = lib.mkDefault true;
395 "distributed_poller_memcached_host" = "localhost";
396 "distributed_poller_memcached_port" = 11211;
397 "rrdcached" = "localhost:42217";
400 services.memcached = lib.mkIf cfg.useDistributedPollers {
405 systemd.services.rrdcached = lib.mkIf cfg.useDistributedPollers {
406 description = "rrdcached";
407 after = [ "librenms-setup.service" ];
408 wantedBy = [ "multi-user.target" ];
414 RuntimeDirectory = "rrdcached";
415 PidFile = "/run/rrdcached/rrdcached.pid";
416 # rrdcached params from https://docs.librenms.org/Extensions/Distributed-Poller/#config-sample
417 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";
421 services.mysql = lib.mkIf cfg.database.createLocally {
423 package = lib.mkDefault pkgs.mariadb;
425 innodb_file_per_table = 1;
426 lower_case_table_names = 0;
427 } // (lib.optionalAttrs cfg.useDistributedPollers {
428 bind-address = "0.0.0.0";
430 ensureDatabases = [ cfg.database.database ];
433 name = cfg.database.username;
434 ensurePermissions = {
435 "${cfg.database.database}.*" = "ALL PRIVILEGES";
439 initialScript = lib.mkIf cfg.useDistributedPollers (pkgs.writeText "mysql-librenms-init" ''
440 CREATE USER IF NOT EXISTS '${cfg.database.username}'@'%';
441 GRANT ALL PRIVILEGES ON ${cfg.database.database}.* TO '${cfg.database.username}'@'%';
445 services.nginx = lib.mkIf (!cfg.distributedPoller.enable) {
447 virtualHosts."${cfg.hostname}" = lib.mkMerge [
450 root = lib.mkForce "${package}/html";
453 tryFiles = "$uri $uri/ /index.php?$query_string";
455 locations."~ .php$".extraConfig = ''
456 fastcgi_pass unix:${config.services.phpfpm.pools."librenms".socket};
457 fastcgi_split_path_info ^(.+\.php)(/.+)$;
463 services.phpfpm.pools.librenms = lib.mkIf (!cfg.distributedPoller.enable) {
466 inherit (package) phpPackage;
469 "listen.mode" = "0660";
470 "listen.owner" = config.services.nginx.user;
471 "listen.group" = config.services.nginx.group;
475 systemd.services.librenms-scheduler = {
476 description = "LibreNMS Scheduler";
477 path = [ pkgs.unixtools.whereis ];
480 WorkingDirectory = package;
483 ExecStart = "${artisanWrapper}/bin/librenms-artisan schedule:run";
487 systemd.timers.librenms-scheduler = {
488 description = "LibreNMS Scheduler";
489 wantedBy = [ "timers.target" ];
491 OnCalendar = "minutely";
492 AccuracySec = "1second";
496 systemd.services.librenms-setup = {
497 description = "Preparation tasks for LibreNMS";
498 before = [ "phpfpm-librenms.service" ];
499 after = [ "systemd-tmpfiles-setup.service" ]
500 ++ (lib.optional (cfg.database.host == "localhost") "mysql.service");
501 wantedBy = [ "multi-user.target" ];
502 restartTriggers = [ package configFile ];
503 path = [ pkgs.mariadb pkgs.unixtools.whereis pkgs.gnused ];
506 RemainAfterExit = true;
507 EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
510 ExecStartPre = lib.mkIf cfg.database.createLocally [
511 "!${pkgs.writeShellScript "librenms-db-init" ''
512 DB_PASSWORD=$(cat ${cfg.database.passwordFile} | tr -d '\n')
513 echo "ALTER USER '${cfg.database.username}'@'localhost' IDENTIFIED BY '$DB_PASSWORD';" | ${pkgs.mariadb}/bin/mysql
514 ${lib.optionalString cfg.useDistributedPollers ''
515 echo "ALTER USER '${cfg.database.username}'@'%' IDENTIFIED BY '$DB_PASSWORD';" | ${pkgs.mariadb}/bin/mysql
524 ln -sf ${configFile} ${cfg.dataDir}/config.php
525 ${pkgs.envsubst}/bin/envsubst -i ${configJson} -o ${cfg.dataDir}/config.json
526 export PHPRC=${phpIni}
528 if [[ ! -s ${cfg.dataDir}/.env ]]; then
530 echo "APP_KEY=" > ${cfg.dataDir}/.env
531 ${artisanWrapper}/bin/librenms-artisan key:generate --ansi
532 ${artisanWrapper}/bin/librenms-artisan webpush:vapid
533 echo "" >> ${cfg.dataDir}/.env
534 echo -n "NODE_ID=" >> ${cfg.dataDir}/.env
535 ${package.phpPackage}/bin/php -r "echo uniqid();" >> ${cfg.dataDir}/.env
536 echo "" >> ${cfg.dataDir}/.env
538 # .env file already exists --> only update database and cache config
539 ${pkgs.gnused}/bin/sed -i /^DB_/d ${cfg.dataDir}/.env
540 ${pkgs.gnused}/bin/sed -i /^CACHE_DRIVER/d ${cfg.dataDir}/.env
542 ${lib.optionalString (cfg.useDistributedPollers || cfg.distributedPoller.enable) ''
543 echo "CACHE_DRIVER=memcached" >> ${cfg.dataDir}/.env
545 echo "DB_DATABASE=${cfg.database.database}" >> ${cfg.dataDir}/.env
548 if ! isNull cfg.database.socket
550 # use socket connection
551 echo "DB_SOCKET=${cfg.database.socket}" >> ${cfg.dataDir}/.env
555 echo "DB_HOST=${cfg.database.host}" >> ${cfg.dataDir}/.env
556 echo "DB_PORT=${toString cfg.database.port}" >> ${cfg.dataDir}/.env
557 echo "DB_USERNAME=${cfg.database.username}" >> ${cfg.dataDir}/.env
558 echo -n "DB_PASSWORD=" >> ${cfg.dataDir}/.env
559 cat ${cfg.database.passwordFile} >> ${cfg.dataDir}/.env
563 # clear cache after update
564 OLD_VERSION=$(cat ${cfg.dataDir}/version)
565 if [[ $OLD_VERSION != "${package.version}" ]]; then
566 rm -r ${cfg.dataDir}/cache/*
567 echo "${package.version}" > ${cfg.dataDir}/version
570 # convert rrd files when the oneMinutePolling option is changed
571 OLD_ENABLED=$(cat ${cfg.dataDir}/one_minute_enabled)
572 if [[ $OLD_ENABLED != "${lib.boolToString cfg.enableOneMinutePolling}" ]]; then
573 ${package}/scripts/rrdstep.php -h all
574 echo "${lib.boolToString cfg.enableOneMinutePolling}" > ${cfg.dataDir}/one_minute_enabled
578 ${artisanWrapper}/bin/librenms-artisan migrate --force --no-interaction
582 programs.mtr.enable = true;
584 services.logrotate = {
586 settings."${cfg.logDir}/librenms.log" = {
587 su = "${cfg.user} ${cfg.group}";
588 create = "0640 ${cfg.user} ${cfg.group}";
590 frequency = "weekly";
592 delaycompress = true;
602 env = "PHPRC=${phpIni}";
605 # based on crontab provided by LibreNMS
606 "33 */6 * * * ${cfg.user} ${env} ${package}/cronic ${package}/discovery-wrapper.py 1"
607 "*/5 * * * * ${cfg.user} ${env} ${package}/discovery.php -h new >> /dev/null 2>&1"
609 "${if cfg.enableOneMinutePolling then "*" else "*/5"} * * * * ${cfg.user} ${env} ${package}/cronic ${package}/poller-wrapper.py ${toString cfg.pollerThreads}"
610 "* * * * * ${cfg.user} ${env} ${package}/alerts.php >> /dev/null 2>&1"
612 "*/5 * * * * ${cfg.user} ${env} ${package}/poll-billing.php >> /dev/null 2>&1"
613 "01 * * * * ${cfg.user} ${env} ${package}/billing-calculate.php >> /dev/null 2>&1"
614 "*/5 * * * * ${cfg.user} ${env} ${package}/check-services.php >> /dev/null 2>&1"
617 "* * * * * ${cfg.user} ${env} ${package}/ping.php >> /dev/null 2>&1"
619 # daily.sh tasks are split to exclude update
620 "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh cleanup >> /dev/null 2>&1"
621 "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh notifications >> /dev/null 2>&1"
622 "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh peeringdb >> /dev/null 2>&1"
623 "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh mac_oui >> /dev/null 2>&1"
627 security.wrappers = {
632 source = "${pkgs.fping}/bin/fping";
636 environment.systemPackages = [ artisanWrapper lnmsWrapper ];
638 systemd.tmpfiles.rules = [
639 "d ${cfg.logDir} 0750 ${cfg.user} ${cfg.group} - -"
640 "f ${cfg.logDir}/librenms.log 0640 ${cfg.user} ${cfg.group} - -"
641 "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -"
642 "f ${cfg.dataDir}/.env 0600 ${cfg.user} ${cfg.group} - -"
643 "f ${cfg.dataDir}/version 0600 ${cfg.user} ${cfg.group} - -"
644 "f ${cfg.dataDir}/one_minute_enabled 0600 ${cfg.user} ${cfg.group} - -"
645 "f ${cfg.dataDir}/config.json 0600 ${cfg.user} ${cfg.group} - -"
646 "d ${cfg.dataDir}/storage 0700 ${cfg.user} ${cfg.group} - -"
647 "d ${cfg.dataDir}/storage/app 0700 ${cfg.user} ${cfg.group} - -"
648 "d ${cfg.dataDir}/storage/debugbar 0700 ${cfg.user} ${cfg.group} - -"
649 "d ${cfg.dataDir}/storage/framework 0700 ${cfg.user} ${cfg.group} - -"
650 "d ${cfg.dataDir}/storage/framework/cache 0700 ${cfg.user} ${cfg.group} - -"
651 "d ${cfg.dataDir}/storage/framework/sessions 0700 ${cfg.user} ${cfg.group} - -"
652 "d ${cfg.dataDir}/storage/framework/views 0700 ${cfg.user} ${cfg.group} - -"
653 "d ${cfg.dataDir}/storage/logs 0700 ${cfg.user} ${cfg.group} - -"
654 "d ${cfg.dataDir}/rrd 0700 ${cfg.user} ${cfg.group} - -"
655 "d ${cfg.dataDir}/cache 0700 ${cfg.user} ${cfg.group} - -"
656 ] ++ lib.optionals cfg.useDistributedPollers [
657 "d ${cfg.dataDir}/rrdcached-journal 0700 ${cfg.user} ${cfg.group} - -"
662 meta.maintainers = lib.teams.wdz.members;