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}