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}