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