vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / monitoring / librenms.nix
blobb06dbe66fbdeea86c12172c5e99358ad2f9f3a94
1 { config, lib, pkgs, ... }:
3 let
4   cfg = config.services.librenms;
5   settingsFormat = pkgs.formats.json { };
6   configJson = settingsFormat.generate "librenms-config.json" cfg.settings;
8   package = pkgs.librenms.override {
9     logDir = cfg.logDir;
10     dataDir = cfg.dataDir;
11   };
13   phpOptions = ''
14     log_errors = on
15     post_max_size = 100M
16     upload_max_filesize = 100M
17     date.timezone = "${config.time.timeZone}"
18   '';
19   phpIni = pkgs.runCommand "php.ini"
20     {
21       inherit (package) phpPackage;
22       inherit phpOptions;
23       preferLocalBuild = true;
24       passAsFile = [ "phpOptions" ];
25     } ''
26     cat $phpPackage/etc/php.ini $phpOptionsPath > $out
27   '';
29   artisanWrapper = pkgs.writeShellScriptBin "librenms-artisan" ''
30     cd ${package}
31     sudo=exec
32     if [[ "$USER" != ${cfg.user} ]]; then
33       sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}'
34     fi
35     $sudo ${package}/artisan "$@"
36   '';
38   lnmsWrapper = pkgs.writeShellScriptBin "lnms" ''
39     cd ${package}
40     sudo=exec
41     if [[ "$USER" != ${cfg.user} ]]; then
42     sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}'
43     fi
44     $sudo ${package}/lnms "$@"
45   '';
49   configFile = pkgs.writeText "config.php" ''
50     <?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}
55   '';
59   options.services.librenms = with lib; {
60     enable = mkEnableOption "LibreNMS network monitoring system";
62     user = mkOption {
63       type = types.str;
64       default = "librenms";
65       description = ''
66         Name of the LibreNMS user.
67       '';
68     };
70     group = mkOption {
71       type = types.str;
72       default = "librenms";
73       description = ''
74         Name of the LibreNMS group.
75       '';
76     };
78     hostname = mkOption {
79       type = types.str;
80       default = config.networking.fqdnOrHostName;
81       defaultText = literalExpression "config.networking.fqdnOrHostName";
82       description = ''
83         The hostname to serve LibreNMS on.
84       '';
85     };
87     pollerThreads = mkOption {
88       type = types.int;
89       default = 16;
90       description = ''
91         Amount of threads of the cron-poller.
92       '';
93     };
95     enableOneMinutePolling = mkOption {
96       type = types.bool;
97       default = false;
98       description = ''
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.
101       '';
102     };
104     useDistributedPollers = mkOption {
105       type = types.bool;
106       default = false;
107       description = ''
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.
113       '';
114     };
116     distributedPoller = {
117       enable = mkOption {
118         type = types.bool;
119         default = false;
120         description = ''
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.
124         '';
125       };
127       name = mkOption {
128         type = types.nullOr types.str;
129         default = null;
130         description = ''
131           Custom name of this poller.
132         '';
133       };
135       group = mkOption {
136         type = types.str;
137         default = "0";
138         example = "1,2";
139         description = ''
140           Group(s) of this poller.
141         '';
142       };
144       distributedBilling = mkOption {
145         type = types.bool;
146         default = false;
147         description = ''
148           Enable distributed billing on this poller.
149         '';
150       };
152       memcachedHost = mkOption {
153         type = types.str;
154         description = ''
155           Hostname or IP of the `memcached` server.
156         '';
157       };
159       memcachedPort = mkOption {
160         type = types.port;
161         default = 11211;
162         description = ''
163           Port of the `memcached` server.
164         '';
165       };
167       rrdcachedHost = mkOption {
168         type = types.str;
169         description = ''
170           Hostname or IP of the `rrdcached` server.
171         '';
172       };
174       rrdcachedPort = mkOption {
175         type = types.port;
176         default = 42217;
177         description = ''
178           Port of the `memcached` server.
179         '';
180       };
181     };
183     poolConfig = mkOption {
184       type = with types; attrsOf (oneOf [ str int bool ]);
185       default = {
186         "pm" = "dynamic";
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;
192       };
193       description = ''
194         Options for the LibreNMS PHP pool. See the documentation on `php-fpm.conf`
195         for details on configuration directives.
196       '';
197     };
199     nginx = mkOption {
200       type = types.submodule (
201         recursiveUpdate
202           (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
203           { }
204       );
205       default = { };
206       example = literalExpression ''
207         {
208           serverAliases = [
209             "librenms.''${config.networking.domain}"
210           ];
211           # To enable encryption and let let's encrypt take care of certificate
212           forceSSL = true;
213           enableACME = true;
214           # To set the LibreNMS virtualHost as the default virtualHost;
215           default = true;
216         }
217       '';
218       description = ''
219         With this option, you can customize the nginx virtualHost settings.
220       '';
221     };
223     dataDir = mkOption {
224       type = types.path;
225       default = "/var/lib/librenms";
226       description = ''
227         Path of the LibreNMS state directory.
228       '';
229     };
231     logDir = mkOption {
232       type = types.path;
233       default = "/var/log/librenms";
234       description = ''
235         Path of the LibreNMS logging directory.
236       '';
237     };
239     database = {
240       createLocally = mkOption {
241         type = types.bool;
242         default = false;
243         description = ''
244           Whether to create a local database automatically.
245         '';
246       };
248       host = mkOption {
249         default = "localhost";
250         description = ''
251           Hostname or IP of the MySQL/MariaDB server.
252           Ignored if 'socket' is defined.
253         '';
254       };
256       port = mkOption {
257         type = types.port;
258         default = 3306;
259         description = ''
260           Port of the MySQL/MariaDB server.
261           Ignored if 'socket' is defined.
262         '';
263       };
265       database = mkOption {
266         type = types.str;
267         default = "librenms";
268         description = ''
269           Name of the database on the MySQL/MariaDB server.
270         '';
271       };
273       username = mkOption {
274         type = types.str;
275         default = "librenms";
276         description = ''
277           Name of the user on the MySQL/MariaDB server.
278           Ignored if 'socket' is defined.
279         '';
280       };
282       passwordFile = mkOption {
283         type = types.nullOr types.path;
284         default = null;
285         example = "/run/secrets/mysql.pass";
286         description = ''
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.
290         '';
291       };
293       socket = mkOption {
294         type = types.nullOr types.str;
295         default = null;
296         example = "/run/mysqld/mysqld.sock";
297         description = ''
298           A unix socket to mysql, accessible by the librenms user.
299           Useful when mysql is on the localhost.
300         '';
301       };
302     };
304     environmentFile = mkOption {
305       type = types.nullOr types.str;
306       default = null;
307       description = ''
308         File containing env-vars to be substituted into the final config. Useful for secrets.
309         Does not apply to settings defined in `extraConfig`.
310       '';
311     };
313     settings = mkOption {
314       type = types.submodule {
315         freeformType = settingsFormat.type;
316         options = { };
317       };
318       description = ''
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.
323       '';
324       default = { };
325       example = {
326         base_url = "/librenms/";
327         top_devices = true;
328         top_ports = false;
329       };
330     };
332     extraConfig = mkOption {
333       type = types.nullOr types.str;
334       default = null;
335       description = ''
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.
339       '';
340     };
341   };
343   config = lib.mkIf cfg.enable {
344     assertions = [
345       {
346         assertion = config.time.timeZone != null;
347         message = "You must set `time.timeZone` to use the LibreNMS module.";
348       }
349       {
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.";
352       }
353       {
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.";
356       }
357     ];
359     users.users.${cfg.user} = {
360       group = "${cfg.group}";
361       isSystemUser = true;
362     };
364     users.groups.${cfg.group} = { };
366     services.librenms.settings = {
367       # basic configs
368       "user" = cfg.user;
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)
374       "update" = false;
376       # enable fast ping by default
377       "ping_rrd_step" = 60;
379       # one minute polling
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";
398     });
400     services.memcached = lib.mkIf cfg.useDistributedPollers {
401       enable = true;
402       listen = "0.0.0.0";
403     };
405     systemd.services.rrdcached = lib.mkIf cfg.useDistributedPollers {
406       description = "rrdcached";
407       after = [ "librenms-setup.service" ];
408       wantedBy = [ "multi-user.target" ];
409       serviceConfig = {
410         Type = "forking";
411         User = cfg.user;
412         Group = cfg.group;
413         LimitNOFILE = 16384;
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";
418       };
419     };
421     services.mysql = lib.mkIf cfg.database.createLocally {
422       enable = true;
423       package = lib.mkDefault pkgs.mariadb;
424       settings.mysqld = {
425         innodb_file_per_table = 1;
426         lower_case_table_names = 0;
427       } // (lib.optionalAttrs cfg.useDistributedPollers {
428         bind-address = "0.0.0.0";
429       });
430       ensureDatabases = [ cfg.database.database ];
431       ensureUsers = [
432         {
433           name = cfg.database.username;
434           ensurePermissions = {
435             "${cfg.database.database}.*" = "ALL PRIVILEGES";
436           };
437         }
438       ];
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}'@'%';
442       '');
443     };
445     services.nginx = lib.mkIf (!cfg.distributedPoller.enable) {
446       enable = true;
447       virtualHosts."${cfg.hostname}" = lib.mkMerge [
448         cfg.nginx
449         {
450           root = lib.mkForce "${package}/html";
451           locations."/" = {
452             index = "index.php";
453             tryFiles = "$uri $uri/ /index.php?$query_string";
454           };
455           locations."~ .php$".extraConfig = ''
456             fastcgi_pass unix:${config.services.phpfpm.pools."librenms".socket};
457             fastcgi_split_path_info ^(.+\.php)(/.+)$;
458           '';
459         }
460       ];
461     };
463     services.phpfpm.pools.librenms = lib.mkIf (!cfg.distributedPoller.enable) {
464       user = cfg.user;
465       group = cfg.group;
466       inherit (package) phpPackage;
467       inherit phpOptions;
468       settings = {
469         "listen.mode" = "0660";
470         "listen.owner" = config.services.nginx.user;
471         "listen.group" = config.services.nginx.group;
472       } // cfg.poolConfig;
473     };
475     systemd.services.librenms-scheduler = {
476       description = "LibreNMS Scheduler";
477       path = [ pkgs.unixtools.whereis ];
478       serviceConfig = {
479         Type = "oneshot";
480         WorkingDirectory = package;
481         User = cfg.user;
482         Group = cfg.group;
483         ExecStart = "${artisanWrapper}/bin/librenms-artisan schedule:run";
484       };
485     };
487     systemd.timers.librenms-scheduler = {
488       description = "LibreNMS Scheduler";
489       wantedBy = [ "timers.target" ];
490       timerConfig = {
491         OnCalendar = "minutely";
492         AccuracySec = "1second";
493       };
494     };
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 ];
504       serviceConfig = {
505         Type = "oneshot";
506         RemainAfterExit = true;
507         EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
508         User = cfg.user;
509         Group = cfg.group;
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
516           ''}
517         ''}"
518         ];
519       };
520       script = ''
521         set -euo pipefail
523         # config setup
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
529           # init .env file
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
537         else
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
541         fi
542         ${lib.optionalString (cfg.useDistributedPollers || cfg.distributedPoller.enable) ''
543           echo "CACHE_DRIVER=memcached" >> ${cfg.dataDir}/.env
544         ''}
545         echo "DB_DATABASE=${cfg.database.database}" >> ${cfg.dataDir}/.env
546       ''
547       + (
548         if ! isNull cfg.database.socket
549         then ''
550           # use socket connection
551           echo "DB_SOCKET=${cfg.database.socket}" >> ${cfg.dataDir}/.env
552         ''
553         else ''
554           # use TCP connection
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
560         ''
561       )
562       + ''
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
568         fi
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
575         fi
577         # migrate db
578         ${artisanWrapper}/bin/librenms-artisan migrate --force --no-interaction
579       '';
580     };
582     programs.mtr.enable = true;
584     services.logrotate = {
585       enable = true;
586       settings."${cfg.logDir}/librenms.log" = {
587         su = "${cfg.user} ${cfg.group}";
588         create = "0640 ${cfg.user} ${cfg.group}";
589         rotate = 6;
590         frequency = "weekly";
591         compress = true;
592         delaycompress = true;
593         missingok = true;
594         notifempty = true;
595       };
596     };
598     services.cron = {
599       enable = true;
600       systemCronJobs =
601         let
602           env = "PHPRC=${phpIni}";
603         in
604         [
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"
616           # extra: fast ping
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"
624         ];
625     };
627     security.wrappers = {
628       fping = {
629         setuid = true;
630         owner = "root";
631         group = "root";
632         source = "${pkgs.fping}/bin/fping";
633       };
634     };
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} - -"
658     ];
660   };
662   meta.maintainers = lib.teams.wdz.members;