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