diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
index a84549bda1fb..933522591af4 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
@@ -873,6 +873,34 @@
has been hardened.
+
+
+ The services.grafana options were converted
+ to a
+ RFC
+ 0042 configuration.
+
+
+
+
+ The services.grafana.provision.datasources
+ and services.grafana.provision.dashboards
+ options were converted to a
+ RFC
+ 0042 configuration. They also now support specifying
+ the provisioning YAML file with path
+ option.
+
+
+
+
+ The services.grafana.provision.alerting
+ option was added. It includes suboptions for every
+ alerting-related objects (with the exception of
+ notifiers), which means it’s now possible
+ to configure modern Grafana alerting declaratively.
+
+
Matrix Synapse now requires entries in the
diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md
index 53afe1ca4d68..b91014bdb21a 100644
--- a/nixos/doc/manual/release-notes/rl-2211.section.md
+++ b/nixos/doc/manual/release-notes/rl-2211.section.md
@@ -282,6 +282,12 @@ Available as [services.patroni](options.html#opt-services.patroni.enable).
- The `services.matrix-synapse` systemd unit has been hardened.
+- The `services.grafana` options were converted to a [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) configuration.
+
+- The `services.grafana.provision.datasources` and `services.grafana.provision.dashboards` options were converted to a [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) configuration. They also now support specifying the provisioning YAML file with `path` option.
+
+- The `services.grafana.provision.alerting` option was added. It includes suboptions for every alerting-related objects (with the exception of `notifiers`), which means it's now possible to configure modern Grafana alerting declaratively.
+
- Matrix Synapse now requires entries in the `state_group_edges` table to be unique, in order to prevent accidentally introducing duplicate information (for example, because a database backup was restored multiple times). If your Synapse database already has duplicate rows in this table, this could fail with an error and require manual remediation.
- The `diamond` package has been update from 0.8.36 to 2.0.15. See the [upstream release notes](https://github.com/bbuchfink/diamond/releases) for more details.
diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix
index 549da138fe23..60f6e84c63c7 100644
--- a/nixos/modules/services/monitoring/grafana-image-renderer.nix
+++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix
@@ -106,9 +106,9 @@ in {
}
];
- services.grafana.extraOptions = mkIf cfg.provisionGrafana {
- RENDERING_SERVER_URL = "http://localhost:${toString cfg.settings.service.port}/render";
- RENDERING_CALLBACK_URL = "http://localhost:${toString config.services.grafana.port}";
+ services.grafana.settings.rendering = mkIf cfg.provisionGrafana {
+ url = "http://localhost:${toString cfg.settings.service.port}/render";
+ callback_url = "http://localhost:${toString config.services.grafana.port}";
};
services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix
index 15ca11a2e164..964602547e7d 100644
--- a/nixos/modules/services/monitoring/grafana.nix
+++ b/nixos/modules/services/monitoring/grafana.nix
@@ -5,86 +5,29 @@ with lib;
let
cfg = config.services.grafana;
opt = options.services.grafana;
+ provisioningSettingsFormat = pkgs.formats.yaml {};
declarativePlugins = pkgs.linkFarm "grafana-plugins" (builtins.map (pkg: { name = pkg.pname; path = pkg; }) cfg.declarativePlugins);
- useMysql = cfg.database.type == "mysql";
- usePostgresql = cfg.database.type == "postgres";
+ useMysql = cfg.settings.database.type == "mysql";
+ usePostgresql = cfg.settings.database.type == "postgres";
- envOptions = {
- PATHS_DATA = cfg.dataDir;
- PATHS_PLUGINS = if builtins.isNull cfg.declarativePlugins then "${cfg.dataDir}/plugins" else declarativePlugins;
- PATHS_LOGS = "${cfg.dataDir}/log";
-
- SERVER_SERVE_FROM_SUBPATH = boolToString cfg.server.serveFromSubPath;
- SERVER_PROTOCOL = cfg.protocol;
- SERVER_HTTP_ADDR = cfg.addr;
- SERVER_HTTP_PORT = cfg.port;
- SERVER_SOCKET = cfg.socket;
- SERVER_DOMAIN = cfg.domain;
- SERVER_ROOT_URL = cfg.rootUrl;
- SERVER_STATIC_ROOT_PATH = cfg.staticRootPath;
- SERVER_CERT_FILE = cfg.certFile;
- SERVER_CERT_KEY = cfg.certKey;
-
- DATABASE_TYPE = cfg.database.type;
- DATABASE_HOST = cfg.database.host;
- DATABASE_NAME = cfg.database.name;
- DATABASE_USER = cfg.database.user;
- DATABASE_PASSWORD = cfg.database.password;
- DATABASE_PATH = cfg.database.path;
- DATABASE_CONN_MAX_LIFETIME = cfg.database.connMaxLifetime;
-
- SECURITY_ADMIN_USER = cfg.security.adminUser;
- SECURITY_ADMIN_PASSWORD = cfg.security.adminPassword;
- SECURITY_SECRET_KEY = cfg.security.secretKey;
-
- USERS_ALLOW_SIGN_UP = boolToString cfg.users.allowSignUp;
- USERS_ALLOW_ORG_CREATE = boolToString cfg.users.allowOrgCreate;
- USERS_AUTO_ASSIGN_ORG = boolToString cfg.users.autoAssignOrg;
- USERS_AUTO_ASSIGN_ORG_ROLE = cfg.users.autoAssignOrgRole;
-
- AUTH_DISABLE_LOGIN_FORM = boolToString cfg.auth.disableLoginForm;
-
- AUTH_ANONYMOUS_ENABLED = boolToString cfg.auth.anonymous.enable;
- AUTH_ANONYMOUS_ORG_NAME = cfg.auth.anonymous.org_name;
- AUTH_ANONYMOUS_ORG_ROLE = cfg.auth.anonymous.org_role;
-
- AUTH_AZUREAD_NAME = "Azure AD";
- AUTH_AZUREAD_ENABLED = boolToString cfg.auth.azuread.enable;
- AUTH_AZUREAD_ALLOW_SIGN_UP = boolToString cfg.auth.azuread.allowSignUp;
- AUTH_AZUREAD_CLIENT_ID = cfg.auth.azuread.clientId;
- AUTH_AZUREAD_SCOPES = "openid email profile";
- AUTH_AZUREAD_AUTH_URL = "https://login.microsoftonline.com/${cfg.auth.azuread.tenantId}/oauth2/v2.0/authorize";
- AUTH_AZUREAD_TOKEN_URL = "https://login.microsoftonline.com/${cfg.auth.azuread.tenantId}/oauth2/v2.0/token";
- AUTH_AZUREAD_ALLOWED_DOMAINS = cfg.auth.azuread.allowedDomains;
- AUTH_AZUREAD_ALLOWED_GROUPS = cfg.auth.azuread.allowedGroups;
- AUTH_AZUREAD_ROLE_ATTRIBUTE_STRICT = false;
-
- AUTH_GOOGLE_ENABLED = boolToString cfg.auth.google.enable;
- AUTH_GOOGLE_ALLOW_SIGN_UP = boolToString cfg.auth.google.allowSignUp;
- AUTH_GOOGLE_CLIENT_ID = cfg.auth.google.clientId;
-
- ANALYTICS_REPORTING_ENABLED = boolToString cfg.analytics.reporting.enable;
-
- SMTP_ENABLED = boolToString cfg.smtp.enable;
- SMTP_HOST = cfg.smtp.host;
- SMTP_USER = cfg.smtp.user;
- SMTP_PASSWORD = cfg.smtp.password;
- SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
- } // cfg.extraOptions;
+ settingsFormatIni = pkgs.formats.ini {};
+ configFile = settingsFormatIni.generate "config.ini" cfg.settings;
datasourceConfiguration = {
apiVersion = 1;
datasources = cfg.provision.datasources;
};
- datasourceFile = pkgs.writeText "datasource.yaml" (builtins.toJSON datasourceConfiguration);
+ datasourceFileNew = if (cfg.provision.datasources.path == null) then provisioningSettingsFormat.generate "datasource.yaml" cfg.provision.datasources.settings else cfg.provision.datasources.path;
+ datasourceFile = if (builtins.isList cfg.provision.datasources) then provisioningSettingsFormat.generate "datasource.yaml" datasourceConfiguration else datasourceFileNew;
dashboardConfiguration = {
apiVersion = 1;
providers = cfg.provision.dashboards;
};
- dashboardFile = pkgs.writeText "dashboard.yaml" (builtins.toJSON dashboardConfiguration);
+ dashboardFileNew = if (cfg.provision.dashboards.path == null) then provisioningSettingsFormat.generate "dashboard.yaml" cfg.provision.dashboards.settings else cfg.provision.dashboards.path;
+ dashboardFile = if (builtins.isList cfg.provision.dashboards) then provisioningSettingsFormat.generate "dashboard.yaml" dashboardConfiguration else dashboardFileNew;
notifierConfiguration = {
apiVersion = 1;
@@ -93,11 +36,25 @@ let
notifierFile = pkgs.writeText "notifier.yaml" (builtins.toJSON notifierConfiguration);
+ generateAlertingProvisioningYaml = x: if (cfg.provision.alerting."${x}".path == null)
+ then provisioningSettingsFormat.generate "${x}.yaml" cfg.provision.alerting."${x}".settings
+ else cfg.provision.alerting."${x}".path;
+ rulesFile = generateAlertingProvisioningYaml "rules";
+ contactPointsFile = generateAlertingProvisioningYaml "contactPoints";
+ policiesFile = generateAlertingProvisioningYaml "policies";
+ templatesFile = generateAlertingProvisioningYaml "templates";
+ muteTimingsFile = generateAlertingProvisioningYaml "muteTimings";
+
provisionConfDir = pkgs.runCommand "grafana-provisioning" { } ''
- mkdir -p $out/{datasources,dashboards,notifiers}
+ mkdir -p $out/{datasources,dashboards,notifiers,alerting}
ln -sf ${datasourceFile} $out/datasources/datasource.yaml
ln -sf ${dashboardFile} $out/dashboards/dashboard.yaml
ln -sf ${notifierFile} $out/notifiers/notifier.yaml
+ ln -sf ${rulesFile} $out/alerting/rules.yaml
+ ln -sf ${contactPointsFile} $out/alerting/contactPoints.yaml
+ ln -sf ${policiesFile} $out/alerting/policies.yaml
+ ln -sf ${templatesFile} $out/alerting/templates.yaml
+ ln -sf ${muteTimingsFile} $out/alerting/muteTimings.yaml
'';
# Get a submodule without any embedded metadata:
@@ -105,6 +62,8 @@ let
# http://docs.grafana.org/administration/provisioning/#datasources
grafanaTypes.datasourceConfig = types.submodule {
+ freeformType = provisioningSettingsFormat.type;
+
options = {
name = mkOption {
type = types.str;
@@ -119,11 +78,6 @@ let
default = "proxy";
description = lib.mdDoc "Access mode. proxy or direct (Server or Browser in the UI). Required.";
};
- orgId = mkOption {
- type = types.int;
- default = 1;
- description = lib.mdDoc "Org id. will default to orgId 1 if not specified.";
- };
uid = mkOption {
type = types.nullOr types.str;
default = null;
@@ -131,114 +85,68 @@ let
};
url = mkOption {
type = types.str;
+ default = "localhost";
description = lib.mdDoc "Url of the datasource.";
};
- password = mkOption {
- type = types.nullOr types.str;
- default = null;
- description = lib.mdDoc "Database password, if used.";
- };
- user = mkOption {
- type = types.nullOr types.str;
- default = null;
- description = lib.mdDoc "Database user, if used.";
- };
- database = mkOption {
- type = types.nullOr types.str;
- default = null;
- description = lib.mdDoc "Database name, if used.";
- };
- basicAuth = mkOption {
- type = types.nullOr types.bool;
- default = null;
- description = lib.mdDoc "Enable/disable basic auth.";
- };
- basicAuthUser = mkOption {
- type = types.nullOr types.str;
- default = null;
- description = lib.mdDoc "Basic auth username.";
- };
- basicAuthPassword = mkOption {
- type = types.nullOr types.str;
- default = null;
- description = lib.mdDoc "Basic auth password.";
- };
- withCredentials = mkOption {
- type = types.bool;
- default = false;
- description = lib.mdDoc "Enable/disable with credentials headers.";
- };
- isDefault = mkOption {
- type = types.bool;
- default = false;
- description = lib.mdDoc "Mark as default datasource. Max one per org.";
- };
- jsonData = mkOption {
- type = types.nullOr types.attrs;
- default = null;
- description = lib.mdDoc "Datasource specific configuration.";
- };
- secureJsonData = mkOption {
- type = types.nullOr types.attrs;
- default = null;
- description = lib.mdDoc "Datasource specific secure configuration.";
- };
- version = mkOption {
- type = types.int;
- default = 1;
- description = lib.mdDoc "Version.";
- };
editable = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc "Allow users to edit datasources from the UI.";
};
+ password = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = lib.mdDoc ''
+ Database password, if used. Please note that the contents of this option
+ will end up in a world-readable Nix store. Use the file provider
+ pointing at a reasonably secured file in the local filesystem
+ to work around that. Look at the documentation for details:
+
+ '';
+ };
+ basicAuthPassword = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = lib.mdDoc ''
+ Basic auth password. Please note that the contents of this option
+ will end up in a world-readable Nix store. Use the file provider
+ pointing at a reasonably secured file in the local filesystem
+ to work around that. Look at the documentation for details:
+
+ '';
+ };
+ secureJsonData = mkOption {
+ type = types.nullOr types.attrs;
+ default = null;
+ description = lib.mdDoc ''
+ Datasource specific secure configuration. Please note that the contents of this option
+ will end up in a world-readable Nix store. Use the file provider
+ pointing at a reasonably secured file in the local filesystem
+ to work around that. Look at the documentation for details:
+
+ '';
+ };
};
};
# http://docs.grafana.org/administration/provisioning/#dashboards
grafanaTypes.dashboardConfig = types.submodule {
+ freeformType = provisioningSettingsFormat.type;
+
options = {
name = mkOption {
type = types.str;
default = "default";
- description = lib.mdDoc "Provider name.";
- };
- orgId = mkOption {
- type = types.int;
- default = 1;
- description = lib.mdDoc "Organization ID.";
- };
- folder = mkOption {
- type = types.str;
- default = "";
- description = lib.mdDoc "Add dashboards to the specified folder.";
+ description = lib.mdDoc "A unique provider name.";
};
type = mkOption {
type = types.str;
default = "file";
description = lib.mdDoc "Dashboard provider type.";
};
- disableDeletion = mkOption {
- type = types.bool;
- default = false;
- description = lib.mdDoc "Disable deletion when JSON file is removed.";
- };
- updateIntervalSeconds = mkOption {
- type = types.int;
- default = 10;
- description = lib.mdDoc "How often Grafana will scan for changed dashboards.";
- };
- options = {
- path = mkOption {
- type = types.path;
- description = lib.mdDoc "Path grafana will watch for dashboards.";
- };
- foldersFromFilesStructure = mkOption {
- type = types.bool;
- default = false;
- description = lib.mdDoc "Use folder names from filesystem to create folders in Grafana.";
- };
+ options.path = mkOption {
+ type = types.path;
+ description = lib.mdDoc "Path grafana will watch for dashboards. Required when using the 'file' type.";
};
};
};
@@ -296,76 +204,85 @@ let
secure_settings = mkOption {
type = types.nullOr types.attrs;
default = null;
- description = lib.mdDoc "Secure settings for the notifier type.";
+ description = lib.mdDoc ''
+ Secure settings for the notifier type. Please note that the contents of this option
+ will end up in a world-readable Nix store. Use the file provider
+ pointing at a reasonably secured file in the local filesystem
+ to work around that. Look at the documentation for details:
+
+ '';
};
};
};
in {
+ imports = [
+ (mkRenamedOptionModule [ "services" "grafana" "protocol" ] [ "services" "grafana" "settings" "server" "protocol" ])
+ (mkRenamedOptionModule [ "services" "grafana" "addr" ] [ "services" "grafana" "settings" "server" "http_addr" ])
+ (mkRenamedOptionModule [ "services" "grafana" "port" ] [ "services" "grafana" "settings" "server" "http_port" ])
+ (mkRenamedOptionModule [ "services" "grafana" "domain" ] [ "services" "grafana" "settings" "server" "domain" ])
+ (mkRenamedOptionModule [ "services" "grafana" "rootUrl" ] [ "services" "grafana" "settings" "server" "root_url" ])
+ (mkRenamedOptionModule [ "services" "grafana" "staticRootPath" ] [ "services" "grafana" "settings" "server" "static_root_path" ])
+ (mkRenamedOptionModule [ "services" "grafana" "certFile" ] [ "services" "grafana" "settings" "server" "cert_file" ])
+ (mkRenamedOptionModule [ "services" "grafana" "certKey" ] [ "services" "grafana" "settings" "server" "cert_key" ])
+ (mkRenamedOptionModule [ "services" "grafana" "socket" ] [ "services" "grafana" "settings" "server" "socket" ])
+ (mkRenamedOptionModule [ "services" "grafana" "database" "type" ] [ "services" "grafana" "settings" "database" "type" ])
+ (mkRenamedOptionModule [ "services" "grafana" "database" "host" ] [ "services" "grafana" "settings" "database" "host" ])
+ (mkRenamedOptionModule [ "services" "grafana" "database" "name" ] [ "services" "grafana" "settings" "database" "name" ])
+ (mkRenamedOptionModule [ "services" "grafana" "database" "user" ] [ "services" "grafana" "settings" "database" "user" ])
+ (mkRenamedOptionModule [ "services" "grafana" "database" "password" ] [ "services" "grafana" "settings" "database" "password" ])
+ (mkRenamedOptionModule [ "services" "grafana" "database" "path" ] [ "services" "grafana" "settings" "database" "path" ])
+ (mkRenamedOptionModule [ "services" "grafana" "database" "connMaxLifetime" ] [ "services" "grafana" "settings" "database" "conn_max_lifetime" ])
+ (mkRenamedOptionModule [ "services" "grafana" "security" "adminUser" ] [ "services" "grafana" "settings" "security" "admin_user" ])
+ (mkRenamedOptionModule [ "services" "grafana" "security" "adminPassword" ] [ "services" "grafana" "settings" "security" "admin_password" ])
+ (mkRenamedOptionModule [ "services" "grafana" "security" "secretKey" ] [ "services" "grafana" "settings" "security" "secret_key" ])
+ (mkRenamedOptionModule [ "services" "grafana" "server" "serveFromSubPath" ] [ "services" "grafana" "settings" "server" "serve_from_sub_path" ])
+ (mkRenamedOptionModule [ "services" "grafana" "smtp" "enable" ] [ "services" "grafana" "settings" "smtp" "enabled" ])
+ (mkRenamedOptionModule [ "services" "grafana" "smtp" "user" ] [ "services" "grafana" "settings" "smtp" "user" ])
+ (mkRenamedOptionModule [ "services" "grafana" "smtp" "password" ] [ "services" "grafana" "settings" "smtp" "password" ])
+ (mkRenamedOptionModule [ "services" "grafana" "smtp" "fromAddress" ] [ "services" "grafana" "settings" "smtp" "from_address" ])
+ (mkRenamedOptionModule [ "services" "grafana" "users" "allowSignUp" ] [ "services" "grafana" "settings" "users" "allow_sign_up" ])
+ (mkRenamedOptionModule [ "services" "grafana" "users" "allowOrgCreate" ] [ "services" "grafana" "settings" "users" "allow_org_create" ])
+ (mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrg" ] [ "services" "grafana" "settings" "users" "auto_assign_org" ])
+ (mkRenamedOptionModule [ "services" "grafana" "users" "autoAssignOrgRole" ] [ "services" "grafana" "settings" "users" "auto_assign_org_role" ])
+ (mkRenamedOptionModule [ "services" "grafana" "auth" "disableLoginForm" ] [ "services" "grafana" "settings" "auth" "disable_login_form" ])
+ (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "enable" ] [ "services" "grafana" "settings" "auth" "anonymous" "enable" ])
+ (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_name" ] [ "services" "grafana" "settings" "auth" "anonymous" "org_name" ])
+ (mkRenamedOptionModule [ "services" "grafana" "auth" "anonymous" "org_role" ] [ "services" "grafana" "settings" "auth" "anonymous" "org_role" ])
+ (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "enable" ] [ "services" "grafana" "settings" "auth" "azuread" "enable" ])
+ (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowSignUp" ] [ "services" "grafana" "settings" "auth" "azuread" "allow_sign_up" ])
+ (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "clientId" ] [ "services" "grafana" "settings" "auth" "azuread" "client_id" ])
+ (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedDomains" ] [ "services" "grafana" "settings" "auth" "azuread" "allowed_domains" ])
+ (mkRenamedOptionModule [ "services" "grafana" "auth" "azuread" "allowedGroups" ] [ "services" "grafana" "settings" "auth" "azuread" "allowed_groups" ])
+ (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "enable" ] [ "services" "grafana" "settings" "auth" "google" "enable" ])
+ (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "allowSignUp" ] [ "services" "grafana" "settings" "auth" "google" "allow_sign_up" ])
+ (mkRenamedOptionModule [ "services" "grafana" "auth" "google" "clientId" ] [ "services" "grafana" "settings" "auth" "google" "client_id" ])
+ (mkRenamedOptionModule [ "services" "grafana" "analytics" "reporting" "enable" ] [ "services" "grafana" "settings" "analytics" "reporting_enabled" ])
+
+ (mkRemovedOptionModule [ "services" "grafana" "database" "passwordFile" ] ''
+ This option has been removed. Use 'services.grafana.settings.database.password' with file provider instead.
+ '')
+ (mkRemovedOptionModule [ "services" "grafana" "security" "adminPasswordFile" ] ''
+ This option has been removed. Use 'services.grafana.settings.security.admin_password' with file provider instead.
+ '')
+ (mkRemovedOptionModule [ "services" "grafana" "security" "secretKeyFile" ] ''
+ This option has been removed. Use 'services.grafana.settings.security.secret_key' with file provider instead.
+ '')
+ (mkRemovedOptionModule [ "services" "grafana" "smtp" "passwordFile" ] ''
+ This option has been removed. Use 'services.grafana.settings.smtp.password' with file provider instead.
+ '')
+ (mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "clientSecretFile" ] ''
+ This option has been removed. Use 'services.grafana.settings.azuread.client_secret' with file provider instead.
+ '')
+ (mkRemovedOptionModule [ "services" "grafana" "auth" "google" "clientSecretFile" ] ''
+ This option has been removed. Use 'services.grafana.settings.google.client_secret' with file provider instead.
+ '')
+
+ (mkRemovedOptionModule [ "services" "grafana" "auth" "azuread" "tenantId" ] "This option has been deprecated upstream.")
+ ];
+
options.services.grafana = {
enable = mkEnableOption (lib.mdDoc "grafana");
- protocol = mkOption {
- description = lib.mdDoc "Which protocol to listen.";
- default = "http";
- type = types.enum ["http" "https" "socket"];
- };
-
- addr = mkOption {
- description = lib.mdDoc "Listening address.";
- default = "127.0.0.1";
- type = types.str;
- };
-
- port = mkOption {
- description = lib.mdDoc "Listening port.";
- default = 3000;
- type = types.port;
- };
-
- socket = mkOption {
- description = lib.mdDoc "Listening socket.";
- default = "/run/grafana/grafana.sock";
- type = types.str;
- };
-
- domain = mkOption {
- description = lib.mdDoc "The public facing domain name used to access grafana from a browser.";
- default = "localhost";
- type = types.str;
- };
-
- rootUrl = mkOption {
- description = lib.mdDoc "Full public facing url.";
- default = "%(protocol)s://%(domain)s:%(http_port)s/";
- type = types.str;
- };
-
- certFile = mkOption {
- description = lib.mdDoc "Cert file for ssl.";
- default = "";
- type = types.str;
- };
-
- certKey = mkOption {
- description = lib.mdDoc "Cert key for ssl.";
- default = "";
- type = types.str;
- };
-
- staticRootPath = mkOption {
- description = lib.mdDoc "Root path for static assets.";
- default = "${cfg.package}/share/grafana/public";
- defaultText = literalExpression ''"''${package}/share/grafana/public"'';
- type = types.str;
- };
-
- package = mkOption {
- description = lib.mdDoc "Package to use.";
- default = pkgs.grafana;
- defaultText = literalExpression "pkgs.grafana";
- type = types.package;
- };
-
declarativePlugins = mkOption {
type = with types; nullOr (listOf path);
default = null;
@@ -377,396 +294,958 @@ in {
apply = x: if isList x then lib.unique x else x;
};
+ package = mkOption {
+ description = lib.mdDoc "Package to use.";
+ default = pkgs.grafana;
+ defaultText = literalExpression "pkgs.grafana";
+ type = types.package;
+ };
+
dataDir = mkOption {
description = lib.mdDoc "Data directory.";
default = "/var/lib/grafana";
type = types.path;
};
- database = {
- type = mkOption {
- description = lib.mdDoc "Database type.";
- default = "sqlite3";
- type = types.enum ["mysql" "sqlite3" "postgres"];
- };
+ settings = mkOption {
+ description = lib.mdDoc ''
+ Grafana settings. See
+ for available options. INI format is used.
+ '';
+ type = types.submodule {
+ freeformType = settingsFormatIni.type;
- host = mkOption {
- description = lib.mdDoc "Database host.";
- default = "127.0.0.1:3306";
- type = types.str;
- };
+ options = {
+ paths = {
+ plugins = mkOption {
+ description = lib.mdDoc "Directory where grafana will automatically scan and look for plugins";
+ default = if (cfg.declarativePlugins == null) then "${cfg.dataDir}/plugins" else declarativePlugins;
+ defaultText = literalExpression "if (cfg.declarativePlugins == null) then \"\${cfg.dataDir}/plugins\" else declarativePlugins";
+ type = types.path;
+ };
- name = mkOption {
- description = lib.mdDoc "Database name.";
- default = "grafana";
- type = types.str;
- };
+ provisioning = mkOption {
+ description = lib.mdDoc ''
+ Folder that contains provisioning config files that grafana will apply on startup and while running.
+ Don't change the value of this option if you are planning to use `services.grafana.provision` options.
+ '';
+ default = provisionConfDir;
+ defaultText = literalExpression ''
+ pkgs.runCommand "grafana-provisioning" { } \'\'
+ mkdir -p $out/{datasources,dashboards,notifiers,alerting}
+ ln -sf ''${datasourceFile} $out/datasources/datasource.yaml
+ ln -sf ''${dashboardFile} $out/dashboards/dashboard.yaml
+ ln -sf ''${notifierFile} $out/notifiers/notifier.yaml
+ ln -sf ''${rulesFile} $out/alerting/rules.yaml
+ ln -sf ''${contactPointsFile} $out/alerting/contactPoints.yaml
+ ln -sf ''${policiesFile} $out/alerting/policies.yaml
+ ln -sf ''${templatesFile} $out/alerting/templates.yaml
+ ln -sf ''${muteTimingsFile} $out/alerting/muteTimings.yaml
+ \'\'
+ '';
+ type = types.path;
+ };
+ };
- user = mkOption {
- description = lib.mdDoc "Database user.";
- default = "root";
- type = types.str;
- };
+ server = {
+ protocol = mkOption {
+ description = lib.mdDoc "Which protocol to listen.";
+ default = "http";
+ type = types.enum ["http" "https" "socket"];
+ };
- password = mkOption {
- description = lib.mdDoc ''
- Database password.
- This option is mutual exclusive with the passwordFile option.
- '';
- default = "";
- type = types.str;
- };
+ http_addr = mkOption {
+ description = lib.mdDoc "Listening address.";
+ default = "";
+ type = types.str;
+ };
- passwordFile = mkOption {
- description = lib.mdDoc ''
- File that containts the database password.
- This option is mutual exclusive with the password option.
- '';
- default = null;
- type = types.nullOr types.path;
- };
+ http_port = mkOption {
+ description = lib.mdDoc "Listening port.";
+ default = 3000;
+ type = types.port;
+ };
- path = mkOption {
- description = lib.mdDoc "Database path.";
- default = "${cfg.dataDir}/data/grafana.db";
- defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"'';
- type = types.path;
- };
+ domain = mkOption {
+ description = lib.mdDoc "The public facing domain name used to access grafana from a browser.";
+ default = "localhost";
+ type = types.str;
+ };
- connMaxLifetime = mkOption {
- description = lib.mdDoc ''
- Sets the maximum amount of time (in seconds) a connection may be reused.
- For MySQL this setting should be shorter than the `wait_timeout' variable.
- '';
- default = "unlimited";
- example = 14400;
- type = types.either types.int (types.enum [ "unlimited" ]);
+ root_url = mkOption {
+ description = lib.mdDoc "Full public facing url.";
+ default = "%(protocol)s://%(domain)s:%(http_port)s/";
+ type = types.str;
+ };
+
+ static_root_path = mkOption {
+ description = lib.mdDoc "Root path for static assets.";
+ default = "${cfg.package}/share/grafana/public";
+ defaultText = literalExpression ''"''${package}/share/grafana/public"'';
+ type = types.str;
+ };
+
+ enable_gzip = mkOption {
+ description = lib.mdDoc ''
+ Set this option to true to enable HTTP compression, this can improve transfer speed and bandwidth utilization.
+ It is recommended that most users set it to true. By default it is set to false for compatibility reasons.
+ '';
+ default = false;
+ type = types.bool;
+ };
+
+ cert_file = mkOption {
+ description = lib.mdDoc "Cert file for ssl.";
+ default = "";
+ type = types.str;
+ };
+
+ cert_key = mkOption {
+ description = lib.mdDoc "Cert key for ssl.";
+ default = "";
+ type = types.str;
+ };
+
+ socket = mkOption {
+ 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.";
+ default = "";
+ type = types.str;
+ };
+ };
+
+ database = {
+ type = mkOption {
+ description = lib.mdDoc "Database type.";
+ default = "sqlite3";
+ type = types.enum ["mysql" "sqlite3" "postgres"];
+ };
+
+ host = mkOption {
+ description = lib.mdDoc "Database host.";
+ default = "127.0.0.1:3306";
+ type = types.str;
+ };
+
+ name = mkOption {
+ description = lib.mdDoc "Database name.";
+ default = "grafana";
+ type = types.str;
+ };
+
+ user = mkOption {
+ description = lib.mdDoc "Database user.";
+ default = "root";
+ type = types.str;
+ };
+
+ password = mkOption {
+ description = lib.mdDoc ''
+ Database password. Please note that the contents of this option
+ will end up in a world-readable Nix store. Use the file provider
+ pointing at a reasonably secured file in the local filesystem
+ to work around that. Look at the documentation for details:
+
+ '';
+ default = "";
+ type = types.str;
+ };
+
+ path = mkOption {
+ description = lib.mdDoc "Only applicable to sqlite3 database. The file path where the database will be stored.";
+ default = "${cfg.dataDir}/data/grafana.db";
+ defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"'';
+ type = types.path;
+ };
+ };
+
+ security = {
+ admin_user = mkOption {
+ description = lib.mdDoc "Default admin username.";
+ default = "admin";
+ type = types.str;
+ };
+
+ admin_password = mkOption {
+ description = lib.mdDoc ''
+ Default admin password. Please note that the contents of this option
+ will end up in a world-readable Nix store. Use the file provider
+ pointing at a reasonably secured file in the local filesystem
+ to work around that. Look at the documentation for details:
+
+ '';
+ default = "admin";
+ type = types.str;
+ };
+
+ secret_key = mkOption {
+ description = lib.mdDoc ''
+ Secret key used for signing. Please note that the contents of this option
+ will end up in a world-readable Nix store. Use the file provider
+ pointing at a reasonably secured file in the local filesystem
+ to work around that. Look at the documentation for details:
+
+ '';
+ default = "SW2YcwTIb9zpOOhoPsMm";
+ type = types.str;
+ };
+ };
+
+ smtp = {
+ enabled = mkOption {
+ description = lib.mdDoc "Whether to enable SMTP.";
+ default = false;
+ type = types.bool;
+ };
+ host = mkOption {
+ description = lib.mdDoc "Host to connect to.";
+ default = "localhost:25";
+ type = types.str;
+ };
+ user = mkOption {
+ description = lib.mdDoc "User used for authentication.";
+ default = "";
+ type = types.str;
+ };
+ password = mkOption {
+ description = lib.mdDoc ''
+ Password used for authentication. Please note that the contents of this option
+ will end up in a world-readable Nix store. Use the file provider
+ pointing at a reasonably secured file in the local filesystem
+ to work around that. Look at the documentation for details:
+
+ '';
+ default = "";
+ type = types.str;
+ };
+ from_address = mkOption {
+ description = lib.mdDoc "Email address used for sending.";
+ default = "admin@grafana.localhost";
+ type = types.str;
+ };
+ };
+
+ users = {
+ allow_sign_up = mkOption {
+ description = lib.mdDoc "Disable user signup / registration.";
+ default = false;
+ type = types.bool;
+ };
+
+ allow_org_create = mkOption {
+ description = lib.mdDoc "Whether user is allowed to create organizations.";
+ default = false;
+ type = types.bool;
+ };
+
+ auto_assign_org = mkOption {
+ description = lib.mdDoc "Whether to automatically assign new users to default org.";
+ default = true;
+ type = types.bool;
+ };
+
+ auto_assign_org_role = mkOption {
+ description = lib.mdDoc "Default role new users will be auto assigned.";
+ default = "Viewer";
+ type = types.enum ["Viewer" "Editor"];
+ };
+ };
+
+ analytics.reporting_enabled = mkOption {
+ description = lib.mdDoc "Whether to allow anonymous usage reporting to stats.grafana.net.";
+ default = true;
+ type = types.bool;
+ };
+ };
};
};
provision = {
enable = mkEnableOption (lib.mdDoc "provision");
+
datasources = mkOption {
- description = lib.mdDoc "Grafana datasources configuration.";
+ description = lib.mdDoc ''
+ Deprecated option for Grafana datasource configuration. Use either
+ `services.grafana.provision.datasources.settings` or
+ `services.grafana.provision.datasources.path` instead.
+ '';
default = [];
- type = types.listOf grafanaTypes.datasourceConfig;
- apply = x: map _filter x;
+ apply = x: if (builtins.isList x) then map _filter x else x;
+ type = with types; either (listOf grafanaTypes.datasourceConfig) (submodule {
+ options.settings = mkOption {
+ description = lib.mdDoc ''
+ Grafana datasource configuration in Nix. Can't be used with
+ `services.grafana.provision.datasources.path` simultaneously. See
+
+ for supported options.
+ '';
+ default = null;
+ type = types.nullOr (types.submodule {
+ options = {
+ apiVersion = mkOption {
+ description = lib.mdDoc "Config file version.";
+ default = 1;
+ type = types.int;
+ };
+
+ datasources = mkOption {
+ description = lib.mdDoc "List of datasources to insert/update.";
+ default = [];
+ type = types.listOf grafanaTypes.datasourceConfig;
+ };
+
+ deleteDatasources = mkOption {
+ description = lib.mdDoc "List of datasources that should be deleted from the database.";
+ default = [];
+ type = types.listOf (types.submodule {
+ options.name = mkOption {
+ description = lib.mdDoc "Name of the datasource to delete.";
+ type = types.str;
+ };
+
+ options.orgId = mkOption {
+ description = lib.mdDoc "Organization ID of the datasource to delete.";
+ type = types.int;
+ };
+ });
+ };
+ };
+ });
+ example = literalExpression ''
+ {
+ apiVersion = 1;
+
+ datasources = [{
+ name = "Graphite";
+ type = "graphite";
+ }];
+
+ deleteDatasources = [{
+ name = "Graphite";
+ orgId = 1;
+ }];
+ }
+ '';
+ };
+
+ options.path = mkOption {
+ description = lib.mdDoc ''
+ Path to YAML datasource configuration. Can't be used with
+ `services.grafana.provision.datasources.settings` simultaneously.
+ '';
+ default = null;
+ type = types.nullOr types.path;
+ };
+ });
};
+
+
dashboards = mkOption {
- description = lib.mdDoc "Grafana dashboard configuration.";
+ description = lib.mdDoc ''
+ Deprecated option for Grafana dashboard configuration. Use either
+ `services.grafana.provision.dashboards.settings` or
+ `services.grafana.provision.dashboards.path` instead.
+ '';
default = [];
- type = types.listOf grafanaTypes.dashboardConfig;
- apply = x: map _filter x;
+ apply = x: if (builtins.isList x) then map _filter x else x;
+ type = with types; either (listOf grafanaTypes.dashboardConfig) (submodule {
+ options.settings = mkOption {
+ description = lib.mdDoc ''
+ Grafana dashboard configuration in Nix. Can't be used with
+ `services.grafana.provision.dashboards.path` simultaneously. See
+
+ for supported options.
+ '';
+ default = null;
+ type = types.nullOr (types.submodule {
+ options.apiVersion = mkOption {
+ description = lib.mdDoc "Config file version.";
+ default = 1;
+ type = types.int;
+ };
+
+ options.providers = mkOption {
+ description = lib.mdDoc "List of dashboards to insert/update.";
+ default = [];
+ type = types.listOf grafanaTypes.dashboardConfig;
+ };
+ });
+ example = literalExpression ''
+ {
+ apiVersion = 1;
+
+ providers = [{
+ name = "default";
+ options.path = "/var/lib/grafana/dashboards";
+ }];
+ }
+ '';
+ };
+
+ options.path = mkOption {
+ description = lib.mdDoc ''
+ Path to YAML dashboard configuration. Can't be used with
+ `services.grafana.provision.dashboards.settings` simultaneously.
+ '';
+ default = null;
+ type = types.nullOr types.path;
+ };
+ });
};
+
+
notifiers = mkOption {
description = lib.mdDoc "Grafana notifier configuration.";
default = [];
type = types.listOf grafanaTypes.notifierConfig;
apply = x: map _filter x;
};
- };
- security = {
- adminUser = mkOption {
- description = lib.mdDoc "Default admin username.";
- default = "admin";
- type = types.str;
- };
- adminPassword = mkOption {
- description = lib.mdDoc ''
- Default admin password.
- This option is mutual exclusive with the adminPasswordFile option.
- '';
- default = "admin";
- type = types.str;
- };
-
- adminPasswordFile = mkOption {
- description = lib.mdDoc ''
- Default admin password.
- This option is mutual exclusive with the `adminPassword` option.
- '';
- default = null;
- type = types.nullOr types.path;
- };
-
- secretKey = mkOption {
- description = lib.mdDoc "Secret key used for signing.";
- default = "SW2YcwTIb9zpOOhoPsMm";
- type = types.str;
- };
-
- secretKeyFile = mkOption {
- description = lib.mdDoc "Secret key used for signing.";
- default = null;
- type = types.nullOr types.path;
- };
- };
-
- server = {
- serveFromSubPath = mkOption {
- description = lib.mdDoc "Serve Grafana from subpath specified in rootUrl setting";
- default = false;
- type = types.bool;
- };
- };
-
- smtp = {
- enable = mkEnableOption (lib.mdDoc "smtp");
- host = mkOption {
- description = lib.mdDoc "Host to connect to.";
- default = "localhost:25";
- type = types.str;
- };
- user = mkOption {
- description = lib.mdDoc "User used for authentication.";
- default = "";
- type = types.str;
- };
- password = mkOption {
- description = lib.mdDoc ''
- Password used for authentication.
- This option is mutual exclusive with the passwordFile option.
- '';
- default = "";
- type = types.str;
- };
- passwordFile = mkOption {
- description = lib.mdDoc ''
- Password used for authentication.
- This option is mutual exclusive with the password option.
- '';
- default = null;
- type = types.nullOr types.path;
- };
- fromAddress = mkOption {
- description = lib.mdDoc "Email address used for sending.";
- default = "admin@grafana.localhost";
- type = types.str;
- };
- };
-
- users = {
- allowSignUp = mkOption {
- description = lib.mdDoc "Disable user signup / registration.";
- default = false;
- type = types.bool;
- };
-
- allowOrgCreate = mkOption {
- description = lib.mdDoc "Whether user is allowed to create organizations.";
- default = false;
- type = types.bool;
- };
-
- autoAssignOrg = mkOption {
- description = lib.mdDoc "Whether to automatically assign new users to default org.";
- default = true;
- type = types.bool;
- };
-
- autoAssignOrgRole = mkOption {
- description = lib.mdDoc "Default role new users will be auto assigned.";
- default = "Viewer";
- type = types.enum ["Viewer" "Editor"];
- };
- };
-
- auth = {
- disableLoginForm = mkOption {
- description = lib.mdDoc "Set to true to disable (hide) the login form, useful if you use OAuth";
- default = false;
- type = types.bool;
- };
-
- anonymous = {
- enable = mkOption {
- description = lib.mdDoc "Whether to allow anonymous access.";
- default = false;
- type = types.bool;
- };
- org_name = mkOption {
- description = lib.mdDoc "Which organization to allow anonymous access to.";
- default = "Main Org.";
- type = types.str;
- };
- org_role = mkOption {
- description = lib.mdDoc "Which role anonymous users have in the organization.";
- default = "Viewer";
- type = types.str;
- };
- };
- azuread = {
- enable = mkOption {
- description = lib.mdDoc "Whether to allow Azure AD OAuth.";
- default = false;
- type = types.bool;
- };
- allowSignUp = mkOption {
- description = lib.mdDoc "Whether to allow sign up with Azure AD OAuth.";
- default = false;
- type = types.bool;
- };
- clientId = mkOption {
- description = lib.mdDoc "Azure AD OAuth client ID.";
- default = "";
- type = types.str;
- };
- clientSecretFile = mkOption {
- description = lib.mdDoc "Azure AD OAuth client secret.";
- default = null;
- type = types.nullOr types.path;
- };
- tenantId = mkOption {
- description = lib.mdDoc ''
- Tenant id used to create auth and token url. Default to "common"
- , let user sign in with any tenant.
+ alerting = {
+ rules = {
+ path = mkOption {
+ description = lib.mdDoc ''
+ Path to YAML rules configuration. Can't be used with
+ `services.grafana.provision.alerting.rules.settings` simultaneously.
'';
- default = "common";
- type = types.str;
- };
- allowedDomains = mkOption {
- description = lib.mdDoc ''
- Limits access to users who belong to specific domains.
- Separate domains with space or comma.
- '';
- default = "";
- type = types.str;
- };
- allowedGroups = mkOption {
- description = lib.mdDoc ''
- To limit access to authenticated users who are members of one or more groups,
- set allowedGroups to a comma- or space-separated list of group object IDs.
- You can find object IDs for a specific group on the Azure portal.
+ default = null;
+ type = types.nullOr types.path;
+ };
+
+ settings = mkOption {
+ description = lib.mdDoc ''
+ Grafana rules configuration in Nix. Can't be used with
+ `services.grafana.provision.alerting.rules.path` simultaneously. See
+
+ for supported options.
'';
- default = "";
- type = types.str;
- };
- };
- google = {
- enable = mkOption {
- description = lib.mdDoc "Whether to allow Google OAuth2.";
- default = false;
- type = types.bool;
- };
- allowSignUp = mkOption {
- description = lib.mdDoc "Whether to allow sign up with Google OAuth2.";
- default = false;
- type = types.bool;
- };
- clientId = mkOption {
- description = lib.mdDoc "Google OAuth2 client ID.";
- default = "";
- type = types.str;
- };
- clientSecretFile = mkOption {
- description = lib.mdDoc "Google OAuth2 client secret.";
- default = null;
- type = types.nullOr types.path;
- };
- };
- };
+ default = null;
+ type = types.nullOr (types.submodule {
+ options = {
+ apiVersion = mkOption {
+ description = lib.mdDoc "Config file version.";
+ default = 1;
+ type = types.int;
+ };
- analytics.reporting = {
- enable = mkOption {
- description = lib.mdDoc "Whether to allow anonymous usage reporting to stats.grafana.net.";
- default = true;
- type = types.bool;
- };
- };
+ groups = mkOption {
+ description = lib.mdDoc "List of rule groups to import or update.";
+ default = [];
+ type = types.listOf (types.submodule {
+ freeformType = provisioningSettingsFormat.type;
- extraOptions = mkOption {
- description = lib.mdDoc ''
- Extra configuration options passed as env variables as specified in
- [documentation](http://docs.grafana.org/installation/configuration/),
- but without GF_ prefix
- '';
- default = {};
- type = with types; attrsOf (either str path);
+ options.name = mkOption {
+ description = lib.mdDoc "Name of the rule group. Required.";
+ type = types.str;
+ };
+
+ options.folder = mkOption {
+ description = lib.mdDoc "Name of the folder the rule group will be stored in. Required.";
+ type = types.str;
+ };
+
+ options.interval = mkOption {
+ description = lib.mdDoc "Interval that the rule group should be evaluated at. Required.";
+ type = types.str;
+ };
+ });
+ };
+
+ deleteRules = mkOption {
+ description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
+ default = [];
+ type = types.listOf (types.submodule {
+ options.orgId = mkOption {
+ description = lib.mdDoc "Organization ID, default = 1";
+ default = 1;
+ type = types.int;
+ };
+
+ options.uid = mkOption {
+ description = lib.mdDoc "Unique identifier for the rule. Required.";
+ type = types.str;
+ };
+ });
+ };
+ };
+ });
+ example = literalExpression ''
+ {
+ apiVersion = 1;
+
+ groups = [{
+ orgId = 1;
+ name = "my_rule_group";
+ folder = "my_first_folder";
+ interval = "60s";
+ rules = [{
+ uid = "my_id_1";
+ title = "my_first_rule";
+ condition = "A";
+ data = [{
+ refId = "A";
+ datasourceUid = "-100";
+ model = {
+ conditions = [{
+ evaluator = {
+ params = [ 3 ];
+ type = "git";
+ };
+ operator.type = "and";
+ query.params = [ "A" ];
+ reducer.type = "last";
+ type = "query";
+ }];
+ datasource = {
+ type = "__expr__";
+ uid = "-100";
+ };
+ expression = "1==0";
+ intervalMs = 1000;
+ maxDataPoints = 43200;
+ refId = "A";
+ type = "math";
+ };
+ }];
+ dashboardUid = "my_dashboard";
+ panelId = 123;
+ noDataState = "Alerting";
+ for = "60s";
+ annotations.some_key = "some_value";
+ labels.team = "sre_team1";
+ }];
+ }];
+
+ deleteRules = [{
+ orgId = 1;
+ uid = "my_id_1";
+ }];
+ }
+ '';
+ };
+ };
+
+ contactPoints = {
+ path = mkOption {
+ description = lib.mdDoc ''
+ Path to YAML contact points configuration. Can't be used with
+ `services.grafana.provision.alerting.contactPoints.settings` simultaneously.
+ '';
+ default = null;
+ type = types.nullOr types.path;
+ };
+
+ settings = mkOption {
+ description = lib.mdDoc ''
+ Grafana contact points configuration in Nix. Can't be used with
+ `services.grafana.provision.alerting.contactPoints.path` simultaneously. See
+
+ for supported options.
+ '';
+ default = null;
+ type = types.nullOr (types.submodule {
+ options = {
+ apiVersion = mkOption {
+ description = lib.mdDoc "Config file version.";
+ default = 1;
+ type = types.int;
+ };
+
+ contactPoints = mkOption {
+ description = lib.mdDoc "List of contact points to import or update.";
+ default = [];
+ type = types.listOf (types.submodule {
+ freeformType = provisioningSettingsFormat.type;
+
+ options.name = mkOption {
+ description = lib.mdDoc "Name of the contact point. Required.";
+ type = types.str;
+ };
+ });
+ };
+
+ deleteContactPoints = mkOption {
+ description = lib.mdDoc "List of receivers that should be deleted.";
+ default = [];
+ type = types.listOf (types.submodule {
+ options.orgId = mkOption {
+ description = lib.mdDoc "Organization ID, default = 1.";
+ default = 1;
+ type = types.int;
+ };
+
+ options.uid = mkOption {
+ description = lib.mdDoc "Unique identifier for the receiver. Required.";
+ type = types.str;
+ };
+ });
+ };
+ };
+ });
+ example = literalExpression ''
+ {
+ apiVersion = 1;
+
+ contactPoints = [{
+ orgId = 1;
+ name = "cp_1";
+ receivers = [{
+ uid = "first_uid";
+ type = "prometheus-alertmanager";
+ settings.url = "http://test:9000";
+ }];
+ }];
+
+ deleteContactPoints = [{
+ orgId = 1;
+ uid = "first_uid";
+ }];
+ }
+ '';
+ };
+ };
+
+ policies = {
+ path = mkOption {
+ description = lib.mdDoc ''
+ Path to YAML notification policies configuration. Can't be used with
+ `services.grafana.provision.alerting.policies.settings` simultaneously.
+ '';
+ default = null;
+ type = types.nullOr types.path;
+ };
+
+ settings = mkOption {
+ description = lib.mdDoc ''
+ Grafana notification policies configuration in Nix. Can't be used with
+ `services.grafana.provision.alerting.policies.path` simultaneously. See
+
+ for supported options.
+ '';
+ default = null;
+ type = types.nullOr (types.submodule {
+ options = {
+ apiVersion = mkOption {
+ description = lib.mdDoc "Config file version.";
+ default = 1;
+ type = types.int;
+ };
+
+ policies = mkOption {
+ description = lib.mdDoc "List of contact points to import or update.";
+ default = [];
+ type = types.listOf (types.submodule {
+ freeformType = provisioningSettingsFormat.type;
+ });
+ };
+
+ resetPolicies = mkOption {
+ description = lib.mdDoc "List of orgIds that should be reset to the default policy.";
+ default = [];
+ type = types.listOf types.int;
+ };
+ };
+ });
+ example = literalExpression ''
+ {
+ apiVersion = 1;
+
+ policies = [{
+ orgId = 1;
+ receiver = "grafana-default-email";
+ group_by = [ "..." ];
+ matchers = [
+ "alertname = Watchdog"
+ "severity =~ \"warning|critical\""
+ ];
+ mute_time_intervals = [
+ "abc"
+ ];
+ group_wait = "30s";
+ group_interval = "5m";
+ repeat_interval = "4h";
+ }];
+
+ resetPolicies = [
+ 1
+ ];
+ }
+ '';
+ };
+ };
+
+ templates = {
+ path = mkOption {
+ description = lib.mdDoc ''
+ Path to YAML templates configuration. Can't be used with
+ `services.grafana.provision.alerting.templates.settings` simultaneously.
+ '';
+ default = null;
+ type = types.nullOr types.path;
+ };
+
+ settings = mkOption {
+ description = lib.mdDoc ''
+ Grafana templates configuration in Nix. Can't be used with
+ `services.grafana.provision.alerting.templates.path` simultaneously. See
+
+ for supported options.
+ '';
+ default = null;
+ type = types.nullOr (types.submodule {
+ options = {
+ apiVersion = mkOption {
+ description = lib.mdDoc "Config file version.";
+ default = 1;
+ type = types.int;
+ };
+
+ templates = mkOption {
+ description = lib.mdDoc "List of templates to import or update.";
+ default = [];
+ type = types.listOf (types.submodule {
+ freeformType = provisioningSettingsFormat.type;
+
+ options.name = mkOption {
+ description = lib.mdDoc "Name of the template, must be unique. Required.";
+ type = types.str;
+ };
+
+ options.template = mkOption {
+ description = lib.mdDoc "Alerting with a custom text template";
+ type = types.str;
+ };
+ });
+ };
+
+ deleteTemplates = mkOption {
+ description = lib.mdDoc "List of alert rule UIDs that should be deleted.";
+ default = [];
+ type = types.listOf (types.submodule {
+ options.orgId = mkOption {
+ description = lib.mdDoc "Organization ID, default = 1.";
+ default = 1;
+ type = types.int;
+ };
+
+ options.name = mkOption {
+ description = lib.mdDoc "Name of the template, must be unique. Required.";
+ type = types.str;
+ };
+ });
+ };
+ };
+ });
+ example = literalExpression ''
+ {
+ apiVersion = 1;
+
+ templates = [{
+ orgId = 1;
+ name = "my_first_template";
+ template = "Alerting with a custom text template";
+ }];
+
+ deleteTemplates = [{
+ orgId = 1;
+ name = "my_first_template";
+ }];
+ }
+ '';
+ };
+ };
+
+ muteTimings = {
+ path = mkOption {
+ description = lib.mdDoc ''
+ Path to YAML mute timings configuration. Can't be used with
+ `services.grafana.provision.alerting.muteTimings.settings` simultaneously.
+ '';
+ default = null;
+ type = types.nullOr types.path;
+ };
+
+ settings = mkOption {
+ description = lib.mdDoc ''
+ Grafana mute timings configuration in Nix. Can't be used with
+ `services.grafana.provision.alerting.muteTimings.path` simultaneously. See
+
+ for supported options.
+ '';
+ default = null;
+ type = types.nullOr (types.submodule {
+ options = {
+ apiVersion = mkOption {
+ description = lib.mdDoc "Config file version.";
+ default = 1;
+ type = types.int;
+ };
+
+ muteTimes = mkOption {
+ description = lib.mdDoc "List of mute time intervals to import or update.";
+ default = [];
+ type = types.listOf (types.submodule {
+ freeformType = provisioningSettingsFormat.type;
+
+ options.name = mkOption {
+ description = lib.mdDoc "Name of the mute time interval, must be unique. Required.";
+ type = types.str;
+ };
+ });
+ };
+
+ deleteMuteTimes = mkOption {
+ description = lib.mdDoc "List of mute time intervals that should be deleted.";
+ default = [];
+ type = types.listOf (types.submodule {
+ options.orgId = mkOption {
+ description = lib.mdDoc "Organization ID, default = 1.";
+ default = 1;
+ type = types.int;
+ };
+
+ options.name = mkOption {
+ description = lib.mdDoc "Name of the mute time interval, must be unique. Required.";
+ type = types.str;
+ };
+ });
+ };
+ };
+ });
+ example = literalExpression ''
+ {
+ apiVersion = 1;
+
+ muteTimes = [{
+ orgId = 1;
+ name = "mti_1";
+ time_intervals = [{
+ times = [{
+ start_time = "06:00";
+ end_time = "23:59";
+ }];
+ weekdays = [
+ "monday:wednesday"
+ "saturday"
+ "sunday"
+ ];
+ months = [
+ "1:3"
+ "may:august"
+ "december"
+ ];
+ years = [
+ "2020:2022"
+ "2030"
+ ];
+ days_of_month = [
+ "1:5"
+ "-3:-1"
+ ];
+ }];
+ }];
+
+ deleteMuteTimes = [{
+ orgId = 1;
+ name = "mti_1";
+ }];
+ }
+ '';
+ };
+ };
+ };
};
};
config = mkIf cfg.enable {
warnings = flatten [
(optional (
- cfg.database.password != opt.database.password.default ||
- cfg.security.adminPassword != opt.security.adminPassword.default
- ) "Grafana passwords will be stored as plaintext in the Nix store!")
+ cfg.settings.database.password != "" ||
+ cfg.settings.security.admin_password != "admin"
+ ) "Grafana passwords will be stored as plaintext in the Nix store! Use file provider instead.")
(optional (
- any (x: x.password != null || x.basicAuthPassword != null || x.secureJsonData != null) cfg.provision.datasources
- ) "Datasource passwords will be stored as plaintext in the Nix store!")
+ let
+ checkOpts = opt: any (x: x.password != null || x.basicAuthPassword != null || x.secureJsonData != null) opt;
+ datasourcesUsed = if (cfg.provision.datasources.settings == null) then [] else cfg.provision.datasources.settings.datasources;
+ in if (builtins.isList cfg.provision.datasources) then checkOpts cfg.provision.datasources else checkOpts datasourcesUsed
+ ) "Datasource passwords will be stored as plaintext in the Nix store! Use file provider instead.")
(optional (
any (x: x.secure_settings != null) cfg.provision.notifiers
- ) "Notifier secure settings will be stored as plaintext in the Nix store!")
+ ) "Notifier secure settings will be stored as plaintext in the Nix store! Use file provider instead.")
+ (optional (
+ builtins.isList cfg.provision.datasources
+ ) ''
+ Provisioning Grafana datasources with options has been deprecated.
+ Use `services.grafana.provision.datasources.settings` or
+ `services.grafana.provision.datasources.path` instead.
+ '')
+ (optional (
+ builtins.isList cfg.provision.dashboards
+ ) ''
+ Provisioning Grafana dashboards with options has been deprecated.
+ Use `services.grafana.provision.dashboards.settings` or
+ `services.grafana.provision.dashboards.path` instead.
+ '')
+ (optional (
+ cfg.provision.notifiers != []
+ ) ''
+ Notifiers are deprecated upstream and will be removed in Grafana 10.
+ Use `services.grafana.provision.alerting.contactPoints` instead.
+ '')
];
environment.systemPackages = [ cfg.package ];
assertions = [
{
- assertion = cfg.database.password != opt.database.password.default -> cfg.database.passwordFile == null;
- message = "Cannot set both password and passwordFile";
+ assertion = if (builtins.isList cfg.provision.datasources) then true else cfg.provision.datasources.settings == null || cfg.provision.datasources.path == null;
+ message = "Cannot set both datasources settings and datasources path";
}
{
- assertion = cfg.security.adminPassword != opt.security.adminPassword.default -> cfg.security.adminPasswordFile == null;
- message = "Cannot set both adminPassword and adminPasswordFile";
- }
- {
- assertion = cfg.security.secretKey != opt.security.secretKey.default -> cfg.security.secretKeyFile == null;
- message = "Cannot set both secretKey and secretKeyFile";
- }
- {
- assertion = cfg.smtp.password != opt.smtp.password.default -> cfg.smtp.passwordFile == null;
- message = "Cannot set both password and passwordFile";
- }
- {
- assertion = all
+ assertion = let
+ prometheusIsNotDirect = opt: all
({ type, access, ... }: type == "prometheus" -> access != "direct")
- cfg.provision.datasources;
+ opt;
+ in
+ if (builtins.isList cfg.provision.datasources) then prometheusIsNotDirect cfg.provision.datasources
+ else cfg.provision.datasources.settings == null || prometheusIsNotDirect cfg.provision.datasources.settings.datasources;
message = "For datasources of type `prometheus`, the `direct` access mode is not supported anymore (since Grafana 9.2.0)";
}
+ {
+ assertion = if (builtins.isList cfg.provision.dashboards) then true else cfg.provision.dashboards.settings == null || cfg.provision.dashboards.path == null;
+ message = "Cannot set both dashboards settings and dashboards path";
+ }
+ {
+ assertion = cfg.provision.alerting.rules.settings == null || cfg.provision.alerting.rules.path == null;
+ message = "Cannot set both rules settings and rules path";
+ }
+ {
+ assertion = cfg.provision.alerting.contactPoints.settings == null || cfg.provision.alerting.contactPoints.path == null;
+ message = "Cannot set both contact points settings and contact points path";
+ }
+ {
+ assertion = cfg.provision.alerting.policies.settings == null || cfg.provision.alerting.policies.path == null;
+ message = "Cannot set both policies settings and policies path";
+ }
+ {
+ assertion = cfg.provision.alerting.templates.settings == null || cfg.provision.alerting.templates.path == null;
+ message = "Cannot set both templates settings and templates path";
+ }
+ {
+ assertion = cfg.provision.alerting.muteTimings.settings == null || cfg.provision.alerting.muteTimings.path == null;
+ message = "Cannot set both mute timings settings and mute timings path";
+ }
];
systemd.services.grafana = {
description = "Grafana Service Daemon";
wantedBy = ["multi-user.target"];
after = ["networking.target"] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
- environment = {
- QT_QPA_PLATFORM = "offscreen";
- } // mapAttrs' (n: v: nameValuePair "GF_${n}" (toString v)) envOptions;
script = ''
set -o errexit -o pipefail -o nounset -o errtrace
shopt -s inherit_errexit
- ${optionalString (cfg.auth.azuread.clientSecretFile != null) ''
- GF_AUTH_AZUREAD_CLIENT_SECRET="$(<${escapeShellArg cfg.auth.azuread.clientSecretFile})"
- export GF_AUTH_AZUREAD_CLIENT_SECRET
- ''}
- ${optionalString (cfg.auth.google.clientSecretFile != null) ''
- GF_AUTH_GOOGLE_CLIENT_SECRET="$(<${escapeShellArg cfg.auth.google.clientSecretFile})"
- export GF_AUTH_GOOGLE_CLIENT_SECRET
- ''}
- ${optionalString (cfg.database.passwordFile != null) ''
- GF_DATABASE_PASSWORD="$(<${escapeShellArg cfg.database.passwordFile})"
- export GF_DATABASE_PASSWORD
- ''}
- ${optionalString (cfg.security.adminPasswordFile != null) ''
- GF_SECURITY_ADMIN_PASSWORD="$(<${escapeShellArg cfg.security.adminPasswordFile})"
- export GF_SECURITY_ADMIN_PASSWORD
- ''}
- ${optionalString (cfg.security.secretKeyFile != null) ''
- GF_SECURITY_SECRET_KEY="$(<${escapeShellArg cfg.security.secretKeyFile})"
- export GF_SECURITY_SECRET_KEY
- ''}
- ${optionalString (cfg.smtp.passwordFile != null) ''
- GF_SMTP_PASSWORD="$(<${escapeShellArg cfg.smtp.passwordFile})"
- export GF_SMTP_PASSWORD
- ''}
- ${optionalString cfg.provision.enable ''
- export GF_PATHS_PROVISIONING=${provisionConfDir};
- ''}
- exec ${cfg.package}/bin/grafana-server -homepath ${cfg.dataDir}
+ exec ${cfg.package}/bin/grafana-server -homepath ${cfg.dataDir} -config ${configFile}
'';
serviceConfig = {
WorkingDirectory = cfg.dataDir;
diff --git a/nixos/modules/services/network-filesystems/litestream/litestream.xml b/nixos/modules/services/network-filesystems/litestream/litestream.xml
index 598f9be8cf63..8f5597bb6891 100644
--- a/nixos/modules/services/network-filesystems/litestream/litestream.xml
+++ b/nixos/modules/services/network-filesystems/litestream/litestream.xml
@@ -15,7 +15,7 @@
Litestream service is managed by a dedicated user named litestream
which needs permission to the database file. Here's an example config which gives
- required permissions to access
+ required permissions to access
grafana database:
{ pkgs, ... }:
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 0662c3ab08a7..78f90761d352 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -231,7 +231,7 @@ in {
gollum = handleTest ./gollum.nix {};
google-oslogin = handleTest ./google-oslogin {};
gotify-server = handleTest ./gotify-server.nix {};
- grafana = handleTest ./grafana.nix {};
+ grafana = handleTest ./grafana {};
grafana-agent = handleTest ./grafana-agent.nix {};
graphite = handleTest ./graphite.nix {};
graylog = handleTest ./graylog.nix {};
diff --git a/nixos/tests/grafana.nix b/nixos/tests/grafana/basic.nix
similarity index 91%
rename from nixos/tests/grafana.nix
rename to nixos/tests/grafana/basic.nix
index 5364f0ca8b0b..f91d649540e6 100644
--- a/nixos/tests/grafana.nix
+++ b/nixos/tests/grafana/basic.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ lib, pkgs, ... }:
+import ../make-test-python.nix ({ lib, pkgs, ... }:
let
inherit (lib) mkMerge nameValuePair maintainers;
@@ -17,6 +17,8 @@ let
};
extraNodeConfs = {
+ sqlite = {};
+
declarativePlugins = {
services.grafana.declarativePlugins = [ pkgs.grafanaPlugins.grafana-clock-panel ];
};
@@ -52,14 +54,9 @@ let
};
};
- nodes = builtins.listToAttrs (map (dbName:
- nameValuePair dbName (mkMerge [
- baseGrafanaConf
- (extraNodeConfs.${dbName} or {})
- ])) [ "sqlite" "declarativePlugins" "postgresql" "mysql" ]);
-
+ nodes = builtins.mapAttrs (_: val: mkMerge [ val baseGrafanaConf ]) extraNodeConfs;
in {
- name = "grafana";
+ name = "grafana-basic";
meta = with maintainers; {
maintainers = [ willibutz ];
diff --git a/nixos/tests/grafana/default.nix b/nixos/tests/grafana/default.nix
new file mode 100644
index 000000000000..9c2622571800
--- /dev/null
+++ b/nixos/tests/grafana/default.nix
@@ -0,0 +1,9 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../../.. { inherit system config; }
+}:
+
+{
+ basic = import ./basic.nix { inherit system pkgs; };
+ provision = import ./provision { inherit system pkgs; };
+}
diff --git a/nixos/tests/grafana/provision/contact-points.yaml b/nixos/tests/grafana/provision/contact-points.yaml
new file mode 100644
index 000000000000..2a5f14e75e2d
--- /dev/null
+++ b/nixos/tests/grafana/provision/contact-points.yaml
@@ -0,0 +1,9 @@
+apiVersion: 1
+
+contactPoints:
+ - name: "Test Contact Point"
+ receivers:
+ - uid: "test_contact_point"
+ type: prometheus-alertmanager
+ settings:
+ url: http://localhost:9000
diff --git a/nixos/tests/grafana/provision/dashboards.yaml b/nixos/tests/grafana/provision/dashboards.yaml
new file mode 100644
index 000000000000..dc83fe6b892d
--- /dev/null
+++ b/nixos/tests/grafana/provision/dashboards.yaml
@@ -0,0 +1,6 @@
+apiVersion: 1
+
+providers:
+ - name: 'default'
+ options:
+ path: /var/lib/grafana/dashboards
diff --git a/nixos/tests/grafana/provision/datasources.yaml b/nixos/tests/grafana/provision/datasources.yaml
new file mode 100644
index 000000000000..ccf9481db7f3
--- /dev/null
+++ b/nixos/tests/grafana/provision/datasources.yaml
@@ -0,0 +1,7 @@
+apiVersion: 1
+
+datasources:
+ - name: 'Test Datasource'
+ type: 'testdata'
+ access: 'proxy'
+ uid: 'test_datasource'
diff --git a/nixos/tests/grafana/provision/default.nix b/nixos/tests/grafana/provision/default.nix
new file mode 100644
index 000000000000..331de35ed7fa
--- /dev/null
+++ b/nixos/tests/grafana/provision/default.nix
@@ -0,0 +1,223 @@
+import ../../make-test-python.nix ({ lib, pkgs, ... }:
+
+let
+ inherit (lib) mkMerge nameValuePair maintainers;
+
+ baseGrafanaConf = {
+ services.grafana = {
+ enable = true;
+ addr = "localhost";
+ analytics.reporting.enable = false;
+ domain = "localhost";
+ security = {
+ adminUser = "testadmin";
+ adminPassword = "snakeoilpwd";
+ };
+ provision.enable = true;
+ };
+
+ systemd.tmpfiles.rules = [
+ "L /var/lib/grafana/dashboards/test.json 0700 grafana grafana - ${pkgs.writeText "test.json" (builtins.readFile ./test_dashboard.json)}"
+ ];
+ };
+
+ extraNodeConfs = {
+ provisionOld = {
+ services.grafana.provision = {
+ datasources = [{
+ name = "Test Datasource";
+ type = "testdata";
+ access = "proxy";
+ uid = "test_datasource";
+ }];
+
+ dashboards = [{ options.path = "/var/lib/grafana/dashboards"; }];
+
+ notifiers = [{
+ uid = "test_notifiers";
+ name = "Test Notifiers";
+ type = "email";
+ settings = {
+ singleEmail = true;
+ addresses = "test@test.com";
+ };
+ }];
+ };
+ };
+
+ provisionNix = {
+ services.grafana.provision = {
+ datasources.settings = {
+ apiVersion = 1;
+ datasources = [{
+ name = "Test Datasource";
+ type = "testdata";
+ access = "proxy";
+ uid = "test_datasource";
+ }];
+ };
+
+ dashboards.settings = {
+ apiVersion = 1;
+ providers = [{
+ name = "default";
+ options.path = "/var/lib/grafana/dashboards";
+ }];
+ };
+
+ alerting = {
+ rules.settings = {
+ groups = [{
+ name = "test_rule_group";
+ folder = "test_folder";
+ interval = "60s";
+ rules = [{
+ uid = "test_rule";
+ title = "Test Rule";
+ condition = "A";
+ data = [{
+ refId = "A";
+ datasourceUid = "-100";
+ model = {
+ conditions = [{
+ evaluator = {
+ params = [ 3 ];
+ type = "git";
+ };
+ operator.type = "and";
+ query.params = [ "A" ];
+ reducer.type = "last";
+ type = "query";
+ }];
+ datasource = {
+ type = "__expr__";
+ uid = "-100";
+ };
+ expression = "1==0";
+ intervalMs = 1000;
+ maxDataPoints = 43200;
+ refId = "A";
+ type = "math";
+ };
+ }];
+ for = "60s";
+ }];
+ }];
+ };
+
+ contactPoints.settings = {
+ contactPoints = [{
+ name = "Test Contact Point";
+ receivers = [{
+ uid = "test_contact_point";
+ type = "prometheus-alertmanager";
+ settings.url = "http://localhost:9000";
+ }];
+ }];
+ };
+
+ policies.settings = {
+ policies = [{
+ receiver = "Test Contact Point";
+ }];
+ };
+
+ templates.settings = {
+ templates = [{
+ name = "Test Template";
+ template = "Test message";
+ }];
+ };
+
+ muteTimings.settings = {
+ muteTimes = [{
+ name = "Test Mute Timing";
+ }];
+ };
+ };
+ };
+ };
+
+ provisionYaml = {
+ services.grafana.provision = {
+ datasources.path = ./datasources.yaml;
+ dashboards.path = ./dashboards.yaml;
+ alerting = {
+ rules.path = ./rules.yaml;
+ contactPoints.path = ./contact-points.yaml;
+ policies.path = ./policies.yaml;
+ templates.path = ./templates.yaml;
+ muteTimings.path = ./mute-timings.yaml;
+ };
+ };
+ };
+ };
+
+ nodes = builtins.mapAttrs (_: val: mkMerge [ val baseGrafanaConf ]) extraNodeConfs;
+in {
+ name = "grafana-provision";
+
+ meta = with maintainers; {
+ maintainers = [ kfears willibutz ];
+ };
+
+ inherit nodes;
+
+ testScript = ''
+ start_all()
+
+ nodeOld = ("Nix (old format)", provisionOld)
+ nodeNix = ("Nix (new format)", provisionNix)
+ nodeYaml = ("Nix (YAML)", provisionYaml)
+
+ for nodeInfo in [nodeOld, nodeNix, nodeYaml]:
+ with subtest(f"Should start provision node: {nodeInfo[0]}"):
+ nodeInfo[1].wait_for_unit("grafana.service")
+ nodeInfo[1].wait_for_open_port(3000)
+
+ with subtest(f"Successful datasource provision with {nodeInfo[0]}"):
+ nodeInfo[1].succeed(
+ "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/datasources/uid/test_datasource | grep Test\ Datasource"
+ )
+
+ with subtest(f"Successful dashboard provision with {nodeInfo[0]}"):
+ nodeInfo[1].succeed(
+ "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/dashboards/uid/test_dashboard | grep Test\ Dashboard"
+ )
+
+
+
+ with subtest(f"Successful notifiers provision with {nodeOld[0]}"):
+ nodeOld[1].succeed(
+ "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/alert-notifications/uid/test_notifiers | grep Test\ Notifiers"
+ )
+
+
+
+ for nodeInfo in [nodeNix, nodeYaml]:
+ with subtest(f"Successful rule provision with {nodeInfo[0]}"):
+ nodeInfo[1].succeed(
+ "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/alert-rules/test_rule | grep Test\ Rule"
+ )
+
+ with subtest(f"Successful contact point provision with {nodeInfo[0]}"):
+ nodeInfo[1].succeed(
+ "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/contact-points | grep Test\ Contact\ Point"
+ )
+
+ with subtest(f"Successful policy provision with {nodeInfo[0]}"):
+ nodeInfo[1].succeed(
+ "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/policies | grep Test\ Contact\ Point"
+ )
+
+ with subtest(f"Successful template provision with {nodeInfo[0]}"):
+ nodeInfo[1].succeed(
+ "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/templates | grep Test\ Template"
+ )
+
+ with subtest("Successful mute timings provision with {nodeInfo[0]}"):
+ nodeInfo[1].succeed(
+ "curl -sSfN -u testadmin:snakeoilpwd http://127.0.0.1:3000/api/v1/provisioning/mute-timings | grep Test\ Mute\ Timing"
+ )
+ '';
+})
diff --git a/nixos/tests/grafana/provision/mute-timings.yaml b/nixos/tests/grafana/provision/mute-timings.yaml
new file mode 100644
index 000000000000..1f47f7c18f0c
--- /dev/null
+++ b/nixos/tests/grafana/provision/mute-timings.yaml
@@ -0,0 +1,4 @@
+apiVersion: 1
+
+muteTimes:
+ - name: "Test Mute Timing"
diff --git a/nixos/tests/grafana/provision/policies.yaml b/nixos/tests/grafana/provision/policies.yaml
new file mode 100644
index 000000000000..eb31126c4ba5
--- /dev/null
+++ b/nixos/tests/grafana/provision/policies.yaml
@@ -0,0 +1,4 @@
+apiVersion: 1
+
+policies:
+ - receiver: "Test Contact Point"
diff --git a/nixos/tests/grafana/provision/rules.yaml b/nixos/tests/grafana/provision/rules.yaml
new file mode 100644
index 000000000000..946539c8cb69
--- /dev/null
+++ b/nixos/tests/grafana/provision/rules.yaml
@@ -0,0 +1,36 @@
+apiVersion: 1
+
+groups:
+ - name: "test_rule_group"
+ folder: "test_group"
+ interval: 60s
+ rules:
+ - uid: "test_rule"
+ title: "Test Rule"
+ condition: A
+ data:
+ - refId: A
+ datasourceUid: '-100'
+ model:
+ conditions:
+ - evaluator:
+ params:
+ - 3
+ type: gt
+ operator:
+ type: and
+ query:
+ params:
+ - A
+ reducer:
+ type: last
+ type: query
+ datasource:
+ type: __expr__
+ uid: '-100'
+ expression: 1==0
+ intervalMs: 1000
+ maxDataPoints: 43200
+ refId: A
+ type: math
+ for: 60s
diff --git a/nixos/tests/grafana/provision/templates.yaml b/nixos/tests/grafana/provision/templates.yaml
new file mode 100644
index 000000000000..09df247b3451
--- /dev/null
+++ b/nixos/tests/grafana/provision/templates.yaml
@@ -0,0 +1,5 @@
+apiVersion: 1
+
+templates:
+ - name: "Test Template"
+ template: "Test message"
diff --git a/nixos/tests/grafana/provision/test_dashboard.json b/nixos/tests/grafana/provision/test_dashboard.json
new file mode 100644
index 000000000000..6e7a5b37f22b
--- /dev/null
+++ b/nixos/tests/grafana/provision/test_dashboard.json
@@ -0,0 +1,47 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "grafana",
+ "uid": "-- Grafana --"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "target": {
+ "limit": 100,
+ "matchAny": false,
+ "tags": [],
+ "type": "dashboard"
+ },
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": 28,
+ "links": [],
+ "liveNow": false,
+ "panels": [],
+ "schemaVersion": 37,
+ "style": "dark",
+ "tags": [],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-6h",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "",
+ "title": "Test Dashboard",
+ "uid": "test_dashboard",
+ "version": 1,
+ "weekStart": ""
+}