diff options
Diffstat (limited to 'nixos/modules/services')
25 files changed, 1508 insertions, 592 deletions
diff --git a/nixos/modules/services/continuous-integration/jenkins/default.nix b/nixos/modules/services/continuous-integration/jenkins/default.nix index 6cd5718f42276..a9a587b41e881 100644 --- a/nixos/modules/services/continuous-integration/jenkins/default.nix +++ b/nixos/modules/services/continuous-integration/jenkins/default.nix @@ -87,8 +87,8 @@ in { }; packages = mkOption { - default = [ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ]; - defaultText = literalExpression "[ pkgs.stdenv pkgs.git pkgs.jdk11 config.programs.ssh.package pkgs.nix ]"; + default = [ pkgs.stdenv pkgs.git pkgs.jdk17 config.programs.ssh.package pkgs.nix ]; + defaultText = literalExpression "[ pkgs.stdenv pkgs.git pkgs.jdk17 config.programs.ssh.package pkgs.nix ]"; type = types.listOf types.package; description = lib.mdDoc '' Packages to add to PATH for the jenkins process. @@ -228,7 +228,7 @@ in { # For reference: https://wiki.jenkins.io/display/JENKINS/JenkinsLinuxStartupScript script = '' - ${pkgs.jdk11}/bin/java ${concatStringsSep " " cfg.extraJavaOptions} -jar ${cfg.package}/webapps/jenkins.war --httpListenAddress=${cfg.listenAddress} \ + ${pkgs.jdk17}/bin/java ${concatStringsSep " " cfg.extraJavaOptions} -jar ${cfg.package}/webapps/jenkins.war --httpListenAddress=${cfg.listenAddress} \ --httpPort=${toString cfg.port} \ --prefix=${cfg.prefix} \ -Djava.awt.headless=true \ diff --git a/nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json b/nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json new file mode 100644 index 0000000000000..689fca88359ba --- /dev/null +++ b/nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json @@ -0,0 +1,28 @@ +{ + "context.properties": { + "log.level": 0 + }, + "context.spa-libs": { + "audio.convert.*": "audioconvert/libspa-audioconvert", + "support.*": "support/libspa-support" + }, + "context.modules": [ + { + "name": "libpipewire-module-rt", + "args": {}, + "flags": [ + "ifexists", + "nofail" + ] + }, + { + "name": "libpipewire-module-protocol-native" + }, + { + "name": "libpipewire-module-client-node" + }, + { + "name": "libpipewire-module-adapter" + } + ] +} diff --git a/nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json b/nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json new file mode 100644 index 0000000000000..4f669895d87b6 --- /dev/null +++ b/nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json @@ -0,0 +1,38 @@ +{ + "context.properties": {}, + "context.spa-libs": { + "audio.convert.*": "audioconvert/libspa-audioconvert", + "support.*": "support/libspa-support" + }, + "context.modules": [ + { + "name": "libpipewire-module-rt", + "args": { + "nice.level": -11 + }, + "flags": [ + "ifexists", + "nofail" + ] + }, + { + "name": "libpipewire-module-protocol-native" + }, + { + "name": "libpipewire-module-client-node" + }, + { + "name": "libpipewire-module-adapter" + }, + { + "name": "libpipewire-module-avb", + "args": {} + } + ], + "context.exec": [], + "stream.properties": {}, + "avb.properties": { + "ifname": "enp3s0", + "vm.overrides": {} + } +} diff --git a/nixos/modules/services/misc/ethminer.nix b/nixos/modules/services/misc/ethminer.nix index 909c49866e543..c9b2e24b8bf1b 100644 --- a/nixos/modules/services/misc/ethminer.nix +++ b/nixos/modules/services/misc/ethminer.nix @@ -85,7 +85,7 @@ in config = mkIf cfg.enable { systemd.services.ethminer = { - path = optional (cfg.toolkit == "cuda") [ pkgs.cudaPackages.cudatoolkit ]; + path = optionals (cfg.toolkit == "cuda") [ pkgs.cudaPackages.cudatoolkit ]; description = "ethminer ethereum mining service"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix index ba3ea4c47ac1d..e8a21c352bdd7 100644 --- a/nixos/modules/services/misc/nix-daemon.nix +++ b/nixos/modules/services/misc/nix-daemon.nix @@ -59,7 +59,7 @@ let ${mkKeyValuePairs cfg.settings} ${cfg.extraOptions} ''; - checkPhase = + checkPhase = lib.optionalString cfg.checkConfig ( if pkgs.stdenv.hostPlatform != pkgs.stdenv.buildPlatform then '' echo "Ignoring validation for cross-compilation" '' @@ -72,9 +72,9 @@ let ${cfg.package}/bin/nix show-config ${optionalString (isNixAtLeast "2.3pre") "--no-net"} \ ${optionalString (isNixAtLeast "2.4pre") "--option experimental-features nix-command"} \ |& sed -e 's/^warning:/error:/' \ - | (! grep '${if cfg.checkConfig then "^error:" else "^error: unknown setting"}') + | (! grep '${if cfg.checkAllErrors then "^error:" else "^error: unknown setting"}') set -o pipefail - ''; + ''); }; legacyConfMappings = { @@ -395,8 +395,15 @@ in type = types.bool; default = true; description = lib.mdDoc '' - If enabled (the default), checks for data type mismatches and that Nix - can parse the generated nix.conf. + If enabled, checks that Nix can parse the generated nix.conf. + ''; + }; + + checkAllErrors = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + If enabled, checks the nix.conf parsing for any kind of error. When disabled, checks only for unknown settings. ''; }; diff --git a/nixos/modules/services/misc/podgrab.nix b/nixos/modules/services/misc/podgrab.nix index 10c7bc96b8f04..c0a1247185050 100644 --- a/nixos/modules/services/misc/podgrab.nix +++ b/nixos/modules/services/misc/podgrab.nix @@ -36,7 +36,7 @@ in }; serviceConfig = { DynamicUser = true; - EnvironmentFile = lib.optional (cfg.passwordFile != null) [ + EnvironmentFile = lib.optionals (cfg.passwordFile != null) [ cfg.passwordFile ]; ExecStart = "${pkgs.podgrab}/bin/podgrab"; diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix index 549da138fe236..60f6e84c63c7d 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 15ca11a2e1642..964602547e7df 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"; - - 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; + useMysql = cfg.settings.database.type == "mysql"; + usePostgresql = cfg.settings.database.type == "postgres"; + + 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."; + editable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Allow users to edit datasources from the UI."; }; - basicAuthUser = mkOption { + password = mkOption { type = types.nullOr types.str; default = null; - description = lib.mdDoc "Basic auth username."; + 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: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; }; 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."; + 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: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; }; 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."; + 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: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; }; }; }; # 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: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; }; }; }; 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,354 +294,946 @@ 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"]; - }; - - 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. - This option is mutual exclusive with the passwordFile option. - ''; - 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; - }; - - path = mkOption { - description = lib.mdDoc "Database path."; - default = "${cfg.dataDir}/data/grafana.db"; - defaultText = literalExpression ''"''${config.${opt.dataDir}}/data/grafana.db"''; - type = types.path; - }; - - 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" ]); + settings = mkOption { + description = lib.mdDoc '' + Grafana settings. See <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/> + for available options. INI format is used. + ''; + type = types.submodule { + freeformType = settingsFormatIni.type; + + 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; + }; + + 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; + }; + }; + + server = { + protocol = mkOption { + description = lib.mdDoc "Which protocol to listen."; + default = "http"; + type = types.enum ["http" "https" "socket"]; + }; + + http_addr = mkOption { + description = lib.mdDoc "Listening address."; + default = ""; + type = types.str; + }; + + http_port = mkOption { + description = lib.mdDoc "Listening port."; + default = 3000; + type = types.port; + }; + + domain = mkOption { + description = lib.mdDoc "The public facing domain name used to access grafana from a browser."; + default = "localhost"; + type = types.str; + }; + + 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: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; + 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: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; + 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: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; + 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: + <https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider> + ''; + 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."; - default = []; - type = types.listOf grafanaTypes.datasourceConfig; - apply = x: map _filter x; - }; - dashboards = mkOption { - description = lib.mdDoc "Grafana dashboard configuration."; - default = []; - type = types.listOf grafanaTypes.dashboardConfig; - apply = x: map _filter x; - }; - 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 { + datasources = mkOption { description = lib.mdDoc '' - Default admin password. - This option is mutual exclusive with the `adminPassword` option. + Deprecated option for Grafana datasource configuration. Use either + `services.grafana.provision.datasources.settings` or + `services.grafana.provision.datasources.path` instead. ''; - default = null; - type = types.nullOr types.path; - }; - - secretKey = mkOption { - description = lib.mdDoc "Secret key used for signing."; - default = "SW2YcwTIb9zpOOhoPsMm"; - type = types.str; - }; + default = []; + 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 + <https://grafana.com/docs/grafana/latest/administration/provisioning/#data-sources> + 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; + }]; + } + ''; + }; - secretKeyFile = mkOption { - description = lib.mdDoc "Secret key used for signing."; - default = null; - type = types.nullOr types.path; + 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; + }; + }); }; - }; - 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 { + dashboards = mkOption { description = lib.mdDoc '' - Password used for authentication. - This option is mutual exclusive with the password option. + Deprecated option for Grafana dashboard configuration. Use either + `services.grafana.provision.dashboards.settings` or + `services.grafana.provision.dashboards.path` instead. ''; - 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; - }; + default = []; + 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 + <https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards> + 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"; + }]; + } + ''; + }; - allowOrgCreate = mkOption { - description = lib.mdDoc "Whether user is allowed to create organizations."; - default = false; - type = types.bool; + 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; + }; + }); }; - 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"]; + notifiers = mkOption { + description = lib.mdDoc "Grafana notifier configuration."; + default = []; + type = types.listOf grafanaTypes.notifierConfig; + apply = x: map _filter x; }; - }; - 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 + <https://grafana.com/docs/grafana/latest/administration/provisioning/#rules> + for supported options. ''; - default = ""; - type = types.str; - }; - }; - google = { - enable = mkOption { - description = lib.mdDoc "Whether to allow Google OAuth2."; - default = false; - type = types.bool; + default = null; + type = types.nullOr (types.submodule { + options = { + apiVersion = mkOption { + description = lib.mdDoc "Config file version."; + default = 1; + type = types.int; + }; + + groups = mkOption { + description = lib.mdDoc "List of rule groups to import or update."; + default = []; + type = types.listOf (types.submodule { + freeformType = provisioningSettingsFormat.type; + + 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"; + }]; + } + ''; + }; }; - allowSignUp = mkOption { - description = lib.mdDoc "Whether to allow sign up with Google OAuth2."; - default = false; - type = types.bool; + + 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 + <https://grafana.com/docs/grafana/latest/administration/provisioning/#contact-points> + 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"; + }]; + } + ''; + }; }; - clientId = mkOption { - description = lib.mdDoc "Google OAuth2 client ID."; - default = ""; - type = types.str; + + 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 + <https://grafana.com/docs/grafana/latest/administration/provisioning/#notification-policies> + 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 + ]; + } + ''; + }; }; - clientSecretFile = mkOption { - description = lib.mdDoc "Google OAuth2 client secret."; - default = null; - type = types.nullOr types.path; + + 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 + <https://grafana.com/docs/grafana/latest/administration/provisioning/#templates> + 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"; + }]; + } + ''; + }; }; - }; - }; - analytics.reporting = { - enable = mkOption { - description = lib.mdDoc "Whether to allow anonymous usage reporting to stats.grafana.net."; - default = true; - type = types.bool; + 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 + <https://grafana.com/docs/grafana/latest/administration/provisioning/#mute-timings> + 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"; + }]; + } + ''; + }; + }; }; }; - - 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); - }; }; 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 = let + prometheusIsNotDirect = opt: all + ({ type, access, ... }: type == "prometheus" -> access != "direct") + 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 = cfg.security.secretKey != opt.security.secretKey.default -> cfg.security.secretKeyFile == null; - message = "Cannot set both secretKey and secretKeyFile"; + 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.smtp.password != opt.smtp.password.default -> cfg.smtp.passwordFile == null; - message = "Cannot set both password and passwordFile"; + assertion = cfg.provision.alerting.rules.settings == null || cfg.provision.alerting.rules.path == null; + message = "Cannot set both rules settings and rules path"; } { - assertion = all - ({ type, access, ... }: type == "prometheus" -> access != "direct") - cfg.provision.datasources; - message = "For datasources of type `prometheus`, the `direct` access mode is not supported anymore (since Grafana 9.2.0)"; + 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"; } ]; @@ -732,41 +1241,11 @@ in { 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/monitoring/uptime-kuma.nix b/nixos/modules/services/monitoring/uptime-kuma.nix new file mode 100644 index 0000000000000..3a6091de679d9 --- /dev/null +++ b/nixos/modules/services/monitoring/uptime-kuma.nix @@ -0,0 +1,76 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.services.uptime-kuma; +in +{ + + options = { + services.uptime-kuma = { + enable = mkEnableOption (mdDoc "Uptime Kuma, this assumes a reverse proxy to be set."); + + package = mkOption { + type = types.package; + example = literalExpression "pkgs.uptime-kuma"; + default = pkgs.uptime-kuma; + defaultText = "pkgs.uptime-kuma"; + description = lib.mdDoc "Uptime Kuma package to use."; + }; + + settings = lib.mkOption { + type = + lib.types.submodule { freeformType = with lib.types; attrsOf str; }; + default = { }; + example = { + PORT = "4000"; + NODE_EXTRA_CA_CERTS = "/etc/ssl/certs/ca-certificates.crt"; + }; + description = lib.mdDoc '' + Additional configuration for Uptime Kuma, see + <https://github.com/louislam/uptime-kuma/wiki/Environment-Variables"> + for supported values. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + + services.uptime-kuma.settings = { + DATA_DIR = "/var/lib/uptime-kuma/"; + NODE_ENV = mkDefault "production"; + }; + + systemd.services.uptime-kuma = { + description = "Uptime Kuma"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = cfg.settings; + serviceConfig = { + Type = "simple"; + StateDirectory = "uptime-kuma"; + DynamicUser = true; + ExecStart = "${cfg.package}/bin/uptime-kuma-server"; + Restart = "on-failure"; + ProtectHome = true; + ProtectSystem = "strict"; + PrivateTmp = true; + PrivateDevices = true; + ProtectHostname = true; + ProtectClock = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + NoNewPrivileges = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + RemoveIPC = true; + PrivateMounts = true; + }; + }; + }; +} + diff --git a/nixos/modules/services/network-filesystems/litestream/litestream.xml b/nixos/modules/services/network-filesystems/litestream/litestream.xml index 598f9be8cf632..8f5597bb6891e 100644 --- a/nixos/modules/services/network-filesystems/litestream/litestream.xml +++ b/nixos/modules/services/network-filesystems/litestream/litestream.xml @@ -15,7 +15,7 @@ <para> Litestream service is managed by a dedicated user named <literal>litestream</literal> which needs permission to the database file. Here's an example config which gives - required permissions to access <link linkend="opt-services.grafana.database.path"> + required permissions to access <link linkend="opt-services.grafana.settings.database.path"> grafana database</link>: <programlisting> { pkgs, ... }: diff --git a/nixos/modules/services/networking/hostapd.nix b/nixos/modules/services/networking/hostapd.nix index ec1a7a58b1e09..63bb44256dd69 100644 --- a/nixos/modules/services/networking/hostapd.nix +++ b/nixos/modules/services/networking/hostapd.nix @@ -199,7 +199,7 @@ in environment.systemPackages = [ pkgs.hostapd ]; - services.udev.packages = optional (cfg.countryCode != null) [ pkgs.crda ]; + services.udev.packages = optionals (cfg.countryCode != null) [ pkgs.crda ]; systemd.services.hostapd = { description = "hostapd wireless AP"; diff --git a/nixos/modules/services/networking/ntp/chrony.nix b/nixos/modules/services/networking/ntp/chrony.nix index a89c7769152e6..7e3bb565d10bf 100644 --- a/nixos/modules/services/networking/ntp/chrony.nix +++ b/nixos/modules/services/networking/ntp/chrony.nix @@ -27,7 +27,7 @@ let ${cfg.extraConfig} ''; - chronyFlags = "-n -m -u chrony -f ${configFile} ${toString cfg.extraFlags}"; + chronyFlags = [ "-n" "-m" "-u" "chrony" "-f" "${configFile}" ] ++ cfg.extraFlags; in { options = { @@ -166,7 +166,7 @@ in unitConfig.ConditionCapability = "CAP_SYS_TIME"; serviceConfig = { Type = "simple"; - ExecStart = "${chronyPkg}/bin/chronyd ${chronyFlags}"; + ExecStart = "${chronyPkg}/bin/chronyd ${builtins.toString chronyFlags}"; ProtectHome = "yes"; ProtectSystem = "full"; diff --git a/nixos/modules/services/networking/ntp/ntpd.nix b/nixos/modules/services/networking/ntp/ntpd.nix index a9dae2c8667aa..036a8df635db0 100644 --- a/nixos/modules/services/networking/ntp/ntpd.nix +++ b/nixos/modules/services/networking/ntp/ntpd.nix @@ -25,7 +25,7 @@ let ${cfg.extraConfig} ''; - ntpFlags = "-c ${configFile} -u ntp:ntp ${toString cfg.extraFlags}"; + ntpFlags = [ "-c" "${configFile}" "-u" "ntp:ntp" ] ++ cfg.extraFlags; in @@ -137,7 +137,7 @@ in ''; serviceConfig = { - ExecStart = "@${ntp}/bin/ntpd ntpd -g ${ntpFlags}"; + ExecStart = "@${ntp}/bin/ntpd ntpd -g ${builtins.toString ntpFlags}"; Type = "forking"; }; }; diff --git a/nixos/modules/services/search/hound.nix b/nixos/modules/services/search/hound.nix index c81ceee546965..b41a2e2bae1fa 100644 --- a/nixos/modules/services/search/hound.nix +++ b/nixos/modules/services/search/hound.nix @@ -120,7 +120,6 @@ in { " -conf ${pkgs.writeText "hound.json" cfg.config}"; }; - path = [ pkgs.git pkgs.mercurial pkgs.openssh ]; }; }; diff --git a/nixos/modules/services/security/tor.nix b/nixos/modules/services/security/tor.nix index 730802d92cfa8..b85b78f269a1d 100644 --- a/nixos/modules/services/security/tor.nix +++ b/nixos/modules/services/security/tor.nix @@ -816,13 +816,13 @@ in always create a container/VM with a separate Tor daemon instance. '' ++ flatten (mapAttrsToList (n: o: - optional (o.settings.HiddenServiceVersion == 2) [ + optionals (o.settings.HiddenServiceVersion == 2) [ (optional (o.settings.HiddenServiceExportCircuitID != null) '' HiddenServiceExportCircuitID is used in the HiddenService: ${n} but this option is only for v3 hidden services. '') ] ++ - optional (o.settings.HiddenServiceVersion != 2) [ + optionals (o.settings.HiddenServiceVersion != 2) [ (optional (o.settings.HiddenServiceAuthorizeClient != null) '' HiddenServiceAuthorizeClient is used in the HiddenService: ${n} but this option is only for v2 hidden services. diff --git a/nixos/modules/services/web-apps/changedetection-io.nix b/nixos/modules/services/web-apps/changedetection-io.nix index 83d8b32c0c833..6a54311e35ed6 100644 --- a/nixos/modules/services/web-apps/changedetection-io.nix +++ b/nixos/modules/services/web-apps/changedetection-io.nix @@ -119,7 +119,7 @@ in assertions = [ { assertion = !((cfg.webDriverSupport == true) && (cfg.playwrightSupport == true)); - message = "'services.changedetection-io.webDriverSupport' and 'services.changedetion-io.playwrightSupport' cannot be used together."; + message = "'services.changedetection-io.webDriverSupport' and 'services.changedetection-io.playwrightSupport' cannot be used together."; } ]; @@ -135,7 +135,7 @@ in serviceConfig = { User = cfg.user; Group = cfg.group; - StateDirectory = mkIf defaultStateDir "changedetion-io"; + StateDirectory = mkIf defaultStateDir "changedetection-io"; StateDirectoryMode = mkIf defaultStateDir "0750"; WorkingDirectory = cfg.datastorePath; Environment = lib.optional (cfg.baseURL != null) "BASE_URL=${cfg.baseURL}" @@ -213,6 +213,7 @@ in }; }) ]; + podman.defaultNetwork.dnsname.enable = true; }; }; } diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix index f4016cd5228e3..f0b3c7b2bcf89 100644 --- a/nixos/modules/services/web-apps/dokuwiki.nix +++ b/nixos/modules/services/web-apps/dokuwiki.nix @@ -7,7 +7,6 @@ let eachSite = cfg.sites; user = "dokuwiki"; webserver = config.services.${cfg.webserver}; - stateDir = hostName: "/var/lib/dokuwiki/${hostName}/data"; dokuwikiAclAuthConfig = hostName: cfg: pkgs.writeText "acl.auth-${hostName}.php" '' # acl.auth.php @@ -325,17 +324,17 @@ in { systemd.tmpfiles.rules = flatten (mapAttrsToList (hostName: cfg: [ - "d ${stateDir hostName}/attic 0750 ${user} ${webserver.group} - -" - "d ${stateDir hostName}/cache 0750 ${user} ${webserver.group} - -" - "d ${stateDir hostName}/index 0750 ${user} ${webserver.group} - -" - "d ${stateDir hostName}/locks 0750 ${user} ${webserver.group} - -" - "d ${stateDir hostName}/log 0750 ${user} ${webserver.group} - -" - "d ${stateDir hostName}/media 0750 ${user} ${webserver.group} - -" - "d ${stateDir hostName}/media_attic 0750 ${user} ${webserver.group} - -" - "d ${stateDir hostName}/media_meta 0750 ${user} ${webserver.group} - -" - "d ${stateDir hostName}/meta 0750 ${user} ${webserver.group} - -" - "d ${stateDir hostName}/pages 0750 ${user} ${webserver.group} - -" - "d ${stateDir hostName}/tmp 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/attic 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/cache 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/index 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/locks 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/log 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/media 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/media_attic 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/media_meta 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/meta 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/pages 0750 ${user} ${webserver.group} - -" + "d ${cfg.stateDir}/tmp 0750 ${user} ${webserver.group} - -" ] ++ lib.optional (cfg.aclFile != null) "C ${cfg.aclFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/acl.auth.php.dist" ++ lib.optional (cfg.usersFile != null) "C ${cfg.usersFile} 0640 ${user} ${webserver.group} - ${pkg hostName cfg}/share/dokuwiki/conf/users.auth.php.dist" ) eachSite); @@ -359,7 +358,7 @@ in }; "~ ^/data/" = { - root = "${stateDir hostName}"; + root = "${cfg.stateDir}"; extraConfig = "internal;"; }; diff --git a/nixos/modules/services/web-apps/freshrss.nix b/nixos/modules/services/web-apps/freshrss.nix index 7898347e0b4f2..a0fb79742d84a 100644 --- a/nixos/modules/services/web-apps/freshrss.nix +++ b/nixos/modules/services/web-apps/freshrss.nix @@ -155,9 +155,17 @@ in virtualHosts.${cfg.virtualHost} = { root = "${cfg.package}/p"; + # php files handling + # this regex is mandatory because of the API locations."~ ^.+?\.php(/.*)?$".extraConfig = '' fastcgi_pass unix:${config.services.phpfpm.pools.${cfg.pool}.socket}; fastcgi_split_path_info ^(.+\.php)(/.*)$; + # By default, the variable PATH_INFO is not set under PHP-FPM + # But FreshRSS API greader.php need it. If you have a “Bad Request” error, double check this var! + # NOTE: the separate $path_info variable is required. For more details, see: + # https://trac.nginx.org/nginx/ticket/321 + set $path_info $fastcgi_path_info; + fastcgi_param PATH_INFO $path_info; include ${pkgs.nginx}/conf/fastcgi_params; include ${pkgs.nginx}/conf/fastcgi.conf; ''; diff --git a/nixos/modules/services/web-apps/miniflux.nix b/nixos/modules/services/web-apps/miniflux.nix index fad5701aeedc4..34a108cebd2b8 100644 --- a/nixos/modules/services/web-apps/miniflux.nix +++ b/nixos/modules/services/web-apps/miniflux.nix @@ -116,7 +116,7 @@ in RestrictRealtime = true; RestrictSUIDSGID = true; SystemCallArchitectures = "native"; - SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; + SystemCallFilter = [ "@system-service" "~@privileged" ]; UMask = "0077"; }; diff --git a/nixos/modules/services/web-apps/netbox.nix b/nixos/modules/services/web-apps/netbox.nix index 2826e57f2c776..f09a8dfc5b215 100644 --- a/nixos/modules/services/web-apps/netbox.nix +++ b/nixos/modules/services/web-apps/netbox.nix @@ -46,7 +46,7 @@ let ''; })).override { plugins = ps: ((cfg.plugins ps) - ++ optional cfg.enableLdap [ ps.django-auth-ldap ]); + ++ optionals cfg.enableLdap [ ps.django-auth-ldap ]); }; netboxManageScript = with pkgs; (writeScriptBin "netbox-manage" '' #!${stdenv.shell} diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix index fdbaa90ebb7a3..750a64cf2bc8e 100644 --- a/nixos/modules/services/web-apps/nextcloud.nix +++ b/nixos/modules/services/web-apps/nextcloud.nix @@ -156,7 +156,7 @@ in { package = mkOption { type = types.package; description = lib.mdDoc "Which package to use for the Nextcloud instance."; - relatedPackages = [ "nextcloud23" "nextcloud24" ]; + relatedPackages = [ "nextcloud24" "nextcloud25" ]; }; phpPackage = mkOption { type = types.package; @@ -637,10 +637,9 @@ in { Using config.services.nextcloud.poolConfig is deprecated and will become unsupported in a future release. Please migrate your configuration to config.services.nextcloud.poolSettings. '') - ++ (optional (versionOlder cfg.package.version "21") (upgradeWarning 20 "21.05")) - ++ (optional (versionOlder cfg.package.version "22") (upgradeWarning 21 "21.11")) ++ (optional (versionOlder cfg.package.version "23") (upgradeWarning 22 "22.05")) ++ (optional (versionOlder cfg.package.version "24") (upgradeWarning 23 "22.05")) + ++ (optional (versionOlder cfg.package.version "25") (upgradeWarning 24 "22.11")) ++ (optional isUnsupportedMariadb '' You seem to be using MariaDB at an unsupported version (i.e. at least 10.6)! Please note that this isn't supported officially by Nextcloud. You can either @@ -661,19 +660,13 @@ in { nextcloud defined in an overlay, please set `services.nextcloud.package` to `pkgs.nextcloud`. '' - else if versionOlder stateVersion "22.05" then nextcloud22 - else nextcloud24 + else if versionOlder stateVersion "22.11" then nextcloud24 + else nextcloud25 ); services.nextcloud.phpPackage = if versionOlder cfg.package.version "24" then pkgs.php80 - # FIXME: Use PHP 8.1 with Nextcloud 24 and higher, once issues like this one are fixed: - # - # https://github.com/nextcloud/twofactor_totp/issues/1192 - # - # else if versionOlder cfg.package.version "24" then pkgs.php80 - # else pkgs.php81; - else pkgs.php80; + else pkgs.php81; } { assertions = [ diff --git a/nixos/modules/services/web-apps/nextcloud.xml b/nixos/modules/services/web-apps/nextcloud.xml index b46f34420a703..a0b69dbd606ce 100644 --- a/nixos/modules/services/web-apps/nextcloud.xml +++ b/nixos/modules/services/web-apps/nextcloud.xml @@ -11,7 +11,7 @@ desktop client is packaged at <literal>pkgs.nextcloud-client</literal>. </para> <para> - The current default by NixOS is <package>nextcloud24</package> which is also the latest + The current default by NixOS is <package>nextcloud25</package> which is also the latest major version available. </para> <section xml:id="module-services-nextcloud-basic-usage"> diff --git a/nixos/modules/services/web-apps/peertube.nix b/nixos/modules/services/web-apps/peertube.nix index 1ac6c15dace9a..a42d1a1a932e3 100644 --- a/nixos/modules/services/web-apps/peertube.nix +++ b/nixos/modules/services/web-apps/peertube.nix @@ -67,6 +67,12 @@ let node ~/dist/server/tools/peertube.js $@ ''; + nginxCommonHeaders = '' + add_header Access-Control-Allow-Origin '*'; + add_header Access-Control-Allow-Methods 'GET, OPTIONS'; + add_header Access-Control-Allow-Headers 'Range,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; + ''; + in { options.services.peertube = { enable = lib.mkEnableOption (lib.mdDoc "Enable Peertube’s service"); @@ -145,6 +151,12 @@ in { description = lib.mdDoc "Configuration for peertube."; }; + configureNginx = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc "Configure nginx as a reverse proxy for peertube."; + }; + database = { createLocally = lib.mkOption { type = lib.types.bool; @@ -351,6 +363,8 @@ in { systemd.tmpfiles.rules = [ "d '/var/lib/peertube/config' 0700 ${cfg.user} ${cfg.group} - -" "z '/var/lib/peertube/config' 0700 ${cfg.user} ${cfg.group} - -" + "d '/var/lib/peertube/www' 0750 ${cfg.user} ${cfg.group} - -" + "z '/var/lib/peertube/www' 0750 ${cfg.user} ${cfg.group} - -" ]; systemd.services.peertube-init-db = lib.mkIf cfg.database.createLocally { @@ -410,8 +424,11 @@ in { password: '$(cat ${cfg.smtp.passwordFile})' ''} EOF - ln -sf ${cfg.package}/config/default.yaml /var/lib/peertube/config/default.yaml + umask 027 ln -sf ${configFile} /var/lib/peertube/config/production.json + ln -sf ${cfg.package}/config/default.yaml /var/lib/peertube/config/default.yaml + ln -sf ${cfg.package}/client/dist -T /var/lib/peertube/www/client + ln -sf ${cfg.settings.storage.client_overrides} -T /var/lib/peertube/www/client-overrides npm start ''; serviceConfig = { @@ -441,6 +458,269 @@ in { } // cfgService; }; + services.nginx = lib.mkIf cfg.configureNginx { + enable = true; + virtualHosts."${cfg.localDomain}" = { + root = "/var/lib/peertube"; + + # Application + locations."/" = { + tryFiles = "/dev/null @api"; + priority = 1110; + }; + + locations."= /api/v1/videos/upload-resumable" = { + tryFiles = "/dev/null @api"; + priority = 1120; + + extraConfig = '' + client_max_body_size 0; + proxy_request_buffering off; + ''; + }; + + locations."~ ^/api/v1/videos/(upload|([^/]+/studio/edit))$" = { + tryFiles = "/dev/null @api"; + root = cfg.settings.storage.tmp; + priority = 1130; + + extraConfig = '' + client_max_body_size 12G; + add_header X-File-Maximum-Size 8G always; + ''; + }; + + locations."~ ^/api/v1/(videos|video-playlists|video-channels|users/me)" = { + tryFiles = "/dev/null @api"; + priority = 1140; + + extraConfig = '' + client_max_body_size 6M; + add_header X-File-Maximum-Size 4M always; + ''; + }; + + locations."@api" = { + proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}"; + priority = 1150; + + extraConfig = '' + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + + proxy_connect_timeout 10m; + + proxy_send_timeout 10m; + proxy_read_timeout 10m; + + client_max_body_size 100k; + send_timeout 10m; + ''; + }; + + # Websocket + locations."/socket.io" = { + tryFiles = "/dev/null @api_websocket"; + priority = 1210; + }; + + locations."/tracker/socket" = { + tryFiles = "/dev/null @api_websocket"; + priority = 1220; + + extraConfig = '' + proxy_read_timeout 15m; + ''; + }; + + locations."@api_websocket" = { + proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}"; + priority = 1230; + + extraConfig = '' + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + + proxy_http_version 1.1; + ''; + }; + + # Bypass PeerTube for performance reasons. + locations."~ ^/client/(assets/images/(icons/icon-36x36\.png|icons/icon-48x48\.png|icons/icon-72x72\.png|icons/icon-96x96\.png|icons/icon-144x144\.png|icons/icon-192x192\.png|icons/icon-512x512\.png|logo\.svg|favicon\.png|default-playlist\.jpg|default-avatar-account\.png|default-avatar-account-48x48\.png|default-avatar-video-channel\.png|default-avatar-video-channel-48x48\.png))$" = { + tryFiles = "/www/client-overrides/$1 /www/client/$1 $1"; + priority = 1310; + }; + + locations."~ ^/client/(.*\.(js|css|png|svg|woff2|otf|ttf|woff|eot))$" = { + alias = "${cfg.package}/client/dist/$1"; + priority = 1320; + extraConfig = '' + add_header Cache-Control 'public, max-age=604800, immutable'; + ''; + }; + + locations."~ ^/lazy-static/(avatars|banners)/" = { + tryFiles = "$uri @api"; + root = cfg.settings.storage.avatars; + priority = 1330; + extraConfig = '' + if ($request_method = 'OPTIONS') { + ${nginxCommonHeaders} + add_header Access-Control-Max-Age 1728000; + add_header Cache-Control 'no-cache'; + add_header Content-Type 'text/plain charset=UTF-8'; + add_header Content-Length 0; + return 204; + } + + ${nginxCommonHeaders} + add_header Cache-Control 'public, max-age=7200'; + + rewrite ^/lazy-static/avatars/(.*)$ /$1 break; + rewrite ^/lazy-static/banners/(.*)$ /$1 break; + ''; + }; + + locations."^~ /lazy-static/previews/" = { + tryFiles = "$uri @api"; + root = cfg.settings.storage.previews; + priority = 1340; + extraConfig = '' + if ($request_method = 'OPTIONS') { + ${nginxCommonHeaders} + add_header Access-Control-Max-Age 1728000; + add_header Cache-Control 'no-cache'; + add_header Content-Type 'text/plain charset=UTF-8'; + add_header Content-Length 0; + return 204; + } + + ${nginxCommonHeaders} + add_header Cache-Control 'public, max-age=7200'; + + rewrite ^/lazy-static/previews/(.*)$ /$1 break; + ''; + }; + + locations."^~ /static/thumbnails/" = { + tryFiles = "$uri @api"; + root = cfg.settings.storage.thumbnails; + priority = 1350; + extraConfig = '' + if ($request_method = 'OPTIONS') { + ${nginxCommonHeaders} + add_header Access-Control-Max-Age 1728000; + add_header Cache-Control 'no-cache'; + add_header Content-Type 'text/plain charset=UTF-8'; + add_header Content-Length 0; + return 204; + } + + ${nginxCommonHeaders} + add_header Cache-Control 'public, max-age=7200'; + + rewrite ^/static/thumbnails/(.*)$ /$1 break; + ''; + }; + + locations."^~ /static/redundancy/" = { + tryFiles = "$uri @api"; + root = cfg.settings.storage.redundancy; + priority = 1360; + extraConfig = '' + if ($request_method = 'OPTIONS') { + ${nginxCommonHeaders} + add_header Access-Control-Max-Age 1728000; + add_header Content-Type 'text/plain charset=UTF-8'; + add_header Content-Length 0; + return 204; + } + if ($request_method = 'GET') { + ${nginxCommonHeaders} + + access_log off; + } + aio threads; + sendfile on; + sendfile_max_chunk 1M; + + limit_rate_after 5M; + + set $peertube_limit_rate 800k; + set $limit_rate $peertube_limit_rate; + + rewrite ^/static/redundancy/(.*)$ /$1 break; + ''; + }; + + locations."^~ /static/streaming-playlists/" = { + tryFiles = "$uri @api"; + root = cfg.settings.storage.streaming_playlists; + priority = 1370; + extraConfig = '' + if ($request_method = 'OPTIONS') { + ${nginxCommonHeaders} + add_header Access-Control-Max-Age 1728000; + add_header Content-Type 'text/plain charset=UTF-8'; + add_header Content-Length 0; + return 204; + } + if ($request_method = 'GET') { + ${nginxCommonHeaders} + + access_log off; + } + + aio threads; + sendfile on; + sendfile_max_chunk 1M; + + limit_rate_after 5M; + + set $peertube_limit_rate 5M; + set $limit_rate $peertube_limit_rate; + + rewrite ^/static/streaming-playlists/(.*)$ /$1 break; + ''; + }; + + locations."~ ^/static/webseed/" = { + tryFiles = "$uri @api"; + root = cfg.settings.storage.videos; + priority = 1380; + extraConfig = '' + if ($request_method = 'OPTIONS') { + ${nginxCommonHeaders} + add_header Access-Control-Max-Age 1728000; + add_header Content-Type 'text/plain charset=UTF-8'; + add_header Content-Length 0; + return 204; + } + if ($request_method = 'GET') { + ${nginxCommonHeaders} + + access_log off; + } + + aio threads; + sendfile on; + sendfile_max_chunk 1M; + + limit_rate_after 5M; + + set $peertube_limit_rate 800k; + set $limit_rate $peertube_limit_rate; + + rewrite ^/static/webseed/(.*)$ /$1 break; + ''; + }; + }; + }; + services.postgresql = lib.mkIf cfg.database.createLocally { enable = true; }; @@ -476,8 +756,10 @@ in { (lib.mkIf cfg.redis.enableUnixSocket {${config.services.peertube.user}.extraGroups = [ "redis-peertube" ];}) ]; - users.groups = lib.optionalAttrs (cfg.group == "peertube") { - peertube = { }; + users.groups = { + ${cfg.group} = { + members = lib.optional cfg.configureNginx config.services.nginx.user; + }; }; }; } diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix index e1456091717d4..50213ec252ff2 100644 --- a/nixos/modules/services/web-servers/caddy/default.nix +++ b/nixos/modules/services/web-servers/caddy/default.nix @@ -290,6 +290,9 @@ in } ''; + # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size + boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000; + systemd.packages = [ cfg.package ]; systemd.services.caddy = { wants = map (hostOpts: "acme-finished-${hostOpts.useACMEHost}.target") acmeVHosts; diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix index aa782b4267e80..9cbac370612fd 100644 --- a/nixos/modules/services/web-servers/nginx/default.nix +++ b/nixos/modules/services/web-servers/nginx/default.nix @@ -275,7 +275,10 @@ let redirectListen = filter (x: !x.ssl) defaultListen; acmeLocation = optionalString (vhost.enableACME || vhost.useACMEHost != null) '' - location /.well-known/acme-challenge { + # Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx) + # We use ^~ here, so that we don't check any regexes (which could + # otherwise easily override this intended match accidentally). + location ^~ /.well-known/acme-challenge/ { ${optionalString (vhost.acmeFallbackHost != null) "try_files $uri @acme-fallback;"} ${optionalString (vhost.acmeRoot != null) "root ${vhost.acmeRoot};"} auth_basic off; |