at 23.11-pre 55 kB view raw
1{ options, config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.grafana; 7 opt = options.services.grafana; 8 provisioningSettingsFormat = pkgs.formats.yaml {}; 9 declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins); 10 useMysql = cfg.settings.database.type == "mysql"; 11 usePostgresql = cfg.settings.database.type == "postgres"; 12 13 settingsFormatIni = pkgs.formats.ini {}; 14 configFile = settingsFormatIni.generate "config.ini" cfg.settings; 15 16 mkProvisionCfg = name: attr: provisionCfg: 17 if provisionCfg.path != null 18 then provisionCfg.path 19 else 20 provisioningSettingsFormat.generate "${name}.yaml" 21 (if provisionCfg.settings != null 22 then provisionCfg.settings 23 else { 24 apiVersion = 1; 25 ${attr} = []; 26 }); 27 28 datasourceFileOrDir = mkProvisionCfg "datasource" "datasources" cfg.provision.datasources; 29 dashboardFileOrDir = mkProvisionCfg "dashboard" "providers" cfg.provision.dashboards; 30 31 notifierConfiguration = { 32 apiVersion = 1; 33 notifiers = cfg.provision.notifiers; 34 }; 35 36 notifierFileOrDir = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration); 37 38 generateAlertingProvisioningYaml = x: if (cfg.provision.alerting."${x}".path == null) 39 then provisioningSettingsFormat.generate "${x}.yaml" cfg.provision.alerting."${x}".settings 40 else cfg.provision.alerting."${x}".path; 41 rulesFileOrDir = generateAlertingProvisioningYaml "rules"; 42 contactPointsFileOrDir = generateAlertingProvisioningYaml "contactPoints"; 43 policiesFileOrDir = generateAlertingProvisioningYaml "policies"; 44 templatesFileOrDir = generateAlertingProvisioningYaml "templates"; 45 muteTimingsFileOrDir = generateAlertingProvisioningYaml "muteTimings"; 46 47 ln = { src, dir, filename }: '' 48 if [[ -d "${src}" ]]; then 49 pushd $out/${dir} &>/dev/null 50 lndir "${src}" 51 popd &>/dev/null 52 else 53 ln -sf ${src} $out/${dir}/${filename}.yaml 54 fi 55 ''; 56 provisionConfDir = pkgs.runCommand "grafana-provisioning" { nativeBuildInputs = [ pkgs.xorg.lndir ]; } '' 57 mkdir -p $out/{datasources,dashboards,notifiers,alerting} 58 ${ln { src = datasourceFileOrDir; dir = "datasources"; filename = "datasource"; }} 59 ${ln { src = dashboardFileOrDir; dir = "dashboards"; filename = "dashboard"; }} 60 ${ln { src = notifierFileOrDir; dir = "notifiers"; filename = "notifier"; }} 61 ${ln { src = rulesFileOrDir; dir = "alerting"; filename = "rules"; }} 62 ${ln { src = contactPointsFileOrDir; dir = "alerting"; filename = "contactPoints"; }} 63 ${ln { src = policiesFileOrDir; dir = "alerting"; filename = "policies"; }} 64 ${ln { src = templatesFileOrDir; dir = "alerting"; filename = "templates"; }} 65 ${ln { src = muteTimingsFileOrDir; dir = "alerting"; filename = "muteTimings"; }} 66 ''; 67 68 # Get a submodule without any embedded metadata: 69 _filter = x: filterAttrs (k: v: k != "_module") x; 70 71 # FIXME(@Ma27) remove before 23.05. This is just a helper-type 72 # because `mkRenamedOptionModule` doesn't work if `foo.bar` is renamed 73 # to `foo.bar.baz`. 74 submodule' = module: types.coercedTo 75 (mkOptionType { 76 name = "grafana-provision-submodule"; 77 description = "Wrapper-type for backwards compat of Grafana's declarative provisioning"; 78 check = x: 79 if builtins.isList x then 80 throw '' 81 Provisioning dashboards and datasources declaratively by 82 setting `dashboards` or `datasources` to a list is not supported 83 anymore. Use `services.grafana.provision.datasources.settings.datasources` 84 (or `services.grafana.provision.dashboards.settings.providers`) instead. 85 '' 86 else isAttrs x || isFunction x; 87 }) 88 id 89 (types.submodule module); 90 91 # http://docs.grafana.org/administration/provisioning/#datasources 92 grafanaTypes.datasourceConfig = types.submodule { 93 freeformType = provisioningSettingsFormat.type; 94 95 options = { 96 name = mkOption { 97 type = types.str; 98 description = lib.mdDoc "Name of the datasource. Required."; 99 }; 100 type = mkOption { 101 type = types.str; 102 description = lib.mdDoc "Datasource type. Required."; 103 }; 104 access = mkOption { 105 type = types.enum ["proxy" "direct"]; 106 default = "proxy"; 107 description = lib.mdDoc "Access mode. proxy or direct (Server or Browser in the UI). Required."; 108 }; 109 uid = mkOption { 110 type = types.nullOr types.str; 111 default = null; 112 description = lib.mdDoc "Custom UID which can be used to reference this datasource in other parts of the configuration, if not specified will be generated automatically."; 113 }; 114 url = mkOption { 115 type = types.str; 116 default = "localhost"; 117 description = lib.mdDoc "Url of the datasource."; 118 }; 119 editable = mkOption { 120 type = types.bool; 121 default = false; 122 description = lib.mdDoc "Allow users to edit datasources from the UI."; 123 }; 124 secureJsonData = mkOption { 125 type = types.nullOr types.attrs; 126 default = null; 127 description = lib.mdDoc '' 128 Datasource specific secure configuration. Please note that the contents of this option 129 will end up in a world-readable Nix store. Use the file provider 130 pointing at a reasonably secured file in the local filesystem 131 to work around that. Look at the documentation for details: 132 <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> 133 ''; 134 }; 135 }; 136 }; 137 138 # http://docs.grafana.org/administration/provisioning/#dashboards 139 grafanaTypes.dashboardConfig = types.submodule { 140 freeformType = provisioningSettingsFormat.type; 141 142 options = { 143 name = mkOption { 144 type = types.str; 145 default = "default"; 146 description = lib.mdDoc "A unique provider name."; 147 }; 148 type = mkOption { 149 type = types.str; 150 default = "file"; 151 description = lib.mdDoc "Dashboard provider type."; 152 }; 153 options.path = mkOption { 154 type = types.path; 155 description = lib.mdDoc "Path grafana will watch for dashboards. Required when using the 'file' type."; 156 }; 157 }; 158 }; 159 160 grafanaTypes.notifierConfig = types.submodule { 161 options = { 162 name = mkOption { 163 type = types.str; 164 default = "default"; 165 description = lib.mdDoc "Notifier name."; 166 }; 167 type = mkOption { 168 type = types.enum ["dingding" "discord" "email" "googlechat" "hipchat" "kafka" "line" "teams" "opsgenie" "pagerduty" "prometheus-alertmanager" "pushover" "sensu" "sensugo" "slack" "telegram" "threema" "victorops" "webhook"]; 169 description = lib.mdDoc "Notifier type."; 170 }; 171 uid = mkOption { 172 type = types.str; 173 description = lib.mdDoc "Unique notifier identifier."; 174 }; 175 org_id = mkOption { 176 type = types.int; 177 default = 1; 178 description = lib.mdDoc "Organization ID."; 179 }; 180 org_name = mkOption { 181 type = types.str; 182 default = "Main Org."; 183 description = lib.mdDoc "Organization name."; 184 }; 185 is_default = mkOption { 186 type = types.bool; 187 description = lib.mdDoc "Is the default notifier."; 188 default = false; 189 }; 190 send_reminder = mkOption { 191 type = types.bool; 192 default = true; 193 description = lib.mdDoc "Should the notifier be sent reminder notifications while alerts continue to fire."; 194 }; 195 frequency = mkOption { 196 type = types.str; 197 default = "5m"; 198 description = lib.mdDoc "How frequently should the notifier be sent reminders."; 199 }; 200 disable_resolve_message = mkOption { 201 type = types.bool; 202 default = false; 203 description = lib.mdDoc "Turn off the message that sends when an alert returns to OK."; 204 }; 205 settings = mkOption { 206 type = types.nullOr types.attrs; 207 default = null; 208 description = lib.mdDoc "Settings for the notifier type."; 209 }; 210 secure_settings = mkOption { 211 type = types.nullOr types.attrs; 212 default = null; 213 description = lib.mdDoc '' 214 Secure settings for the notifier type. Please note that the contents of this option 215 will end up in a world-readable Nix store. Use the file provider 216 pointing at a reasonably secured file in the local filesystem 217 to work around that. Look at the documentation for details: 218 <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> 219 ''; 220 }; 221 }; 222 }; 223in { 224 imports = [ 225 (mkRenamedOptionModule [ "services" "grafana" "protocol" ] [ "services" "grafana" "settings" "server" "protocol" ]) 226 (mkRenamedOptionModule [ "services" "grafana" "addr" ] [ "services" "grafana" "settings" "server" "http_addr" ]) 227 (mkRenamedOptionModule [ "services" "grafana" "port" ] [ "services" "grafana" "settings" "server" "http_port" ]) 228 (mkRenamedOptionModule [ "services" "grafana" "domain" ] [ "services" "grafana" "settings" "server" "domain" ]) 229 (mkRenamedOptionModule [ "services" "grafana" "rootUrl" ] [ "services" "grafana" "settings" "server" "root_url" ]) 230 (mkRenamedOptionModule [ "services" "grafana" "staticRootPath" ] [ "services" "grafana" "settings" "server" "static_root_path" ]) 231 (mkRenamedOptionModule [ "services" "grafana" "certFile" ] [ "services" "grafana" "settings" "server" "cert_file" ]) 232 (mkRenamedOptionModule [ "services" "grafana" "certKey" ] [ "services" "grafana" "settings" "server" "cert_key" ]) 233 (mkRenamedOptionModule [ "services" "grafana" "socket" ] [ "services" "grafana" "settings" "server" "socket" ]) 234 (mkRenamedOptionModule [ "services" "grafana" "database" "type" ] [ "services" "grafana" "settings" "database" "type" ]) 235 (mkRenamedOptionModule [ "services" "grafana" "database" "host" ] [ "services" "grafana" "settings" "database" "host" ]) 236 (mkRenamedOptionModule [ "services" "grafana" "database" "name" ] [ "services" "grafana" "settings" "database" "name" ]) 237 (mkRenamedOptionModule [ "services" "grafana" "database" "user" ] [ "services" "grafana" "settings" "database" "user" ]) 238 (mkRenamedOptionModule [ "services" "grafana" "database" "password" ] [ "services" "grafana" "settings" "database" "password" ]) 239 (mkRenamedOptionModule [ "services" "grafana" "database" "path" ] [ "services" "grafana" "settings" "database" "path" ]) 240 (mkRenamedOptionModule [ "services" "grafana" "database" "connMaxLifetime" ] [ "services" "grafana" "settings" "database" "conn_max_lifetime" ]) 241 (mkRenamedOptionModule [ "services" "grafana" "security" "adminUser" ] [ "services" "grafana" "settings" "security" "admin_user" ]) 242 (mkRenamedOptionModule [ "services" "grafana" "security" "adminPassword" ] [ "services" "grafana" "settings" "security" "admin_password" ]) 243 (mkRenamedOptionModule [ "services" "grafana" "security" "secretKey" ] [ "services" "grafana" "settings" "security" "secret_key" ]) 244 (mkRenamedOptionModule [ "services" "grafana" "server" "serveFromSubPath" ] [ "services" "grafana" "settings" "server" "serve_from_sub_path" ]) 245 (mkRenamedOptionModule [ "services" "grafana" "smtp" "enable" ] [ "services" "grafana" "settings" "smtp" "enabled" ]) 246 (mkRenamedOptionModule [ "services" "grafana" "smtp" "user" ] [ "services" "grafana" "settings" "smtp" "user" ]) 247 (mkRenamedOptionModule [ "services" "grafana" "smtp" "password" ] [ "services" "grafana" "settings" "smtp" "password" ]) 248 (mkRenamedOptionModule [ "services" "grafana" "smtp" "fromAddress" ] [ "services" "grafana" "settings" "smtp" "from_address" ]) 249 (mkRenamedOptionModule [ "services" "grafana" "users" "allowSignUp" ] [ "services" "grafana" "settings" "users" "allow_sign_up" ]) 250 (mkRenamedOptionModule [ "services" "grafana" "users" "allowOrgCreate" ] [ "services" "grafana" "settings" "users" "allow_org_create" ]) 251 (mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrg" ] [ "services" "grafana" "settings" "users" "auto_assign_org" ]) 252 (mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrgRole" ] [ "services" "grafana" "settings" "users" "auto_assign_org_role" ]) 253 (mkRenamedOptionModule [ "services" "grafana" "auth" "disableLoginForm" ] [ "services" "grafana" "settings" "auth" "disable_login_form" ]) 254 (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "enable" ] [ "services" "grafana" "settings" "auth.anonymous" "enabled" ]) 255 (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_name" ] [ "services" "grafana" "settings" "auth.anonymous" "org_name" ]) 256 (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_role" ] [ "services" "grafana" "settings" "auth.anonymous" "org_role" ]) 257 (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "enable" ] [ "services" "grafana" "settings" "auth.azuread" "enabled" ]) 258 (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowSignUp" ] [ "services" "grafana" "settings" "auth.azuread" "allow_sign_up" ]) 259 (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "clientId" ] [ "services" "grafana" "settings" "auth.azuread" "client_id" ]) 260 (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedDomains" ] [ "services" "grafana" "settings" "auth.azuread" "allowed_domains" ]) 261 (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedGroups" ] [ "services" "grafana" "settings" "auth.azuread" "allowed_groups" ]) 262 (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "enable" ] [ "services" "grafana" "settings" "auth.google" "enabled" ]) 263 (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "allowSignUp" ] [ "services" "grafana" "settings" "auth.google" "allow_sign_up" ]) 264 (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "clientId" ] [ "services" "grafana" "settings" "auth.google" "client_id" ]) 265 (mkRenamedOptionModule [ "services" "grafana" "analytics" "reporting" "enable" ] [ "services" "grafana" "settings" "analytics" "reporting_enabled" ]) 266 267 (mkRemovedOptionModule [ "services" "grafana" "database" "passwordFile" ] '' 268 This option has been removed. Use 'services.grafana.settings.database.password' with file provider instead. 269 '') 270 (mkRemovedOptionModule [ "services" "grafana" "security" "adminPasswordFile" ] '' 271 This option has been removed. Use 'services.grafana.settings.security.admin_password' with file provider instead. 272 '') 273 (mkRemovedOptionModule [ "services" "grafana" "security" "secretKeyFile" ] '' 274 This option has been removed. Use 'services.grafana.settings.security.secret_key' with file provider instead. 275 '') 276 (mkRemovedOptionModule [ "services" "grafana" "smtp" "passwordFile" ] '' 277 This option has been removed. Use 'services.grafana.settings.smtp.password' with file provider instead. 278 '') 279 (mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "clientSecretFile" ] '' 280 This option has been removed. Use 'services.grafana.settings.azuread.client_secret' with file provider instead. 281 '') 282 (mkRemovedOptionModule [ "services" "grafana" "auth" "google" "clientSecretFile" ] '' 283 This option has been removed. Use 'services.grafana.settings.google.client_secret' with file provider instead. 284 '') 285 (mkRemovedOptionModule [ "services" "grafana" "extraOptions" ] '' 286 This option has been removed. Use 'services.grafana.settings' instead. For a detailed migration guide, please 287 review the release notes of NixOS 22.11. 288 '') 289 290 (mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "tenantId" ] "This option has been deprecated upstream.") 291 ]; 292 293 options.services.grafana = { 294 enable = mkEnableOption (lib.mdDoc "grafana"); 295 296 declarativePlugins = mkOption { 297 type = with types; nullOr (listOf path); 298 default = null; 299 description = lib.mdDoc "If non-null, then a list of packages containing Grafana plugins to install. If set, plugins cannot be manually installed."; 300 example = literalExpression "with pkgs.grafanaPlugins; [ grafana-piechart-panel ]"; 301 # Make sure each plugin is added only once; otherwise building 302 # the link farm fails, since the same path is added multiple 303 # times. 304 apply = x: if isList x then lib.unique x else x; 305 }; 306 307 package = mkOption { 308 description = lib.mdDoc "Package to use."; 309 default = pkgs.grafana; 310 defaultText = literalExpression "pkgs.grafana"; 311 type = types.package; 312 }; 313 314 dataDir = mkOption { 315 description = lib.mdDoc "Data directory."; 316 default = "/var/lib/grafana"; 317 type = types.path; 318 }; 319 320 settings = mkOption { 321 description = lib.mdDoc '' 322 Grafana settings. See <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/> 323 for available options. INI format is used. 324 ''; 325 type = types.submodule { 326 freeformType = settingsFormatIni.type; 327 328 options = { 329 paths = { 330 plugins = mkOption { 331 description = lib.mdDoc "Directory where grafana will automatically scan and look for plugins"; 332 default = if (cfg.declarativePlugins == null) then "${cfg.dataDir}/plugins" else declarativePlugins; 333 defaultText = literalExpression "if (cfg.declarativePlugins == null) then \"\${cfg.dataDir}/plugins\" else declarativePlugins"; 334 type = types.path; 335 }; 336 337 provisioning = mkOption { 338 description = lib.mdDoc '' 339 Folder that contains provisioning config files that grafana will apply on startup and while running. 340 Don't change the value of this option if you are planning to use `services.grafana.provision` options. 341 ''; 342 default = provisionConfDir; 343 defaultText = "directory with links to files generated from services.grafana.provision"; 344 type = types.path; 345 }; 346 }; 347 348 server = { 349 protocol = mkOption { 350 description = lib.mdDoc "Which protocol to listen."; 351 default = "http"; 352 type = types.enum ["http" "https" "h2" "socket"]; 353 }; 354 355 http_addr = mkOption { 356 type = types.str; 357 default = "127.0.0.1"; 358 description = lib.mdDoc '' 359 Listening address. 360 361 ::: {.note} 362 This setting intentionally varies from upstream's default to be a bit more secure by default. 363 ::: 364 ''; 365 }; 366 367 http_port = mkOption { 368 description = lib.mdDoc "Listening port."; 369 default = 3000; 370 type = types.port; 371 }; 372 373 domain = mkOption { 374 description = lib.mdDoc "The public facing domain name used to access grafana from a browser."; 375 default = "localhost"; 376 type = types.str; 377 }; 378 379 root_url = mkOption { 380 description = lib.mdDoc "Full public facing url."; 381 default = "%(protocol)s://%(domain)s:%(http_port)s/"; 382 type = types.str; 383 }; 384 385 static_root_path = mkOption { 386 description = lib.mdDoc "Root path for static assets."; 387 default = "${cfg.package}/share/grafana/public"; 388 defaultText = literalExpression ''"''${package}/share/grafana/public"''; 389 type = types.str; 390 }; 391 392 enable_gzip = mkOption { 393 description = lib.mdDoc '' 394 Set this option to true to enable HTTP compression, this can improve transfer speed and bandwidth utilization. 395 It is recommended that most users set it to true. By default it is set to false for compatibility reasons. 396 ''; 397 default = false; 398 type = types.bool; 399 }; 400 401 cert_file = mkOption { 402 description = lib.mdDoc "Cert file for ssl."; 403 default = ""; 404 type = types.str; 405 }; 406 407 cert_key = mkOption { 408 description = lib.mdDoc "Cert key for ssl."; 409 default = ""; 410 type = types.str; 411 }; 412 413 socket = mkOption { 414 description = lib.mdDoc "Path where the socket should be created when protocol=socket. Make sure that Grafana has appropriate permissions before you change this setting."; 415 default = "/run/grafana/grafana.sock"; 416 type = types.str; 417 }; 418 }; 419 420 database = { 421 type = mkOption { 422 description = lib.mdDoc "Database type."; 423 default = "sqlite3"; 424 type = types.enum ["mysql" "sqlite3" "postgres"]; 425 }; 426 427 host = mkOption { 428 description = lib.mdDoc "Database host."; 429 default = "127.0.0.1:3306"; 430 type = types.str; 431 }; 432 433 name = mkOption { 434 description = lib.mdDoc "Database name."; 435 default = "grafana"; 436 type = types.str; 437 }; 438 439 user = mkOption { 440 description = lib.mdDoc "Database user."; 441 default = "root"; 442 type = types.str; 443 }; 444 445 password = mkOption { 446 description = lib.mdDoc '' 447 Database password. Please note that the contents of this option 448 will end up in a world-readable Nix store. Use the file provider 449 pointing at a reasonably secured file in the local filesystem 450 to work around that. Look at the documentation for details: 451 <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> 452 ''; 453 default = ""; 454 type = types.str; 455 }; 456 457 path = mkOption { 458 description = lib.mdDoc "Only applicable to sqlite3 database. The file path where the database will be stored."; 459 default = "${cfg.dataDir}/data/grafana.db"; 460 defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"''; 461 type = types.path; 462 }; 463 }; 464 465 security = { 466 admin_user = mkOption { 467 description = lib.mdDoc "Default admin username."; 468 default = "admin"; 469 type = types.str; 470 }; 471 472 admin_password = mkOption { 473 description = lib.mdDoc '' 474 Default admin password. Please note that the contents of this option 475 will end up in a world-readable Nix store. Use the file provider 476 pointing at a reasonably secured file in the local filesystem 477 to work around that. Look at the documentation for details: 478 <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> 479 ''; 480 default = "admin"; 481 type = types.str; 482 }; 483 484 secret_key = mkOption { 485 description = lib.mdDoc '' 486 Secret key used for signing. Please note that the contents of this option 487 will end up in a world-readable Nix store. Use the file provider 488 pointing at a reasonably secured file in the local filesystem 489 to work around that. Look at the documentation for details: 490 <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> 491 ''; 492 default = "SW2YcwTIb9zpOOhoPsMm"; 493 type = types.str; 494 }; 495 }; 496 497 smtp = { 498 enabled = mkOption { 499 description = lib.mdDoc "Whether to enable SMTP."; 500 default = false; 501 type = types.bool; 502 }; 503 host = mkOption { 504 description = lib.mdDoc "Host to connect to."; 505 default = "localhost:25"; 506 type = types.str; 507 }; 508 user = mkOption { 509 description = lib.mdDoc "User used for authentication."; 510 default = ""; 511 type = types.str; 512 }; 513 password = mkOption { 514 description = lib.mdDoc '' 515 Password used for authentication. Please note that the contents of this option 516 will end up in a world-readable Nix store. Use the file provider 517 pointing at a reasonably secured file in the local filesystem 518 to work around that. Look at the documentation for details: 519 <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> 520 ''; 521 default = ""; 522 type = types.str; 523 }; 524 from_address = mkOption { 525 description = lib.mdDoc "Email address used for sending."; 526 default = "admin@grafana.localhost"; 527 type = types.str; 528 }; 529 }; 530 531 users = { 532 allow_sign_up = mkOption { 533 description = lib.mdDoc "Disable user signup / registration."; 534 default = false; 535 type = types.bool; 536 }; 537 538 allow_org_create = mkOption { 539 description = lib.mdDoc "Whether user is allowed to create organizations."; 540 default = false; 541 type = types.bool; 542 }; 543 544 auto_assign_org = mkOption { 545 description = lib.mdDoc "Whether to automatically assign new users to default org."; 546 default = true; 547 type = types.bool; 548 }; 549 550 auto_assign_org_role = mkOption { 551 description = lib.mdDoc "Default role new users will be auto assigned."; 552 default = "Viewer"; 553 type = types.enum ["Viewer" "Editor" "Admin"]; 554 }; 555 }; 556 557 analytics.reporting_enabled = mkOption { 558 description = lib.mdDoc "Whether to allow anonymous usage reporting to stats.grafana.net."; 559 default = true; 560 type = types.bool; 561 }; 562 }; 563 }; 564 }; 565 566 provision = { 567 enable = mkEnableOption (lib.mdDoc "provision"); 568 569 datasources = mkOption { 570 description = lib.mdDoc '' 571 Declaratively provision Grafana's datasources. 572 ''; 573 default = {}; 574 type = submodule' { 575 options.settings = mkOption { 576 description = lib.mdDoc '' 577 Grafana datasource configuration in Nix. Can't be used with 578 [](#opt-services.grafana.provision.datasources.path) simultaneously. See 579 <https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources> 580 for supported options. 581 ''; 582 default = null; 583 type = types.nullOr (types.submodule { 584 options = { 585 apiVersion = mkOption { 586 description = lib.mdDoc "Config file version."; 587 default = 1; 588 type = types.int; 589 }; 590 591 datasources = mkOption { 592 description = lib.mdDoc "List of datasources to insert/update."; 593 default = []; 594 type = types.listOf grafanaTypes.datasourceConfig; 595 }; 596 597 deleteDatasources = mkOption { 598 description = lib.mdDoc "List of datasources that should be deleted from the database."; 599 default = []; 600 type = types.listOf (types.submodule { 601 options.name = mkOption { 602 description = lib.mdDoc "Name of the datasource to delete."; 603 type = types.str; 604 }; 605 606 options.orgId = mkOption { 607 description = lib.mdDoc "Organization ID of the datasource to delete."; 608 type = types.int; 609 }; 610 }); 611 }; 612 }; 613 }); 614 example = literalExpression '' 615 { 616 apiVersion = 1; 617 618 datasources = [{ 619 name = "Graphite"; 620 type = "graphite"; 621 }]; 622 623 deleteDatasources = [{ 624 name = "Graphite"; 625 orgId = 1; 626 }]; 627 } 628 ''; 629 }; 630 631 options.path = mkOption { 632 description = lib.mdDoc '' 633 Path to YAML datasource configuration. Can't be used with 634 [](#opt-services.grafana.provision.datasources.settings) simultaneously. 635 Can be either a directory or a single YAML file. Will end up in the store. 636 ''; 637 default = null; 638 type = types.nullOr types.path; 639 }; 640 }; 641 }; 642 643 644 dashboards = mkOption { 645 description = lib.mdDoc '' 646 Declaratively provision Grafana's dashboards. 647 ''; 648 default = {}; 649 type = submodule' { 650 options.settings = mkOption { 651 description = lib.mdDoc '' 652 Grafana dashboard configuration in Nix. Can't be used with 653 [](#opt-services.grafana.provision.dashboards.path) simultaneously. See 654 <https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards> 655 for supported options. 656 ''; 657 default = null; 658 type = types.nullOr (types.submodule { 659 options.apiVersion = mkOption { 660 description = lib.mdDoc "Config file version."; 661 default = 1; 662 type = types.int; 663 }; 664 665 options.providers = mkOption { 666 description = lib.mdDoc "List of dashboards to insert/update."; 667 default = []; 668 type = types.listOf grafanaTypes.dashboardConfig; 669 }; 670 }); 671 example = literalExpression '' 672 { 673 apiVersion = 1; 674 675 providers = [{ 676 name = "default"; 677 options.path = "/var/lib/grafana/dashboards"; 678 }]; 679 } 680 ''; 681 }; 682 683 options.path = mkOption { 684 description = lib.mdDoc '' 685 Path to YAML dashboard configuration. Can't be used with 686 [](#opt-services.grafana.provision.dashboards.settings) simultaneously. 687 Can be either a directory or a single YAML file. Will end up in the store. 688 ''; 689 default = null; 690 type = types.nullOr types.path; 691 }; 692 }; 693 }; 694 695 696 notifiers = mkOption { 697 description = lib.mdDoc "Grafana notifier configuration."; 698 default = []; 699 type = types.listOf grafanaTypes.notifierConfig; 700 apply = x: map _filter x; 701 }; 702 703 704 alerting = { 705 rules = { 706 path = mkOption { 707 description = lib.mdDoc '' 708 Path to YAML rules configuration. Can't be used with 709 [](#opt-services.grafana.provision.alerting.rules.settings) simultaneously. 710 Can be either a directory or a single YAML file. Will end up in the store. 711 ''; 712 default = null; 713 type = types.nullOr types.path; 714 }; 715 716 settings = mkOption { 717 description = lib.mdDoc '' 718 Grafana rules configuration in Nix. Can't be used with 719 [](#opt-services.grafana.provision.alerting.rules.path) simultaneously. See 720 <https://grafana.com/docs/grafana/latest/administration/provisioning/#rules> 721 for supported options. 722 ''; 723 default = null; 724 type = types.nullOr (types.submodule { 725 options = { 726 apiVersion = mkOption { 727 description = lib.mdDoc "Config file version."; 728 default = 1; 729 type = types.int; 730 }; 731 732 groups = mkOption { 733 description = lib.mdDoc "List of rule groups to import or update."; 734 default = []; 735 type = types.listOf (types.submodule { 736 freeformType = provisioningSettingsFormat.type; 737 738 options.name = mkOption { 739 description = lib.mdDoc "Name of the rule group. Required."; 740 type = types.str; 741 }; 742 743 options.folder = mkOption { 744 description = lib.mdDoc "Name of the folder the rule group will be stored in. Required."; 745 type = types.str; 746 }; 747 748 options.interval = mkOption { 749 description = lib.mdDoc "Interval that the rule group should be evaluated at. Required."; 750 type = types.str; 751 }; 752 }); 753 }; 754 755 deleteRules = mkOption { 756 description = lib.mdDoc "List of alert rule UIDs that should be deleted."; 757 default = []; 758 type = types.listOf (types.submodule { 759 options.orgId = mkOption { 760 description = lib.mdDoc "Organization ID, default = 1"; 761 default = 1; 762 type = types.int; 763 }; 764 765 options.uid = mkOption { 766 description = lib.mdDoc "Unique identifier for the rule. Required."; 767 type = types.str; 768 }; 769 }); 770 }; 771 }; 772 }); 773 example = literalExpression '' 774 { 775 apiVersion = 1; 776 777 groups = [{ 778 orgId = 1; 779 name = "my_rule_group"; 780 folder = "my_first_folder"; 781 interval = "60s"; 782 rules = [{ 783 uid = "my_id_1"; 784 title = "my_first_rule"; 785 condition = "A"; 786 data = [{ 787 refId = "A"; 788 datasourceUid = "-100"; 789 model = { 790 conditions = [{ 791 evaluator = { 792 params = [ 3 ]; 793 type = "git"; 794 }; 795 operator.type = "and"; 796 query.params = [ "A" ]; 797 reducer.type = "last"; 798 type = "query"; 799 }]; 800 datasource = { 801 type = "__expr__"; 802 uid = "-100"; 803 }; 804 expression = "1==0"; 805 intervalMs = 1000; 806 maxDataPoints = 43200; 807 refId = "A"; 808 type = "math"; 809 }; 810 }]; 811 dashboardUid = "my_dashboard"; 812 panelId = 123; 813 noDataState = "Alerting"; 814 for = "60s"; 815 annotations.some_key = "some_value"; 816 labels.team = "sre_team1"; 817 }]; 818 }]; 819 820 deleteRules = [{ 821 orgId = 1; 822 uid = "my_id_1"; 823 }]; 824 } 825 ''; 826 }; 827 }; 828 829 contactPoints = { 830 path = mkOption { 831 description = lib.mdDoc '' 832 Path to YAML contact points configuration. Can't be used with 833 [](#opt-services.grafana.provision.alerting.contactPoints.settings) simultaneously. 834 Can be either a directory or a single YAML file. Will end up in the store. 835 ''; 836 default = null; 837 type = types.nullOr types.path; 838 }; 839 840 settings = mkOption { 841 description = lib.mdDoc '' 842 Grafana contact points configuration in Nix. Can't be used with 843 [](#opt-services.grafana.provision.alerting.contactPoints.path) simultaneously. See 844 <https://grafana.com/docs/grafana/latest/administration/provisioning/#contact-points> 845 for supported options. 846 ''; 847 default = null; 848 type = types.nullOr (types.submodule { 849 options = { 850 apiVersion = mkOption { 851 description = lib.mdDoc "Config file version."; 852 default = 1; 853 type = types.int; 854 }; 855 856 contactPoints = mkOption { 857 description = lib.mdDoc "List of contact points to import or update."; 858 default = []; 859 type = types.listOf (types.submodule { 860 freeformType = provisioningSettingsFormat.type; 861 862 options.name = mkOption { 863 description = lib.mdDoc "Name of the contact point. Required."; 864 type = types.str; 865 }; 866 }); 867 }; 868 869 deleteContactPoints = mkOption { 870 description = lib.mdDoc "List of receivers that should be deleted."; 871 default = []; 872 type = types.listOf (types.submodule { 873 options.orgId = mkOption { 874 description = lib.mdDoc "Organization ID, default = 1."; 875 default = 1; 876 type = types.int; 877 }; 878 879 options.uid = mkOption { 880 description = lib.mdDoc "Unique identifier for the receiver. Required."; 881 type = types.str; 882 }; 883 }); 884 }; 885 }; 886 }); 887 example = literalExpression '' 888 { 889 apiVersion = 1; 890 891 contactPoints = [{ 892 orgId = 1; 893 name = "cp_1"; 894 receivers = [{ 895 uid = "first_uid"; 896 type = "prometheus-alertmanager"; 897 settings.url = "http://test:9000"; 898 }]; 899 }]; 900 901 deleteContactPoints = [{ 902 orgId = 1; 903 uid = "first_uid"; 904 }]; 905 } 906 ''; 907 }; 908 }; 909 910 policies = { 911 path = mkOption { 912 description = lib.mdDoc '' 913 Path to YAML notification policies configuration. Can't be used with 914 [](#opt-services.grafana.provision.alerting.policies.settings) simultaneously. 915 Can be either a directory or a single YAML file. Will end up in the store. 916 ''; 917 default = null; 918 type = types.nullOr types.path; 919 }; 920 921 settings = mkOption { 922 description = lib.mdDoc '' 923 Grafana notification policies configuration in Nix. Can't be used with 924 [](#opt-services.grafana.provision.alerting.policies.path) simultaneously. See 925 <https://grafana.com/docs/grafana/latest/administration/provisioning/#notification-policies> 926 for supported options. 927 ''; 928 default = null; 929 type = types.nullOr (types.submodule { 930 options = { 931 apiVersion = mkOption { 932 description = lib.mdDoc "Config file version."; 933 default = 1; 934 type = types.int; 935 }; 936 937 policies = mkOption { 938 description = lib.mdDoc "List of contact points to import or update."; 939 default = []; 940 type = types.listOf (types.submodule { 941 freeformType = provisioningSettingsFormat.type; 942 }); 943 }; 944 945 resetPolicies = mkOption { 946 description = lib.mdDoc "List of orgIds that should be reset to the default policy."; 947 default = []; 948 type = types.listOf types.int; 949 }; 950 }; 951 }); 952 example = literalExpression '' 953 { 954 apiVersion = 1; 955 956 policies = [{ 957 orgId = 1; 958 receiver = "grafana-default-email"; 959 group_by = [ "..." ]; 960 matchers = [ 961 "alertname = Watchdog" 962 "severity =~ \"warning|critical\"" 963 ]; 964 mute_time_intervals = [ 965 "abc" 966 ]; 967 group_wait = "30s"; 968 group_interval = "5m"; 969 repeat_interval = "4h"; 970 }]; 971 972 resetPolicies = [ 973 1 974 ]; 975 } 976 ''; 977 }; 978 }; 979 980 templates = { 981 path = mkOption { 982 description = lib.mdDoc '' 983 Path to YAML templates configuration. Can't be used with 984 [](#opt-services.grafana.provision.alerting.templates.settings) simultaneously. 985 Can be either a directory or a single YAML file. Will end up in the store. 986 ''; 987 default = null; 988 type = types.nullOr types.path; 989 }; 990 991 settings = mkOption { 992 description = lib.mdDoc '' 993 Grafana templates configuration in Nix. Can't be used with 994 [](#opt-services.grafana.provision.alerting.templates.path) simultaneously. See 995 <https://grafana.com/docs/grafana/latest/administration/provisioning/#templates> 996 for supported options. 997 ''; 998 default = null; 999 type = types.nullOr (types.submodule { 1000 options = { 1001 apiVersion = mkOption { 1002 description = lib.mdDoc "Config file version."; 1003 default = 1; 1004 type = types.int; 1005 }; 1006 1007 templates = mkOption { 1008 description = lib.mdDoc "List of templates to import or update."; 1009 default = []; 1010 type = types.listOf (types.submodule { 1011 freeformType = provisioningSettingsFormat.type; 1012 1013 options.name = mkOption { 1014 description = lib.mdDoc "Name of the template, must be unique. Required."; 1015 type = types.str; 1016 }; 1017 1018 options.template = mkOption { 1019 description = lib.mdDoc "Alerting with a custom text template"; 1020 type = types.str; 1021 }; 1022 }); 1023 }; 1024 1025 deleteTemplates = mkOption { 1026 description = lib.mdDoc "List of alert rule UIDs that should be deleted."; 1027 default = []; 1028 type = types.listOf (types.submodule { 1029 options.orgId = mkOption { 1030 description = lib.mdDoc "Organization ID, default = 1."; 1031 default = 1; 1032 type = types.int; 1033 }; 1034 1035 options.name = mkOption { 1036 description = lib.mdDoc "Name of the template, must be unique. Required."; 1037 type = types.str; 1038 }; 1039 }); 1040 }; 1041 }; 1042 }); 1043 example = literalExpression '' 1044 { 1045 apiVersion = 1; 1046 1047 templates = [{ 1048 orgId = 1; 1049 name = "my_first_template"; 1050 template = "Alerting with a custom text template"; 1051 }]; 1052 1053 deleteTemplates = [{ 1054 orgId = 1; 1055 name = "my_first_template"; 1056 }]; 1057 } 1058 ''; 1059 }; 1060 }; 1061 1062 muteTimings = { 1063 path = mkOption { 1064 description = lib.mdDoc '' 1065 Path to YAML mute timings configuration. Can't be used with 1066 [](#opt-services.grafana.provision.alerting.muteTimings.settings) simultaneously. 1067 Can be either a directory or a single YAML file. Will end up in the store. 1068 ''; 1069 default = null; 1070 type = types.nullOr types.path; 1071 }; 1072 1073 settings = mkOption { 1074 description = lib.mdDoc '' 1075 Grafana mute timings configuration in Nix. Can't be used with 1076 [](#opt-services.grafana.provision.alerting.muteTimings.path) simultaneously. See 1077 <https://grafana.com/docs/grafana/latest/administration/provisioning/#mute-timings> 1078 for supported options. 1079 ''; 1080 default = null; 1081 type = types.nullOr (types.submodule { 1082 options = { 1083 apiVersion = mkOption { 1084 description = lib.mdDoc "Config file version."; 1085 default = 1; 1086 type = types.int; 1087 }; 1088 1089 muteTimes = mkOption { 1090 description = lib.mdDoc "List of mute time intervals to import or update."; 1091 default = []; 1092 type = types.listOf (types.submodule { 1093 freeformType = provisioningSettingsFormat.type; 1094 1095 options.name = mkOption { 1096 description = lib.mdDoc "Name of the mute time interval, must be unique. Required."; 1097 type = types.str; 1098 }; 1099 }); 1100 }; 1101 1102 deleteMuteTimes = mkOption { 1103 description = lib.mdDoc "List of mute time intervals that should be deleted."; 1104 default = []; 1105 type = types.listOf (types.submodule { 1106 options.orgId = mkOption { 1107 description = lib.mdDoc "Organization ID, default = 1."; 1108 default = 1; 1109 type = types.int; 1110 }; 1111 1112 options.name = mkOption { 1113 description = lib.mdDoc "Name of the mute time interval, must be unique. Required."; 1114 type = types.str; 1115 }; 1116 }); 1117 }; 1118 }; 1119 }); 1120 example = literalExpression '' 1121 { 1122 apiVersion = 1; 1123 1124 muteTimes = [{ 1125 orgId = 1; 1126 name = "mti_1"; 1127 time_intervals = [{ 1128 times = [{ 1129 start_time = "06:00"; 1130 end_time = "23:59"; 1131 }]; 1132 weekdays = [ 1133 "monday:wednesday" 1134 "saturday" 1135 "sunday" 1136 ]; 1137 months = [ 1138 "1:3" 1139 "may:august" 1140 "december" 1141 ]; 1142 years = [ 1143 "2020:2022" 1144 "2030" 1145 ]; 1146 days_of_month = [ 1147 "1:5" 1148 "-3:-1" 1149 ]; 1150 }]; 1151 }]; 1152 1153 deleteMuteTimes = [{ 1154 orgId = 1; 1155 name = "mti_1"; 1156 }]; 1157 } 1158 ''; 1159 }; 1160 }; 1161 }; 1162 }; 1163 }; 1164 1165 config = mkIf cfg.enable { 1166 warnings = let 1167 doesntUseFileProvider = opt: defaultValue: 1168 let 1169 regex = "${optionalString (defaultValue != null) "^${defaultValue}$|"}^\\$__(file|env)\\{.*}$|^\\$[^_\\$][^ ]+$"; 1170 in builtins.match regex opt == null; 1171 in 1172 # Ensure that no custom credentials are leaked into the Nix store. Unless the default value 1173 # is specified, this can be achieved by using the file/env provider: 1174 # https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#variable-expansion 1175 (optional ( 1176 doesntUseFileProvider cfg.settings.database.password "" || 1177 doesntUseFileProvider cfg.settings.security.admin_password "admin" 1178 ) '' 1179 Grafana passwords will be stored as plaintext in the Nix store! 1180 Use file provider or an env-var instead. 1181 '') 1182 # Warn about deprecated notifiers. 1183 ++ (optional (cfg.provision.notifiers != []) '' 1184 Notifiers are deprecated upstream and will be removed in Grafana 10. 1185 Use `services.grafana.provision.alerting.contactPoints` instead. 1186 '') 1187 # Ensure that `secureJsonData` of datasources provisioned via `datasources.settings` 1188 # only uses file/env providers. 1189 ++ (optional ( 1190 let 1191 datasourcesToCheck = optionals 1192 (cfg.provision.datasources.settings != null) 1193 cfg.provision.datasources.settings.datasources; 1194 declarationUnsafe = { secureJsonData, ... }: 1195 secureJsonData != null 1196 && any (flip doesntUseFileProvider null) (attrValues secureJsonData); 1197 in any declarationUnsafe datasourcesToCheck 1198 ) '' 1199 Declarations in the `secureJsonData`-block of a datasource will be leaked to the 1200 Nix store unless a file-provider or an env-var is used! 1201 '') 1202 ++ (optional ( 1203 any (x: x.secure_settings != null) cfg.provision.notifiers 1204 ) "Notifier secure settings will be stored as plaintext in the Nix store! Use file provider instead."); 1205 1206 environment.systemPackages = [ cfg.package ]; 1207 1208 assertions = [ 1209 { 1210 assertion = cfg.provision.datasources.settings == null || cfg.provision.datasources.path == null; 1211 message = "Cannot set both datasources settings and datasources path"; 1212 } 1213 { 1214 assertion = let 1215 prometheusIsNotDirect = opt: all 1216 ({ type, access, ... }: type == "prometheus" -> access != "direct") 1217 opt; 1218 in 1219 cfg.provision.datasources.settings == null || prometheusIsNotDirect cfg.provision.datasources.settings.datasources; 1220 message = "For datasources of type `prometheus`, the `direct` access mode is not supported anymore (since Grafana 9.2.0)"; 1221 } 1222 { 1223 assertion = cfg.provision.dashboards.settings == null || cfg.provision.dashboards.path == null; 1224 message = "Cannot set both dashboards settings and dashboards path"; 1225 } 1226 { 1227 assertion = cfg.provision.alerting.rules.settings == null || cfg.provision.alerting.rules.path == null; 1228 message = "Cannot set both rules settings and rules path"; 1229 } 1230 { 1231 assertion = cfg.provision.alerting.contactPoints.settings == null || cfg.provision.alerting.contactPoints.path == null; 1232 message = "Cannot set both contact points settings and contact points path"; 1233 } 1234 { 1235 assertion = cfg.provision.alerting.policies.settings == null || cfg.provision.alerting.policies.path == null; 1236 message = "Cannot set both policies settings and policies path"; 1237 } 1238 { 1239 assertion = cfg.provision.alerting.templates.settings == null || cfg.provision.alerting.templates.path == null; 1240 message = "Cannot set both templates settings and templates path"; 1241 } 1242 { 1243 assertion = cfg.provision.alerting.muteTimings.settings == null || cfg.provision.alerting.muteTimings.path == null; 1244 message = "Cannot set both mute timings settings and mute timings path"; 1245 } 1246 ]; 1247 1248 systemd.services.grafana = { 1249 description = "Grafana Service Daemon"; 1250 wantedBy = ["multi-user.target"]; 1251 after = ["networking.target"] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service"; 1252 script = '' 1253 set -o errexit -o pipefail -o nounset -o errtrace 1254 shopt -s inherit_errexit 1255 1256 exec ${cfg.package}/bin/grafana-server -homepath ${cfg.dataDir} -config ${configFile} 1257 ''; 1258 serviceConfig = { 1259 WorkingDirectory = cfg.dataDir; 1260 User = "grafana"; 1261 RuntimeDirectory = "grafana"; 1262 RuntimeDirectoryMode = "0755"; 1263 # Hardening 1264 AmbientCapabilities = lib.mkIf (cfg.settings.server.http_port < 1024) [ "CAP_NET_BIND_SERVICE" ]; 1265 CapabilityBoundingSet = if (cfg.settings.server.http_port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ]; 1266 DeviceAllow = [ "" ]; 1267 LockPersonality = true; 1268 NoNewPrivileges = true; 1269 PrivateDevices = true; 1270 PrivateTmp = true; 1271 ProtectClock = true; 1272 ProtectControlGroups = true; 1273 ProtectHome = true; 1274 ProtectHostname = true; 1275 ProtectKernelLogs = true; 1276 ProtectKernelModules = true; 1277 ProtectKernelTunables = true; 1278 ProtectProc = "invisible"; 1279 ProtectSystem = "full"; 1280 RemoveIPC = true; 1281 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; 1282 RestrictNamespaces = true; 1283 RestrictRealtime = true; 1284 RestrictSUIDSGID = true; 1285 SystemCallArchitectures = "native"; 1286 # Upstream grafana is not setting SystemCallFilter for compatibility 1287 # reasons, see https://github.com/grafana/grafana/pull/40176 1288 SystemCallFilter = [ 1289 "@system-service" 1290 "~@privileged" 1291 ] ++ lib.optionals (cfg.settings.server.protocol == "socket") [ "@chown" ]; 1292 UMask = "0027"; 1293 }; 1294 preStart = '' 1295 ln -fs ${cfg.package}/share/grafana/conf ${cfg.dataDir} 1296 ln -fs ${cfg.package}/share/grafana/tools ${cfg.dataDir} 1297 ''; 1298 }; 1299 1300 users.users.grafana = { 1301 uid = config.ids.uids.grafana; 1302 description = "Grafana user"; 1303 home = cfg.dataDir; 1304 createHome = true; 1305 group = "grafana"; 1306 }; 1307 users.groups.grafana = {}; 1308 }; 1309}