at 24.11-pre 21 kB view raw
1{ config, lib, pkgs, ... }: 2 3let 4 cfg = config.services.librenms; 5 settingsFormat = pkgs.formats.json {}; 6 configJson = settingsFormat.generate "librenms-config.json" cfg.settings; 7 8 package = pkgs.librenms.override { 9 logDir = cfg.logDir; 10 dataDir = cfg.dataDir; 11 }; 12 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 inherit (package) phpPackage; 21 inherit phpOptions; 22 preferLocalBuild = true; 23 passAsFile = [ "phpOptions" ]; 24 } '' 25 cat $phpPackage/etc/php.ini $phpOptionsPath > $out 26 ''; 27 28 artisanWrapper = pkgs.writeShellScriptBin "librenms-artisan" '' 29 cd ${package} 30 sudo=exec 31 if [[ "$USER" != ${cfg.user} ]]; then 32 sudo='exec /run/wrappers/bin/sudo -u ${cfg.user}' 33 fi 34 $sudo ${package}/artisan $* 35 ''; 36 37 lnmsWrapper = pkgs.writeShellScriptBin "lnms" '' 38 cd ${package} 39 exec ${package}/lnms $* 40 ''; 41 42 configFile = pkgs.writeText "config.php" '' 43 <?php 44 $new_config = json_decode(file_get_contents("${cfg.dataDir}/config.json"), true); 45 $config = ($config == null) ? $new_config : array_merge($config, $new_config); 46 47 ${lib.optionalString (cfg.extraConfig != null) cfg.extraConfig} 48 ''; 49 50in { 51 options.services.librenms = with lib; { 52 enable = mkEnableOption "LibreNMS network monitoring system"; 53 54 user = mkOption { 55 type = types.str; 56 default = "librenms"; 57 description = '' 58 Name of the LibreNMS user. 59 ''; 60 }; 61 62 group = mkOption { 63 type = types.str; 64 default = "librenms"; 65 description = '' 66 Name of the LibreNMS group. 67 ''; 68 }; 69 70 hostname = mkOption { 71 type = types.str; 72 default = config.networking.fqdnOrHostName; 73 defaultText = literalExpression "config.networking.fqdnOrHostName"; 74 description = '' 75 The hostname to serve LibreNMS on. 76 ''; 77 }; 78 79 pollerThreads = mkOption { 80 type = types.int; 81 default = 16; 82 description = '' 83 Amount of threads of the cron-poller. 84 ''; 85 }; 86 87 enableOneMinutePolling = mkOption { 88 type = types.bool; 89 default = false; 90 description = '' 91 Enables the [1-Minute Polling](https://docs.librenms.org/Support/1-Minute-Polling/). 92 Changing this option will automatically convert your existing rrd files. 93 ''; 94 }; 95 96 useDistributedPollers = mkOption { 97 type = types.bool; 98 default = false; 99 description = '' 100 Enables (distributed pollers)[https://docs.librenms.org/Extensions/Distributed-Poller/] 101 for this LibreNMS instance. This will enable a local `rrdcached` and `memcached` server. 102 103 To use this feature, make sure to configure your firewall that the distributed pollers 104 can reach the local `mysql`, `rrdcached` and `memcached` ports. 105 ''; 106 }; 107 108 distributedPoller = { 109 enable = mkOption { 110 type = types.bool; 111 default = false; 112 description = '' 113 Configure this LibreNMS instance as a (distributed poller)[https://docs.librenms.org/Extensions/Distributed-Poller/]. 114 This will disable all web features and just configure the poller features. 115 Use the `mysql` database of your main LibreNMS instance in the database settings. 116 ''; 117 }; 118 119 name = mkOption { 120 type = types.nullOr types.str; 121 default = null; 122 description = '' 123 Custom name of this poller. 124 ''; 125 }; 126 127 group = mkOption { 128 type = types.str; 129 default = "0"; 130 example = "1,2"; 131 description = '' 132 Group(s) of this poller. 133 ''; 134 }; 135 136 distributedBilling = mkOption { 137 type = types.bool; 138 default = false; 139 description = '' 140 Enable distributed billing on this poller. 141 ''; 142 }; 143 144 memcachedHost = mkOption { 145 type = types.str; 146 description = '' 147 Hostname or IP of the `memcached` server. 148 ''; 149 }; 150 151 memcachedPort = mkOption { 152 type = types.port; 153 default = 11211; 154 description = '' 155 Port of the `memcached` server. 156 ''; 157 }; 158 159 rrdcachedHost = mkOption { 160 type = types.str; 161 description = '' 162 Hostname or IP of the `rrdcached` server. 163 ''; 164 }; 165 166 rrdcachedPort = mkOption { 167 type = types.port; 168 default = 42217; 169 description = '' 170 Port of the `memcached` server. 171 ''; 172 }; 173 }; 174 175 poolConfig = mkOption { 176 type = with types; attrsOf (oneOf [ str int bool ]); 177 default = { 178 "pm" = "dynamic"; 179 "pm.max_children" = 32; 180 "pm.start_servers" = 2; 181 "pm.min_spare_servers" = 2; 182 "pm.max_spare_servers" = 4; 183 "pm.max_requests" = 500; 184 }; 185 description = '' 186 Options for the LibreNMS PHP pool. See the documentation on `php-fpm.conf` 187 for details on configuration directives. 188 ''; 189 }; 190 191 nginx = mkOption { 192 type = types.submodule ( 193 recursiveUpdate 194 (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {} 195 ); 196 default = { }; 197 example = literalExpression '' 198 { 199 serverAliases = [ 200 "librenms.''${config.networking.domain}" 201 ]; 202 # To enable encryption and let let's encrypt take care of certificate 203 forceSSL = true; 204 enableACME = true; 205 # To set the LibreNMS virtualHost as the default virtualHost; 206 default = true; 207 } 208 ''; 209 description = '' 210 With this option, you can customize the nginx virtualHost settings. 211 ''; 212 }; 213 214 dataDir = mkOption { 215 type = types.path; 216 default = "/var/lib/librenms"; 217 description = '' 218 Path of the LibreNMS state directory. 219 ''; 220 }; 221 222 logDir = mkOption { 223 type = types.path; 224 default = "/var/log/librenms"; 225 description = '' 226 Path of the LibreNMS logging directory. 227 ''; 228 }; 229 230 database = { 231 createLocally = mkOption { 232 type = types.bool; 233 default = false; 234 description = '' 235 Whether to create a local database automatically. 236 ''; 237 }; 238 239 host = mkOption { 240 default = "localhost"; 241 description = '' 242 Hostname or IP of the MySQL/MariaDB server. 243 ''; 244 }; 245 246 port = mkOption { 247 type = types.port; 248 default = 3306; 249 description = '' 250 Port of the MySQL/MariaDB server. 251 ''; 252 }; 253 254 database = mkOption { 255 type = types.str; 256 default = "librenms"; 257 description = '' 258 Name of the database on the MySQL/MariaDB server. 259 ''; 260 }; 261 262 username = mkOption { 263 type = types.str; 264 default = "librenms"; 265 description = '' 266 Name of the user on the MySQL/MariaDB server. 267 ''; 268 }; 269 270 passwordFile = mkOption { 271 type = types.path; 272 example = "/run/secrets/mysql.pass"; 273 description = '' 274 A file containing the password for the user of the MySQL/MariaDB server. 275 Must be readable for the LibreNMS user. 276 ''; 277 }; 278 }; 279 280 environmentFile = mkOption { 281 type = types.nullOr types.str; 282 default = null; 283 description = '' 284 File containing env-vars to be substituted into the final config. Useful for secrets. 285 Does not apply to settings defined in `extraConfig`. 286 ''; 287 }; 288 289 settings = mkOption { 290 type = types.submodule { 291 freeformType = settingsFormat.type; 292 options = {}; 293 }; 294 description = '' 295 Attrset of the LibreNMS configuration. 296 See https://docs.librenms.org/Support/Configuration/ for reference. 297 All possible options are listed [here](https://github.com/librenms/librenms/blob/master/misc/config_definitions.json). 298 See https://docs.librenms.org/Extensions/Authentication/ for setting other authentication methods. 299 ''; 300 default = { }; 301 example = { 302 base_url = "/librenms/"; 303 top_devices = true; 304 top_ports = false; 305 }; 306 }; 307 308 extraConfig = mkOption { 309 type = types.nullOr types.str; 310 default = null; 311 description = '' 312 Additional config for LibreNMS that will be appended to the `config.php`. See 313 https://github.com/librenms/librenms/blob/master/misc/config_definitions.json 314 for possible options. Useful if you want to use PHP-Functions in your config. 315 ''; 316 }; 317 }; 318 319 config = lib.mkIf cfg.enable { 320 assertions = [ 321 { 322 assertion = config.time.timeZone != null; 323 message = "You must set `time.timeZone` to use the LibreNMS module."; 324 } 325 { 326 assertion = cfg.database.createLocally -> cfg.database.host == "localhost"; 327 message = "The database host must be \"localhost\" if services.librenms.database.createLocally is set to true."; 328 } 329 { 330 assertion = !(cfg.useDistributedPollers && cfg.distributedPoller.enable); 331 message = "The LibreNMS instance can't be a distributed poller and a full instance at the same time."; 332 } 333 ]; 334 335 users.users.${cfg.user} = { 336 group = "${cfg.group}"; 337 isSystemUser = true; 338 }; 339 340 users.groups.${cfg.group} = { }; 341 342 services.librenms.settings = { 343 # basic configs 344 "user" = cfg.user; 345 "own_hostname" = cfg.hostname; 346 "base_url" = lib.mkDefault "/"; 347 "auth_mechanism" = lib.mkDefault "mysql"; 348 349 # disable auto update function (won't work with NixOS) 350 "update" = false; 351 352 # enable fast ping by default 353 "ping_rrd_step" = 60; 354 355 # one minute polling 356 "rrd.step" = if cfg.enableOneMinutePolling then 60 else 300; 357 "rrd.heartbeat" = if cfg.enableOneMinutePolling then 120 else 600; 358 } // (lib.optionalAttrs cfg.distributedPoller.enable { 359 "distributed_poller" = true; 360 "distributed_poller_name" = lib.mkIf (cfg.distributedPoller.name != null) cfg.distributedPoller.name; 361 "distributed_poller_group" = cfg.distributedPoller.group; 362 "distributed_billing" = cfg.distributedPoller.distributedBilling; 363 "distributed_poller_memcached_host" = cfg.distributedPoller.memcachedHost; 364 "distributed_poller_memcached_port" = cfg.distributedPoller.memcachedPort; 365 "rrdcached" = "${cfg.distributedPoller.rrdcachedHost}:${toString cfg.distributedPoller.rrdcachedPort}"; 366 }) // (lib.optionalAttrs cfg.useDistributedPollers { 367 "distributed_poller" = true; 368 # still enable a local poller with distributed polling 369 "distributed_poller_group" = lib.mkDefault "0"; 370 "distributed_billing" = lib.mkDefault true; 371 "distributed_poller_memcached_host" = "localhost"; 372 "distributed_poller_memcached_port" = 11211; 373 "rrdcached" = "localhost:42217"; 374 }); 375 376 services.memcached = lib.mkIf cfg.useDistributedPollers { 377 enable = true; 378 listen = "0.0.0.0"; 379 }; 380 381 systemd.services.rrdcached = lib.mkIf cfg.useDistributedPollers { 382 description = "rrdcached"; 383 after = [ "librenms-setup.service" ]; 384 wantedBy = [ "multi-user.target" ]; 385 serviceConfig = { 386 Type = "forking"; 387 User = cfg.user; 388 Group = cfg.group; 389 LimitNOFILE = 16384; 390 RuntimeDirectory = "rrdcached"; 391 PidFile = "/run/rrdcached/rrdcached.pid"; 392 # rrdcached params from https://docs.librenms.org/Extensions/Distributed-Poller/#config-sample 393 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"; 394 }; 395 }; 396 397 services.mysql = lib.mkIf cfg.database.createLocally { 398 enable = true; 399 package = lib.mkDefault pkgs.mariadb; 400 settings.mysqld = { 401 innodb_file_per_table = 1; 402 lower_case_table_names = 0; 403 } // (lib.optionalAttrs cfg.useDistributedPollers { 404 bind-address = "0.0.0.0"; 405 }); 406 ensureDatabases = [ cfg.database.database ]; 407 ensureUsers = [ 408 { 409 name = cfg.database.username; 410 ensurePermissions = { 411 "${cfg.database.database}.*" = "ALL PRIVILEGES"; 412 }; 413 } 414 ]; 415 initialScript = lib.mkIf cfg.useDistributedPollers (pkgs.writeText "mysql-librenms-init" '' 416 CREATE USER IF NOT EXISTS '${cfg.database.username}'@'%'; 417 GRANT ALL PRIVILEGES ON ${cfg.database.database}.* TO '${cfg.database.username}'@'%'; 418 ''); 419 }; 420 421 services.nginx = lib.mkIf (!cfg.distributedPoller.enable) { 422 enable = true; 423 virtualHosts."${cfg.hostname}" = lib.mkMerge [ 424 cfg.nginx 425 { 426 root = lib.mkForce "${package}/html"; 427 locations."/" = { 428 index = "index.php"; 429 tryFiles = "$uri $uri/ /index.php?$query_string"; 430 }; 431 locations."~ .php$".extraConfig = '' 432 fastcgi_pass unix:${config.services.phpfpm.pools."librenms".socket}; 433 fastcgi_split_path_info ^(.+\.php)(/.+)$; 434 ''; 435 } 436 ]; 437 }; 438 439 services.phpfpm.pools.librenms = lib.mkIf (!cfg.distributedPoller.enable) { 440 user = cfg.user; 441 group = cfg.group; 442 inherit (package) phpPackage; 443 inherit phpOptions; 444 settings = { 445 "listen.mode" = "0660"; 446 "listen.owner" = config.services.nginx.user; 447 "listen.group" = config.services.nginx.group; 448 } // cfg.poolConfig; 449 }; 450 451 systemd.services.librenms-scheduler = { 452 description = "LibreNMS Scheduler"; 453 path = [ pkgs.unixtools.whereis ]; 454 serviceConfig = { 455 Type = "oneshot"; 456 WorkingDirectory = package; 457 User = cfg.user; 458 Group = cfg.group; 459 ExecStart = "${artisanWrapper}/bin/librenms-artisan schedule:run"; 460 }; 461 }; 462 463 systemd.timers.librenms-scheduler = { 464 description = "LibreNMS Scheduler"; 465 wantedBy = [ "timers.target" ]; 466 timerConfig = { 467 OnCalendar = "minutely"; 468 AccuracySec = "1second"; 469 }; 470 }; 471 472 systemd.services.librenms-setup = { 473 description = "Preparation tasks for LibreNMS"; 474 before = [ "phpfpm-librenms.service" ]; 475 after = [ "systemd-tmpfiles-setup.service" ] 476 ++ (lib.optional (cfg.database.host == "localhost") "mysql.service"); 477 wantedBy = [ "multi-user.target" ]; 478 restartTriggers = [ package configFile ]; 479 path = [ pkgs.mariadb pkgs.unixtools.whereis pkgs.gnused ]; 480 serviceConfig = { 481 Type = "oneshot"; 482 RemainAfterExit = true; 483 EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; 484 User = cfg.user; 485 Group = cfg.group; 486 ExecStartPre = lib.mkIf cfg.database.createLocally [ "!${pkgs.writeShellScript "librenms-db-init" '' 487 DB_PASSWORD=$(cat ${cfg.database.passwordFile} | tr -d '\n') 488 echo "ALTER USER '${cfg.database.username}'@'localhost' IDENTIFIED BY '$DB_PASSWORD';" | ${pkgs.mariadb}/bin/mysql 489 ${lib.optionalString cfg.useDistributedPollers '' 490 echo "ALTER USER '${cfg.database.username}'@'%' IDENTIFIED BY '$DB_PASSWORD';" | ${pkgs.mariadb}/bin/mysql 491 ''} 492 ''}"]; 493 }; 494 script = '' 495 set -euo pipefail 496 497 # config setup 498 ln -sf ${configFile} ${cfg.dataDir}/config.php 499 ${pkgs.envsubst}/bin/envsubst -i ${configJson} -o ${cfg.dataDir}/config.json 500 export PHPRC=${phpIni} 501 502 if [[ ! -s ${cfg.dataDir}/.env ]]; then 503 # init .env file 504 echo "APP_KEY=" > ${cfg.dataDir}/.env 505 ${artisanWrapper}/bin/librenms-artisan key:generate --ansi 506 ${artisanWrapper}/bin/librenms-artisan webpush:vapid 507 echo "" >> ${cfg.dataDir}/.env 508 echo -n "NODE_ID=" >> ${cfg.dataDir}/.env 509 ${package.phpPackage}/bin/php -r "echo uniqid();" >> ${cfg.dataDir}/.env 510 echo "" >> ${cfg.dataDir}/.env 511 else 512 # .env file already exists --> only update database and cache config 513 ${pkgs.gnused}/bin/sed -i /^DB_/d ${cfg.dataDir}/.env 514 ${pkgs.gnused}/bin/sed -i /^CACHE_DRIVER/d ${cfg.dataDir}/.env 515 fi 516 ${lib.optionalString (cfg.useDistributedPollers || cfg.distributedPoller.enable) '' 517 echo "CACHE_DRIVER=memcached" >> ${cfg.dataDir}/.env 518 ''} 519 echo "DB_HOST=${cfg.database.host}" >> ${cfg.dataDir}/.env 520 echo "DB_PORT=${toString cfg.database.port}" >> ${cfg.dataDir}/.env 521 echo "DB_DATABASE=${cfg.database.database}" >> ${cfg.dataDir}/.env 522 echo "DB_USERNAME=${cfg.database.username}" >> ${cfg.dataDir}/.env 523 echo -n "DB_PASSWORD=" >> ${cfg.dataDir}/.env 524 cat ${cfg.database.passwordFile} >> ${cfg.dataDir}/.env 525 526 # clear cache after update 527 OLD_VERSION=$(cat ${cfg.dataDir}/version) 528 if [[ $OLD_VERSION != "${package.version}" ]]; then 529 rm -r ${cfg.dataDir}/cache/* 530 echo "${package.version}" > ${cfg.dataDir}/version 531 fi 532 533 # convert rrd files when the oneMinutePolling option is changed 534 OLD_ENABLED=$(cat ${cfg.dataDir}/one_minute_enabled) 535 if [[ $OLD_ENABLED != "${lib.boolToString cfg.enableOneMinutePolling}" ]]; then 536 ${package}/scripts/rrdstep.php -h all 537 echo "${lib.boolToString cfg.enableOneMinutePolling}" > ${cfg.dataDir}/one_minute_enabled 538 fi 539 540 # migrate db 541 ${artisanWrapper}/bin/librenms-artisan migrate --force --no-interaction 542 ''; 543 }; 544 545 programs.mtr.enable = true; 546 547 services.logrotate = { 548 enable = true; 549 settings."${cfg.logDir}/librenms.log" = { 550 su = "${cfg.user} ${cfg.group}"; 551 create = "0640 ${cfg.user} ${cfg.group}"; 552 rotate = 6; 553 frequency = "weekly"; 554 compress = true; 555 delaycompress = true; 556 missingok = true; 557 notifempty = true; 558 }; 559 }; 560 561 services.cron = { 562 enable = true; 563 systemCronJobs = let 564 env = "PHPRC=${phpIni}"; 565 in [ 566 # based on crontab provided by LibreNMS 567 "33 */6 * * * ${cfg.user} ${env} ${package}/cronic ${package}/discovery-wrapper.py 1" 568 "*/5 * * * * ${cfg.user} ${env} ${package}/discovery.php -h new >> /dev/null 2>&1" 569 570 "${if cfg.enableOneMinutePolling then "*" else "*/5"} * * * * ${cfg.user} ${env} ${package}/cronic ${package}/poller-wrapper.py ${toString cfg.pollerThreads}" 571 "* * * * * ${cfg.user} ${env} ${package}/alerts.php >> /dev/null 2>&1" 572 573 "*/5 * * * * ${cfg.user} ${env} ${package}/poll-billing.php >> /dev/null 2>&1" 574 "01 * * * * ${cfg.user} ${env} ${package}/billing-calculate.php >> /dev/null 2>&1" 575 "*/5 * * * * ${cfg.user} ${env} ${package}/check-services.php >> /dev/null 2>&1" 576 577 # extra: fast ping 578 "* * * * * ${cfg.user} ${env} ${package}/ping.php >> /dev/null 2>&1" 579 580 # daily.sh tasks are split to exclude update 581 "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh cleanup >> /dev/null 2>&1" 582 "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh notifications >> /dev/null 2>&1" 583 "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh peeringdb >> /dev/null 2>&1" 584 "19 0 * * * ${cfg.user} ${env} ${package}/daily.sh mac_oui >> /dev/null 2>&1" 585 ]; 586 }; 587 588 security.wrappers = { 589 fping = { 590 setuid = true; 591 owner = "root"; 592 group = "root"; 593 source = "${pkgs.fping}/bin/fping"; 594 }; 595 }; 596 597 environment.systemPackages = [ artisanWrapper lnmsWrapper ]; 598 599 systemd.tmpfiles.rules = [ 600 "d ${cfg.logDir} 0750 ${cfg.user} ${cfg.group} - -" 601 "f ${cfg.logDir}/librenms.log 0640 ${cfg.user} ${cfg.group} - -" 602 "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} - -" 603 "f ${cfg.dataDir}/.env 0600 ${cfg.user} ${cfg.group} - -" 604 "f ${cfg.dataDir}/version 0600 ${cfg.user} ${cfg.group} - -" 605 "f ${cfg.dataDir}/one_minute_enabled 0600 ${cfg.user} ${cfg.group} - -" 606 "f ${cfg.dataDir}/config.json 0600 ${cfg.user} ${cfg.group} - -" 607 "d ${cfg.dataDir}/storage 0700 ${cfg.user} ${cfg.group} - -" 608 "d ${cfg.dataDir}/storage/app 0700 ${cfg.user} ${cfg.group} - -" 609 "d ${cfg.dataDir}/storage/debugbar 0700 ${cfg.user} ${cfg.group} - -" 610 "d ${cfg.dataDir}/storage/framework 0700 ${cfg.user} ${cfg.group} - -" 611 "d ${cfg.dataDir}/storage/framework/cache 0700 ${cfg.user} ${cfg.group} - -" 612 "d ${cfg.dataDir}/storage/framework/sessions 0700 ${cfg.user} ${cfg.group} - -" 613 "d ${cfg.dataDir}/storage/framework/views 0700 ${cfg.user} ${cfg.group} - -" 614 "d ${cfg.dataDir}/storage/logs 0700 ${cfg.user} ${cfg.group} - -" 615 "d ${cfg.dataDir}/rrd 0700 ${cfg.user} ${cfg.group} - -" 616 "d ${cfg.dataDir}/cache 0700 ${cfg.user} ${cfg.group} - -" 617 ] ++ lib.optionals cfg.useDistributedPollers [ 618 "d ${cfg.dataDir}/rrdcached-journal 0700 ${cfg.user} ${cfg.group} - -" 619 ]; 620 621 }; 622 623 meta.maintainers = lib.teams.wdz.members; 624}