1 { config, lib, pkgs, ... }:
31 format = pkgs.formats.json { };
32 cfg = config.services.influxdb2;
33 configFile = format.generate "config.json" cfg.settings;
49 "notificationEndpoints"
60 # Determines whether at least one active api token is defined
62 flip any (attrValues cfg.provision.organizations)
63 (o: o.present && flip any (attrValues o.auths)
64 (a: a.present && a.tokenFile != null));
66 provisionState = pkgs.writeText "provision_state.json" (builtins.toJSON {
67 inherit (cfg.provision) organizations users;
70 influxHost = "http://${escapeShellArg (
71 if ! hasAttr "http-bind-address" cfg.settings
72 || hasInfix "0.0.0.0" cfg.settings.http-bind-address
74 else cfg.settings.http-bind-address
77 waitUntilServiceIsReady = pkgs.writeShellScript "wait-until-service-is-ready" ''
79 export INFLUX_HOST=${influxHost}
81 while ! influx ping &>/dev/null; do
82 if [ "$count" -eq 300 ]; then
83 echo "Tried for 30 seconds, giving up..."
87 if ! kill -0 "$MAINPID"; then
88 echo "Main server died, giving up..."
97 provisioningScript = pkgs.writeShellScript "post-start-provision" ''
99 export INFLUX_HOST=${influxHost}
101 # Do the initial database setup. Pass /dev/null as configs-path to
102 # avoid saving the token as the active config.
103 if test -e "$STATE_DIRECTORY/.first_startup"; then
105 --configs-path /dev/null \
106 --org ${escapeShellArg cfg.provision.initialSetup.organization} \
107 --bucket ${escapeShellArg cfg.provision.initialSetup.bucket} \
108 --username ${escapeShellArg cfg.provision.initialSetup.username} \
109 --password "$(< "$CREDENTIALS_DIRECTORY/admin-password")" \
110 --token "$(< "$CREDENTIALS_DIRECTORY/admin-token")" \
111 --retention ${toString cfg.provision.initialSetup.retention}s \
114 rm -f "$STATE_DIRECTORY/.first_startup"
117 provision_result=$(${getExe pkgs.influxdb2-provision} ${provisionState} "$INFLUX_HOST" "$(< "$CREDENTIALS_DIRECTORY/admin-token")")
118 if [[ "$(jq '[.auths[] | select(.action == "created")] | length' <<< "$provision_result")" -gt 0 ]]; then
119 echo "Created at least one new token, queueing service restart so we can manipulate secrets"
120 touch "$STATE_DIRECTORY/.needs_restart"
124 restarterScript = pkgs.writeShellScript "post-start-restarter" ''
126 if test -e "$STATE_DIRECTORY/.needs_restart"; then
127 rm -f "$STATE_DIRECTORY/.needs_restart"
128 /run/current-system/systemd/bin/systemctl restart influxdb2
132 organizationSubmodule = types.submodule (organizationSubmod: let
133 org = organizationSubmod.config._module.args.name;
137 description = "Whether to ensure that this organization is present or absent.";
142 description = mkOption {
143 description = "Optional description for the organization.";
145 type = types.nullOr types.str;
149 description = "Buckets to provision in this organization.";
151 type = types.attrsOf (types.submodule (bucketSubmod: let
152 bucket = bucketSubmod.config._module.args.name;
156 description = "Whether to ensure that this bucket is present or absent.";
161 description = mkOption {
162 description = "Optional description for the bucket.";
164 type = types.nullOr types.str;
167 retention = mkOption {
168 type = types.ints.unsigned;
170 description = "The duration in seconds for which the bucket will retain data (0 is infinite).";
177 description = "API tokens to provision for the user in this organization.";
179 type = types.attrsOf (types.submodule (authSubmod: let
180 auth = authSubmod.config._module.args.name;
184 description = "A unique identifier for this authentication token. Since influx doesn't store names for tokens, this will be hashed and appended to the description to identify the token.";
186 default = builtins.substring 0 32 (builtins.hashString "sha256" "${org}:${auth}");
187 defaultText = "<a hash derived from org and name>";
192 description = "Whether to ensure that this user is present or absent.";
197 description = mkOption {
199 Optional description for the API token.
200 Note that the actual token will always be created with a descriptionregardless
201 of whether this is given or not. The name is always added plus a unique suffix
202 to later identify the token to track whether it has already been created.
205 type = types.nullOr types.str;
208 tokenFile = mkOption {
209 type = types.nullOr types.path;
211 description = "The token value. If not given, influx will automatically generate one.";
214 operator = mkOption {
215 description = "Grants all permissions in all organizations.";
220 allAccess = mkOption {
221 description = "Grants all permissions in the associated organization.";
226 readPermissions = mkOption {
228 The read permissions to include for this token. Access is usually granted only
229 for resources in the associated organization.
231 Available permissions are `authorizations`, `buckets`, `dashboards`,
232 `orgs`, `tasks`, `telegrafs`, `users`, `variables`, `secrets`, `labels`, `views`,
233 `documents`, `notificationRules`, `notificationEndpoints`, `checks`, `dbrp`,
234 `annotations`, `sources`, `scrapers`, `notebooks`, `remotes`, `replications`.
236 Refer to `influx auth create --help` for a full list with descriptions.
238 `buckets` grants read access to all associated buckets. Use `readBuckets` to define
239 more granular access permissions.
242 type = types.listOf (types.enum validPermissions);
245 writePermissions = mkOption {
247 The read permissions to include for this token. Access is usually granted only
248 for resources in the associated organization.
250 Available permissions are `authorizations`, `buckets`, `dashboards`,
251 `orgs`, `tasks`, `telegrafs`, `users`, `variables`, `secrets`, `labels`, `views`,
252 `documents`, `notificationRules`, `notificationEndpoints`, `checks`, `dbrp`,
253 `annotations`, `sources`, `scrapers`, `notebooks`, `remotes`, `replications`.
255 Refer to `influx auth create --help` for a full list with descriptions.
257 `buckets` grants write access to all associated buckets. Use `writeBuckets` to define
258 more granular access permissions.
261 type = types.listOf (types.enum validPermissions);
264 readBuckets = mkOption {
265 description = "The organization's buckets which should be allowed to be read";
267 type = types.listOf types.str;
270 writeBuckets = mkOption {
271 description = "The organization's buckets which should be allowed to be written";
273 type = types.listOf types.str;
283 services.influxdb2 = {
284 enable = mkEnableOption "the influxdb2 server";
286 package = mkPackageOption pkgs "influxdb2" { };
288 settings = mkOption {
290 description = ''configuration options for influxdb2, see <https://docs.influxdata.com/influxdb/v2.0/reference/config-options> for details.'';
295 enable = mkEnableOption "initial database setup and provisioning";
298 organization = mkOption {
301 description = "Primary organization name";
307 description = "Primary bucket name";
310 username = mkOption {
313 description = "Primary username";
316 retention = mkOption {
317 type = types.ints.unsigned;
319 description = "The duration in seconds for which the bucket will retain data (0 is infinite).";
322 passwordFile = mkOption {
324 description = "Password for primary user. Don't use a file from the nix store!";
327 tokenFile = mkOption {
329 description = "API Token to set for the admin user. Don't use a file from the nix store!";
333 organizations = mkOption {
334 description = "Organizations to provision.";
335 example = literalExpression ''
338 description = "My organization";
340 description = "My bucket";
341 retention = 31536000; # 1 year
344 readBuckets = ["mybucket"];
345 tokenFile = "/run/secrets/mytoken";
351 type = types.attrsOf organizationSubmodule;
355 description = "Users to provision.";
357 example = literalExpression ''
359 # admin = {}; /* The initialSetup.username will automatically be added. */
360 myuser.passwordFile = "/run/secrets/myuser_password";
363 type = types.attrsOf (types.submodule (userSubmod: let
364 user = userSubmod.config._module.args.name;
365 org = userSubmod.config.org;
369 description = "Whether to ensure that this user is present or absent.";
374 passwordFile = mkOption {
375 description = "Password for the user. If unset, the user will not be able to log in until a password is set by an operator! Don't use a file from the nix store!";
377 type = types.nullOr types.path;
386 config = mkIf cfg.enable {
390 assertion = !(hasAttr "bolt-path" cfg.settings) && !(hasAttr "engine-path" cfg.settings);
391 message = "services.influxdb2.config: bolt-path and engine-path should not be set as they are managed by systemd";
394 ++ flatten (flip mapAttrsToList cfg.provision.organizations (orgName: org:
395 flip mapAttrsToList org.auths (authName: auth:
398 assertion = 1 == count (x: x) [
401 (auth.readPermissions != []
402 || auth.writePermissions != []
403 || auth.readBuckets != []
404 || auth.writeBuckets != [])
406 message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: The `operator` and `allAccess` options are mutually exclusive with each other and the granular permission settings.";
408 (let unknownBuckets = subtractLists (attrNames org.buckets) auth.readBuckets; in {
409 assertion = unknownBuckets == [];
410 message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: Refers to invalid buckets in readBuckets: ${toString unknownBuckets}";
412 (let unknownBuckets = subtractLists (attrNames org.buckets) auth.writeBuckets; in {
413 assertion = unknownBuckets == [];
414 message = "influxdb2: provision.organizations.${orgName}.auths.${authName}: Refers to invalid buckets in writeBuckets: ${toString unknownBuckets}";
420 services.influxdb2.provision = mkIf cfg.provision.enable {
421 organizations.${cfg.provision.initialSetup.organization} = {
422 buckets.${cfg.provision.initialSetup.bucket} = {
423 inherit (cfg.provision.initialSetup) retention;
426 users.${cfg.provision.initialSetup.username} = {
427 inherit (cfg.provision.initialSetup) passwordFile;
431 systemd.services.influxdb2 = {
432 description = "InfluxDB is an open-source, distributed, time series database";
433 documentation = [ "https://docs.influxdata.com/influxdb/" ];
434 wantedBy = [ "multi-user.target" ];
435 after = [ "network.target" ];
437 INFLUXD_CONFIG_PATH = configFile;
438 ZONEINFO = "${pkgs.tzdata}/share/zoneinfo";
441 Type = "exec"; # When credentials are used with systemd before v257 this is necessary to make the service start reliably (see systemd/systemd#33953)
442 ExecStart = "${cfg.package}/bin/influxd --bolt-path \${STATE_DIRECTORY}/influxd.bolt --engine-path \${STATE_DIRECTORY}/engine";
443 StateDirectory = "influxdb2";
446 CapabilityBoundingSet = "";
447 SystemCallFilter = "@system-service";
449 KillMode = "control-group";
450 Restart = "on-failure";
451 LoadCredential = mkIf cfg.provision.enable [
452 "admin-password:${cfg.provision.initialSetup.passwordFile}"
453 "admin-token:${cfg.provision.initialSetup.tokenFile}"
457 waitUntilServiceIsReady
458 ] ++ (lib.optionals cfg.provision.enable (
459 [provisioningScript] ++
460 # Only the restarter runs with elevated privileges
461 optional anyAuthDefined "+${restarterScript}"
470 # Mark if this is the first startup so postStart can do the initial setup.
471 # Also extract any token secret mappings and apply them if this isn't the first start.
473 tokenPaths = listToAttrs (flatten
474 # For all organizations
475 (flip mapAttrsToList cfg.provision.organizations
476 # For each contained token that has a token file
477 (_: org: flip mapAttrsToList (filterAttrs (_: x: x.tokenFile != null) org.auths)
478 # Collect id -> tokenFile for the mapping
479 (_: auth: nameValuePair auth.id auth.tokenFile))));
480 tokenMappings = pkgs.writeText "token_mappings.json" (builtins.toJSON tokenPaths);
481 in mkIf cfg.provision.enable ''
482 if ! test -e "$STATE_DIRECTORY/influxd.bolt"; then
483 touch "$STATE_DIRECTORY/.first_startup"
485 # Manipulate provisioned api tokens if necessary
486 ${getExe pkgs.influxdb2-token-manipulator} "$STATE_DIRECTORY/influxd.bolt" ${tokenMappings}
491 users.extraUsers.influxdb2 = {
496 users.extraGroups.influxdb2 = {};
499 meta.maintainers = with lib.maintainers; [ nickcao oddlama ];