at 22.05-pre 23 kB view raw
1{ options, config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.grafana; 7 opt = options.services.grafana; 8 declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins); 9 useMysql = cfg.database.type == "mysql"; 10 usePostgresql = cfg.database.type == "postgres"; 11 12 envOptions = { 13 PATHS_DATA = cfg.dataDir; 14 PATHS_PLUGINS = if builtins.isNull cfg.declarativePlugins then "${cfg.dataDir}/plugins" else declarativePlugins; 15 PATHS_LOGS = "${cfg.dataDir}/log"; 16 17 SERVER_PROTOCOL = cfg.protocol; 18 SERVER_HTTP_ADDR = cfg.addr; 19 SERVER_HTTP_PORT = cfg.port; 20 SERVER_SOCKET = cfg.socket; 21 SERVER_DOMAIN = cfg.domain; 22 SERVER_ROOT_URL = cfg.rootUrl; 23 SERVER_STATIC_ROOT_PATH = cfg.staticRootPath; 24 SERVER_CERT_FILE = cfg.certFile; 25 SERVER_CERT_KEY = cfg.certKey; 26 27 DATABASE_TYPE = cfg.database.type; 28 DATABASE_HOST = cfg.database.host; 29 DATABASE_NAME = cfg.database.name; 30 DATABASE_USER = cfg.database.user; 31 DATABASE_PASSWORD = cfg.database.password; 32 DATABASE_PATH = cfg.database.path; 33 DATABASE_CONN_MAX_LIFETIME = cfg.database.connMaxLifetime; 34 35 SECURITY_ADMIN_USER = cfg.security.adminUser; 36 SECURITY_ADMIN_PASSWORD = cfg.security.adminPassword; 37 SECURITY_SECRET_KEY = cfg.security.secretKey; 38 39 USERS_ALLOW_SIGN_UP = boolToString cfg.users.allowSignUp; 40 USERS_ALLOW_ORG_CREATE = boolToString cfg.users.allowOrgCreate; 41 USERS_AUTO_ASSIGN_ORG = boolToString cfg.users.autoAssignOrg; 42 USERS_AUTO_ASSIGN_ORG_ROLE = cfg.users.autoAssignOrgRole; 43 44 AUTH_ANONYMOUS_ENABLED = boolToString cfg.auth.anonymous.enable; 45 AUTH_ANONYMOUS_ORG_NAME = cfg.auth.anonymous.org_name; 46 AUTH_ANONYMOUS_ORG_ROLE = cfg.auth.anonymous.org_role; 47 AUTH_GOOGLE_ENABLED = boolToString cfg.auth.google.enable; 48 AUTH_GOOGLE_ALLOW_SIGN_UP = boolToString cfg.auth.google.allowSignUp; 49 AUTH_GOOGLE_CLIENT_ID = cfg.auth.google.clientId; 50 51 ANALYTICS_REPORTING_ENABLED = boolToString cfg.analytics.reporting.enable; 52 53 SMTP_ENABLED = boolToString cfg.smtp.enable; 54 SMTP_HOST = cfg.smtp.host; 55 SMTP_USER = cfg.smtp.user; 56 SMTP_PASSWORD = cfg.smtp.password; 57 SMTP_FROM_ADDRESS = cfg.smtp.fromAddress; 58 } // cfg.extraOptions; 59 60 datasourceConfiguration = { 61 apiVersion = 1; 62 datasources = cfg.provision.datasources; 63 }; 64 65 datasourceFile = pkgs.writeText "datasource.yaml" (builtins.toJSON datasourceConfiguration); 66 67 dashboardConfiguration = { 68 apiVersion = 1; 69 providers = cfg.provision.dashboards; 70 }; 71 72 dashboardFile = pkgs.writeText "dashboard.yaml" (builtins.toJSON dashboardConfiguration); 73 74 notifierConfiguration = { 75 apiVersion = 1; 76 notifiers = cfg.provision.notifiers; 77 }; 78 79 notifierFile = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration); 80 81 provisionConfDir = pkgs.runCommand "grafana-provisioning" { } '' 82 mkdir -p $out/{datasources,dashboards,notifiers} 83 ln -sf ${datasourceFile} $out/datasources/datasource.yaml 84 ln -sf ${dashboardFile} $out/dashboards/dashboard.yaml 85 ln -sf ${notifierFile} $out/notifiers/notifier.yaml 86 ''; 87 88 # Get a submodule without any embedded metadata: 89 _filter = x: filterAttrs (k: v: k != "_module") x; 90 91 # http://docs.grafana.org/administration/provisioning/#datasources 92 grafanaTypes.datasourceConfig = types.submodule { 93 options = { 94 name = mkOption { 95 type = types.str; 96 description = "Name of the datasource. Required."; 97 }; 98 type = mkOption { 99 type = types.str; 100 description = "Datasource type. Required."; 101 }; 102 access = mkOption { 103 type = types.enum ["proxy" "direct"]; 104 default = "proxy"; 105 description = "Access mode. proxy or direct (Server or Browser in the UI). Required."; 106 }; 107 orgId = mkOption { 108 type = types.int; 109 default = 1; 110 description = "Org id. will default to orgId 1 if not specified."; 111 }; 112 url = mkOption { 113 type = types.str; 114 description = "Url of the datasource."; 115 }; 116 password = mkOption { 117 type = types.nullOr types.str; 118 default = null; 119 description = "Database password, if used."; 120 }; 121 user = mkOption { 122 type = types.nullOr types.str; 123 default = null; 124 description = "Database user, if used."; 125 }; 126 database = mkOption { 127 type = types.nullOr types.str; 128 default = null; 129 description = "Database name, if used."; 130 }; 131 basicAuth = mkOption { 132 type = types.nullOr types.bool; 133 default = null; 134 description = "Enable/disable basic auth."; 135 }; 136 basicAuthUser = mkOption { 137 type = types.nullOr types.str; 138 default = null; 139 description = "Basic auth username."; 140 }; 141 basicAuthPassword = mkOption { 142 type = types.nullOr types.str; 143 default = null; 144 description = "Basic auth password."; 145 }; 146 withCredentials = mkOption { 147 type = types.bool; 148 default = false; 149 description = "Enable/disable with credentials headers."; 150 }; 151 isDefault = mkOption { 152 type = types.bool; 153 default = false; 154 description = "Mark as default datasource. Max one per org."; 155 }; 156 jsonData = mkOption { 157 type = types.nullOr types.attrs; 158 default = null; 159 description = "Datasource specific configuration."; 160 }; 161 secureJsonData = mkOption { 162 type = types.nullOr types.attrs; 163 default = null; 164 description = "Datasource specific secure configuration."; 165 }; 166 version = mkOption { 167 type = types.int; 168 default = 1; 169 description = "Version."; 170 }; 171 editable = mkOption { 172 type = types.bool; 173 default = false; 174 description = "Allow users to edit datasources from the UI."; 175 }; 176 }; 177 }; 178 179 # http://docs.grafana.org/administration/provisioning/#dashboards 180 grafanaTypes.dashboardConfig = types.submodule { 181 options = { 182 name = mkOption { 183 type = types.str; 184 default = "default"; 185 description = "Provider name."; 186 }; 187 orgId = mkOption { 188 type = types.int; 189 default = 1; 190 description = "Organization ID."; 191 }; 192 folder = mkOption { 193 type = types.str; 194 default = ""; 195 description = "Add dashboards to the specified folder."; 196 }; 197 type = mkOption { 198 type = types.str; 199 default = "file"; 200 description = "Dashboard provider type."; 201 }; 202 disableDeletion = mkOption { 203 type = types.bool; 204 default = false; 205 description = "Disable deletion when JSON file is removed."; 206 }; 207 updateIntervalSeconds = mkOption { 208 type = types.int; 209 default = 10; 210 description = "How often Grafana will scan for changed dashboards."; 211 }; 212 options = { 213 path = mkOption { 214 type = types.path; 215 description = "Path grafana will watch for dashboards."; 216 }; 217 }; 218 }; 219 }; 220 221 grafanaTypes.notifierConfig = types.submodule { 222 options = { 223 name = mkOption { 224 type = types.str; 225 default = "default"; 226 description = "Notifier name."; 227 }; 228 type = mkOption { 229 type = types.enum ["dingding" "discord" "email" "googlechat" "hipchat" "kafka" "line" "teams" "opsgenie" "pagerduty" "prometheus-alertmanager" "pushover" "sensu" "sensugo" "slack" "telegram" "threema" "victorops" "webhook"]; 230 description = "Notifier type."; 231 }; 232 uid = mkOption { 233 type = types.str; 234 description = "Unique notifier identifier."; 235 }; 236 org_id = mkOption { 237 type = types.int; 238 default = 1; 239 description = "Organization ID."; 240 }; 241 org_name = mkOption { 242 type = types.str; 243 default = "Main Org."; 244 description = "Organization name."; 245 }; 246 is_default = mkOption { 247 type = types.bool; 248 description = "Is the default notifier."; 249 default = false; 250 }; 251 send_reminder = mkOption { 252 type = types.bool; 253 default = true; 254 description = "Should the notifier be sent reminder notifications while alerts continue to fire."; 255 }; 256 frequency = mkOption { 257 type = types.str; 258 default = "5m"; 259 description = "How frequently should the notifier be sent reminders."; 260 }; 261 disable_resolve_message = mkOption { 262 type = types.bool; 263 default = false; 264 description = "Turn off the message that sends when an alert returns to OK."; 265 }; 266 settings = mkOption { 267 type = types.nullOr types.attrs; 268 default = null; 269 description = "Settings for the notifier type."; 270 }; 271 secure_settings = mkOption { 272 type = types.nullOr types.attrs; 273 default = null; 274 description = "Secure settings for the notifier type."; 275 }; 276 }; 277 }; 278in { 279 options.services.grafana = { 280 enable = mkEnableOption "grafana"; 281 282 protocol = mkOption { 283 description = "Which protocol to listen."; 284 default = "http"; 285 type = types.enum ["http" "https" "socket"]; 286 }; 287 288 addr = mkOption { 289 description = "Listening address."; 290 default = "127.0.0.1"; 291 type = types.str; 292 }; 293 294 port = mkOption { 295 description = "Listening port."; 296 default = 3000; 297 type = types.port; 298 }; 299 300 socket = mkOption { 301 description = "Listening socket."; 302 default = "/run/grafana/grafana.sock"; 303 type = types.str; 304 }; 305 306 domain = mkOption { 307 description = "The public facing domain name used to access grafana from a browser."; 308 default = "localhost"; 309 type = types.str; 310 }; 311 312 rootUrl = mkOption { 313 description = "Full public facing url."; 314 default = "%(protocol)s://%(domain)s:%(http_port)s/"; 315 type = types.str; 316 }; 317 318 certFile = mkOption { 319 description = "Cert file for ssl."; 320 default = ""; 321 type = types.str; 322 }; 323 324 certKey = mkOption { 325 description = "Cert key for ssl."; 326 default = ""; 327 type = types.str; 328 }; 329 330 staticRootPath = mkOption { 331 description = "Root path for static assets."; 332 default = "${cfg.package}/share/grafana/public"; 333 defaultText = literalExpression ''"''${package}/share/grafana/public"''; 334 type = types.str; 335 }; 336 337 package = mkOption { 338 description = "Package to use."; 339 default = pkgs.grafana; 340 defaultText = literalExpression "pkgs.grafana"; 341 type = types.package; 342 }; 343 344 declarativePlugins = mkOption { 345 type = with types; nullOr (listOf path); 346 default = null; 347 description = "If non-null, then a list of packages containing Grafana plugins to install. If set, plugins cannot be manually installed."; 348 example = literalExpression "with pkgs.grafanaPlugins; [ grafana-piechart-panel ]"; 349 # Make sure each plugin is added only once; otherwise building 350 # the link farm fails, since the same path is added multiple 351 # times. 352 apply = x: if isList x then lib.unique x else x; 353 }; 354 355 dataDir = mkOption { 356 description = "Data directory."; 357 default = "/var/lib/grafana"; 358 type = types.path; 359 }; 360 361 database = { 362 type = mkOption { 363 description = "Database type."; 364 default = "sqlite3"; 365 type = types.enum ["mysql" "sqlite3" "postgres"]; 366 }; 367 368 host = mkOption { 369 description = "Database host."; 370 default = "127.0.0.1:3306"; 371 type = types.str; 372 }; 373 374 name = mkOption { 375 description = "Database name."; 376 default = "grafana"; 377 type = types.str; 378 }; 379 380 user = mkOption { 381 description = "Database user."; 382 default = "root"; 383 type = types.str; 384 }; 385 386 password = mkOption { 387 description = '' 388 Database password. 389 This option is mutual exclusive with the passwordFile option. 390 ''; 391 default = ""; 392 type = types.str; 393 }; 394 395 passwordFile = mkOption { 396 description = '' 397 File that containts the database password. 398 This option is mutual exclusive with the password option. 399 ''; 400 default = null; 401 type = types.nullOr types.path; 402 }; 403 404 path = mkOption { 405 description = "Database path."; 406 default = "${cfg.dataDir}/data/grafana.db"; 407 type = types.path; 408 }; 409 410 connMaxLifetime = mkOption { 411 description = '' 412 Sets the maximum amount of time (in seconds) a connection may be reused. 413 For MySQL this setting should be shorter than the `wait_timeout' variable. 414 ''; 415 default = "unlimited"; 416 example = 14400; 417 type = types.either types.int (types.enum [ "unlimited" ]); 418 }; 419 }; 420 421 provision = { 422 enable = mkEnableOption "provision"; 423 datasources = mkOption { 424 description = "Grafana datasources configuration."; 425 default = []; 426 type = types.listOf grafanaTypes.datasourceConfig; 427 apply = x: map _filter x; 428 }; 429 dashboards = mkOption { 430 description = "Grafana dashboard configuration."; 431 default = []; 432 type = types.listOf grafanaTypes.dashboardConfig; 433 apply = x: map _filter x; 434 }; 435 notifiers = mkOption { 436 description = "Grafana notifier configuration."; 437 default = []; 438 type = types.listOf grafanaTypes.notifierConfig; 439 apply = x: map _filter x; 440 }; 441 }; 442 443 security = { 444 adminUser = mkOption { 445 description = "Default admin username."; 446 default = "admin"; 447 type = types.str; 448 }; 449 450 adminPassword = mkOption { 451 description = '' 452 Default admin password. 453 This option is mutual exclusive with the adminPasswordFile option. 454 ''; 455 default = "admin"; 456 type = types.str; 457 }; 458 459 adminPasswordFile = mkOption { 460 description = '' 461 Default admin password. 462 This option is mutual exclusive with the <literal>adminPassword</literal> option. 463 ''; 464 default = null; 465 type = types.nullOr types.path; 466 }; 467 468 secretKey = mkOption { 469 description = "Secret key used for signing."; 470 default = "SW2YcwTIb9zpOOhoPsMm"; 471 type = types.str; 472 }; 473 474 secretKeyFile = mkOption { 475 description = "Secret key used for signing."; 476 default = null; 477 type = types.nullOr types.path; 478 }; 479 }; 480 481 smtp = { 482 enable = mkEnableOption "smtp"; 483 host = mkOption { 484 description = "Host to connect to."; 485 default = "localhost:25"; 486 type = types.str; 487 }; 488 user = mkOption { 489 description = "User used for authentication."; 490 default = ""; 491 type = types.str; 492 }; 493 password = mkOption { 494 description = '' 495 Password used for authentication. 496 This option is mutual exclusive with the passwordFile option. 497 ''; 498 default = ""; 499 type = types.str; 500 }; 501 passwordFile = mkOption { 502 description = '' 503 Password used for authentication. 504 This option is mutual exclusive with the password option. 505 ''; 506 default = null; 507 type = types.nullOr types.path; 508 }; 509 fromAddress = mkOption { 510 description = "Email address used for sending."; 511 default = "admin@grafana.localhost"; 512 type = types.str; 513 }; 514 }; 515 516 users = { 517 allowSignUp = mkOption { 518 description = "Disable user signup / registration."; 519 default = false; 520 type = types.bool; 521 }; 522 523 allowOrgCreate = mkOption { 524 description = "Whether user is allowed to create organizations."; 525 default = false; 526 type = types.bool; 527 }; 528 529 autoAssignOrg = mkOption { 530 description = "Whether to automatically assign new users to default org."; 531 default = true; 532 type = types.bool; 533 }; 534 535 autoAssignOrgRole = mkOption { 536 description = "Default role new users will be auto assigned."; 537 default = "Viewer"; 538 type = types.enum ["Viewer" "Editor"]; 539 }; 540 }; 541 542 auth = { 543 anonymous = { 544 enable = mkOption { 545 description = "Whether to allow anonymous access."; 546 default = false; 547 type = types.bool; 548 }; 549 org_name = mkOption { 550 description = "Which organization to allow anonymous access to."; 551 default = "Main Org."; 552 type = types.str; 553 }; 554 org_role = mkOption { 555 description = "Which role anonymous users have in the organization."; 556 default = "Viewer"; 557 type = types.str; 558 }; 559 }; 560 google = { 561 enable = mkOption { 562 description = "Whether to allow Google OAuth2."; 563 default = false; 564 type = types.bool; 565 }; 566 allowSignUp = mkOption { 567 description = "Whether to allow sign up with Google OAuth2."; 568 default = false; 569 type = types.bool; 570 }; 571 clientId = mkOption { 572 description = "Google OAuth2 client ID."; 573 default = ""; 574 type = types.str; 575 }; 576 clientSecretFile = mkOption { 577 description = "Google OAuth2 client secret."; 578 default = null; 579 type = types.nullOr types.path; 580 }; 581 }; 582 }; 583 584 analytics.reporting = { 585 enable = mkOption { 586 description = "Whether to allow anonymous usage reporting to stats.grafana.net."; 587 default = true; 588 type = types.bool; 589 }; 590 }; 591 592 extraOptions = mkOption { 593 description = '' 594 Extra configuration options passed as env variables as specified in 595 <link xlink:href="http://docs.grafana.org/installation/configuration/">documentation</link>, 596 but without GF_ prefix 597 ''; 598 default = {}; 599 type = with types; attrsOf (either str path); 600 }; 601 }; 602 603 config = mkIf cfg.enable { 604 warnings = flatten [ 605 (optional ( 606 cfg.database.password != opt.database.password.default || 607 cfg.security.adminPassword != opt.security.adminPassword.default 608 ) "Grafana passwords will be stored as plaintext in the Nix store!") 609 (optional ( 610 any (x: x.password != null || x.basicAuthPassword != null || x.secureJsonData != null) cfg.provision.datasources 611 ) "Datasource passwords will be stored as plaintext in the Nix store!") 612 (optional ( 613 any (x: x.secure_settings != null) cfg.provision.notifiers 614 ) "Notifier secure settings will be stored as plaintext in the Nix store!") 615 ]; 616 617 environment.systemPackages = [ cfg.package ]; 618 619 assertions = [ 620 { 621 assertion = cfg.database.password != opt.database.password.default -> cfg.database.passwordFile == null; 622 message = "Cannot set both password and passwordFile"; 623 } 624 { 625 assertion = cfg.security.adminPassword != opt.security.adminPassword.default -> cfg.security.adminPasswordFile == null; 626 message = "Cannot set both adminPassword and adminPasswordFile"; 627 } 628 { 629 assertion = cfg.security.secretKey != opt.security.secretKey.default -> cfg.security.secretKeyFile == null; 630 message = "Cannot set both secretKey and secretKeyFile"; 631 } 632 { 633 assertion = cfg.smtp.password != opt.smtp.password.default -> cfg.smtp.passwordFile == null; 634 message = "Cannot set both password and passwordFile"; 635 } 636 ]; 637 638 systemd.services.grafana = { 639 description = "Grafana Service Daemon"; 640 wantedBy = ["multi-user.target"]; 641 after = ["networking.target"] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service"; 642 environment = { 643 QT_QPA_PLATFORM = "offscreen"; 644 } // mapAttrs' (n: v: nameValuePair "GF_${n}" (toString v)) envOptions; 645 script = '' 646 set -o errexit -o pipefail -o nounset -o errtrace 647 shopt -s inherit_errexit 648 649 ${optionalString (cfg.auth.google.clientSecretFile != null) '' 650 GF_AUTH_GOOGLE_CLIENT_SECRET="$(<${escapeShellArg cfg.auth.google.clientSecretFile})" 651 export GF_AUTH_GOOGLE_CLIENT_SECRET 652 ''} 653 ${optionalString (cfg.database.passwordFile != null) '' 654 GF_DATABASE_PASSWORD="$(<${escapeShellArg cfg.database.passwordFile})" 655 export GF_DATABASE_PASSWORD 656 ''} 657 ${optionalString (cfg.security.adminPasswordFile != null) '' 658 GF_SECURITY_ADMIN_PASSWORD="$(<${escapeShellArg cfg.security.adminPasswordFile})" 659 export GF_SECURITY_ADMIN_PASSWORD 660 ''} 661 ${optionalString (cfg.security.secretKeyFile != null) '' 662 GF_SECURITY_SECRET_KEY="$(<${escapeShellArg cfg.security.secretKeyFile})" 663 export GF_SECURITY_SECRET_KEY 664 ''} 665 ${optionalString (cfg.smtp.passwordFile != null) '' 666 GF_SMTP_PASSWORD="$(<${escapeShellArg cfg.smtp.passwordFile})" 667 export GF_SMTP_PASSWORD 668 ''} 669 ${optionalString cfg.provision.enable '' 670 export GF_PATHS_PROVISIONING=${provisionConfDir}; 671 ''} 672 exec ${cfg.package}/bin/grafana-server -homepath ${cfg.dataDir} 673 ''; 674 serviceConfig = { 675 WorkingDirectory = cfg.dataDir; 676 User = "grafana"; 677 RuntimeDirectory = "grafana"; 678 RuntimeDirectoryMode = "0755"; 679 # Hardening 680 AmbientCapabilities = lib.mkIf (cfg.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; 681 CapabilityBoundingSet = if (cfg.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ]; 682 DeviceAllow = [ "" ]; 683 LockPersonality = true; 684 NoNewPrivileges = true; 685 PrivateDevices = true; 686 PrivateTmp = true; 687 ProtectClock = true; 688 ProtectControlGroups = true; 689 ProtectHome = true; 690 ProtectHostname = true; 691 ProtectKernelLogs = true; 692 ProtectKernelModules = true; 693 ProtectKernelTunables = true; 694 ProtectProc = "invisible"; 695 ProtectSystem = "full"; 696 RemoveIPC = true; 697 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; 698 RestrictNamespaces = true; 699 RestrictRealtime = true; 700 RestrictSUIDSGID = true; 701 SystemCallArchitectures = "native"; 702 # Upstream grafana is not setting SystemCallFilter for compatibility 703 # reasons, see https://github.com/grafana/grafana/pull/40176 704 SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; 705 UMask = "0027"; 706 }; 707 preStart = '' 708 ln -fs ${cfg.package}/share/grafana/conf ${cfg.dataDir} 709 ln -fs ${cfg.package}/share/grafana/tools ${cfg.dataDir} 710 ''; 711 }; 712 713 users.users.grafana = { 714 uid = config.ids.uids.grafana; 715 description = "Grafana user"; 716 home = cfg.dataDir; 717 createHome = true; 718 group = "grafana"; 719 }; 720 users.groups.grafana = {}; 721 }; 722}