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