From 6648ab0f79083aa34f7233f97f980a28c2ae7590 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 16 May 2023 15:49:06 -0500 Subject: [PATCH] first --- .gitignore | 1 + flake.lock | 60 +++++ flake.nix | 765 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 826 insertions(+) create mode 100644 .gitignore create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6944e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/result* diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6fa896c --- /dev/null +++ b/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1681202837, + "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1684215771, + "narHash": "sha256-fsum28z+g18yreNa1Y7MPo9dtps5h1VkHfZbYQ+YPbk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "963006aab35e3e8ebbf6052b6bf4ea712fdd3c28", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..5bc3f5e --- /dev/null +++ b/flake.nix @@ -0,0 +1,765 @@ +{ + description = "Postgresql"; + + inputs = { + nixpkgs = { + url = "nixpkgs/nixos-unstable"; + }; + flake-utils = { + url = "github:numtide/flake-utils"; + }; + }; + outputs = { self, nixpkgs, flake-utils, ... }@inputs: + ( + flake-utils.lib.eachDefaultSystem + (system: + let + pkgs = import nixpkgs { + inherit system; + }; + in + { + packages = { + scram-sha-256 = + let + pname = "scram-sha-256"; + version = "1.0.1"; + in + pkgs.buildGoModule { + inherit pname version; + src = pkgs.fetchFromGitHub { + owner = "supercaracal"; + repo = pname; + rev = "v${version}"; + hash = "sha256-4/6rog+Or7YBer+Gdc38OPC2UVrXmeKUK/hsZrnYhbo="; + }; + vendorHash = "sha256-qNJSCLMPdWgK/eFPmaYBcgH3P6jHBqQeU4gR6kE/+AE="; + meta = with pkgs.lib; { + homepage = "https://github.com/supercaracal/scram-sha-256"; + description = "A backup program that is fast, efficient and secure"; + platforms = platforms.all; + license = licenses.mit; + }; + }; + }; + } + ) + ) // { + nixosModules = { + postgresql = { config, lib, pkgs, ... }: + let + cfg = config.jcollie.postgresql; + in + { + options = { + jcollie.postgresql = lib.options.mkOption { + type = lib.types.submodule { + options = { + enable = lib.options.mkEnableOption "PostgreSQL"; + port = lib.options.mkOption { + type = lib.types.port; + default = 5432; + }; + superuser = lib.options.mkOption { + type = lib.types.submodule { + options = { + username = lib.options.mkOption { + type = lib.types.str; + default = "postgres"; + }; + password = lib.options.mkOption { + type = lib.types.addCheck lib.types.str (x: lib.hasPrefix "SCRAM-SHA-256$" x); + }; + }; + }; + default = { }; + }; + replication = lib.options.mkOption { + type = lib.types.submodule { + options = { + enable = lib.options.mkEnableOption "Enable PostgreSQL replication"; + role = lib.options.mkOption { + type = lib.types.enum [ + "primary" + "replica" + ]; + default = "primary"; + }; + username = lib.options.mkOption { + type = lib.types.str; + default = "replication"; + }; + passwordFile = lib.options.mkOption { + type = lib.types.path; + }; + # password = lib.options.mkOption { + # type = lib.types.str; + # }; + primary = lib.options.mkOption { + type = lib.types.submodule { + options = { + hostname = lib.options.mkOption { + type = lib.types.str; + }; + port = lib.options.mkOption { + type = lib.types.port; + default = 5432; + }; + }; + }; + default = { }; + }; + }; + }; + }; + backup = lib.options.mkOption { + type = lib.types.submodule { + options = { + enable = lib.options.mkEnableOption "Backup the database with PGBackRest"; + processMax = lib.options.mkOption { + type = lib.types.ints.positive; + default = 4; + }; + log = lib.options.mkOption { + type = lib.types.submodule { + options = + let + logLevelType = lib.types.enum [ + "off" + "error" + "warn" + "info" + "detail" + "debug" + "trace" + ]; + in + { + console = lib.options.mkOption { + type = logLevelType; + default = "detail"; + }; + file = lib.options.mkOption { + type = logLevelType; + default = "off"; + }; + stderr = lib.options.mkOption { + type = logLevelType; + default = "off"; + }; + }; + }; + default = { }; + }; + cipher = lib.options.mkOption { + type = lib.types.submodule { + options = { + password = lib.options.mkOption { + type = lib.types.str; + }; + type = lib.options.mkOption { + type = lib.types.enum [ + "aes-256-cbc" + ]; + default = "aes-256-cbc"; + }; + }; + }; + default = { }; + }; + storage = lib.options.mkOption { + type = lib.types.submodule { + options = { + type = lib.options.mkOption { + type = lib.types.enum [ + "azure" + "b2" + ]; + }; + b2 = lib.options.mkOption { + type = lib.types.submodule { + options = { + account_id = lib.options.mkOption { + type = lib.types.str; + }; + account_key = lib.options.mkOption { + type = lib.types.str; + }; + endpoint = lib.options.mkOption { + type = lib.types.str; + }; + region = lib.options.mkOption { + type = lib.types.str; + }; + bucket = lib.options.mkOption { + type = lib.types.str; + }; + path = lib.options.mkOption { + type = lib.types.str; + }; + }; + }; + default = { }; + }; + azure = lib.options.mkOption { + type = lib.types.submodule { + options = { + account_name = lib.options.mkOption { + type = lib.types.str; + }; + account_key = lib.options.mkOption { + type = lib.types.str; + }; + container = lib.options.mkOption { + type = lib.types.str; + }; + path = lib.options.mkOption { + type = lib.types.str; + }; + }; + }; + default = { }; + }; + }; + }; + default = { }; + }; + }; + }; + default = { }; + }; + + healthcheck = lib.options.mkOption { + type = lib.types.submodule { + options = { + enable = lib.options.mkEnableOption "use healthcheck"; + api_url = lib.options.mkOption { + type = lib.types.str; + }; + api_key = lib.options.mkOption { + type = lib.types.str; + }; + timeout = lib.options.mkOption { + type = lib.types.int; + default = 86400; + }; + grace = lib.options.mkOption { + type = lib.types.int; + default = 14400; + }; + }; + }; + default = { }; + }; + + users = lib.options.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + username = lib.options.mkOption { + type = lib.types.str; + }; + password = lib.options.mkOption { + type = lib.types.addCheck lib.types.str (x: lib.hasPrefix "SCRAM-SHA-256$" x); + }; + }; + } + ); + default = [ ]; + }; + + databases = lib.options.mkOption { + type = lib.types.listOf ( + lib.types.submodule { + options = { + name = lib.options.mkOption { + type = lib.types.str; + }; + owner = lib.options.mkOption { + type = lib.types.str; + }; + }; + } + ); + default = [ ]; + }; + }; + }; + default = { }; + }; + }; + + config = + let + toStr = value: + if true == value then "yes" + else if false == value then "no" + else if lib.isString value then "'${lib.replaceStrings ["'"] ["''"] value}'" + else toString value; + + escapeShell = value: lib.replaceStrings [ "$" ] [ "\\$" ] value; + + postgresql = pkgs.postgresql_15.withPackages ( + plugins: [ + plugins.postgis + ] + ); + + rcloneConfig = pkgs.writeTextFile { + name = "rclone.conf"; + text = { + "b2" = '' + [b2] + type = b2 + account = ${cfg.backup.storage.b2.account_id} + key = ${cfg.backup.storage.b2.account_key} + ''; + "azure" = '' + [azure] + type = azureblob + account = ${cfg.backup.storage.azure.account_name} + key = ${cfg.backup.storage.azure.account_key} + ''; + }.${cfg.backup.storage.type}; + }; + + rclone = lib.mkIf (cfg.backup.enable) ( + pkgs.writeShellScriptBin "rclone" '' + exec ${pkgs.rclone}/bin/rclone --config ${rcloneConfig} "''$@" + '' + ); + + dataDir = "/var/lib/postgresql/15"; + + pgbackrestEnvironment = + { + PGBACKREST_LOG_LEVEL_CONSOLE = cfg.backup.log.console; + PGBACKREST_LOG_LEVEL_FILE = cfg.backup.log.file; + PGBACKREST_LOG_LEVEL_STDERR = cfg.backup.log.stderr; + PGBACKREST_PG1_PATH = dataDir; + PGBACKREST_PROCESS_MAX = "${builtins.toString cfg.backup.processMax}"; + PGBACKREST_REPO1_CIPHER_PASS = cfg.backup.cipher.password; + PGBACKREST_REPO1_CIPHER_TYPE = cfg.backup.cipher.type; + PGBACKREST_REPO1_RETENTION_FULL = "14"; + PGBACKREST_REPO1_RETENTION_FULL_TYPE = "time"; + PGBACKREST_STANZA = "${config.networking.hostName}.${config.networking.domain}"; + } // ( + { + "azure" = { + PGBACKREST_REPO1_AZURE_ACCOUNT = cfg.backup.storage.azure.account_name; + PGBACKREST_REPO1_AZURE_CONTAINER = cfg.backup.storage.azure.container; + PGBACKREST_REPO1_AZURE_KEY = cfg.backup.storage.azure.account_key; + PGBACKREST_REPO1_PATH = cfg.backup.storage.azure.path; + PGBACKREST_REPO1_TYPE = "azure"; + }; + "b2" = { + PGBACKREST_REPO1_PATH = cfg.backup.storage.b2.path; + PGBACKREST_REPO1_S3_BUCKET = cfg.backup.storage.b2.bucket; + PGBACKREST_REPO1_S3_ENDPOINT = cfg.backup.storage.b2.endpoint; + PGBACKREST_REPO1_S3_KEY = cfg.backup.storage.b2.account_id; + PGBACKREST_REPO1_S3_KEY_SECRET = cfg.backup.storage.b2.account_key; + PGBACKREST_REPO1_S3_REGION = cfg.backup.storage.b2.region; + PGBACKREST_REPO1_TYPE = "s3"; + }; + }.${cfg.backup.storage.type} + ); + pgbackrest = lib.mkIf (cfg.backup.enable) ( + let + environment = lib.concatStringsSep "\n" ( + lib.mapAttrsToList + ( + n: v: + ''export ${n}="${builtins.toString v}"'' + ) + pgbackrestEnvironment + ); + in + pkgs.writeShellScriptBin "pgbackrest" '' + ${environment} + exec ${pkgs.pgbackrest}/bin/pgbackrest "''$@" + '' + ); + in + lib.mkIf cfg.enable { + environment.systemPackages = [ + postgresql + pgbackrest + rclone + self.packages.${pkgs.system}.scram-sha-256 + ]; + + networking.firewall.allowedTCPPorts = [ cfg.port ]; + + security.acme.certs."${config.networking.hostName}.${config.networking.domain}" = { + reloadServices = [ + "postgresql" + ]; + }; + + users.groups.postgres.gid = config.ids.gids.postgres; + users.users.postgres = { + name = "postgres"; + uid = config.ids.uids.postgres; + group = "postgres"; + description = "PostgreSQL Server"; + home = "/var/lib/postgresql/15"; + useDefaultShell = true; + }; + + systemd.services.postgresql = + let + hbaFile = pkgs.writeTextDir "pg_hba.conf" '' + local all all ident map=default + hostnossl all all all reject + hostssl all all all scram-sha-256 + local replication all ident map=default + hostnossl replication all all reject + hostssl replication all all scram-sha-256 + ''; + identFile = pkgs.writeTextDir "pg_ident.conf" '' + default root postgres + default postgres postgres + ''; + archiveCommand = "${pkgs.pgbackrest}/bin/pgbackrest archive-push %p"; + settings = { + bgwriter_flush_after = "512kB"; + checkpoint_flush_after = "256kB"; + data_directory = dataDir; + datestyle = "iso, mdy"; + default_text_search_config = "pg_catalog.english"; + dynamic_shared_memory_type = "posix"; + hba_file = "${dataDir}/pg_hba.conf"; + hot_standby = "on"; + ident_file = "${dataDir}/pg_ident.conf"; + lc_messages = "en_US.utf8"; + lc_monetary = "en_US.utf8"; + lc_numeric = "en_US.utf8"; + lc_time = "en_US.utf8"; + listen_addresses = "*"; + log_destination = "stderr"; + log_connections = "on"; + log_line_prefix = "%m [%l][%p] {%h} :%d:%u:%c: "; + # log_statement = "all"; + log_timezone = "America/Chicago"; + logging_collector = "off"; + max_connections = 100; + max_wal_senders = "3"; + max_wal_size = "1GB"; + min_wal_size = "80MB"; + password_encryption = "scram-sha-256"; + port = cfg.port; + shared_buffers = "128MB"; + ssl = "on"; + ssl_cert_file = "/run/credentials/postgresql.service/fullchain.pem"; + ssl_key_file = "/run/credentials/postgresql.service/key.pem"; + timezone = "America/Chicago"; + unix_socket_directories = "/run/postgresql"; + wal_level = "replica"; + wal_log_hints = "on"; + } // ( + if (cfg.backup.enable) + then { + archive_command = archiveCommand; + archive_mode = "on"; + } + else + { } + ); + configFile = pkgs.writeTextDir "postgresql.conf" ( + lib.concatStrings ( + lib.mapAttrsToList + ( + n: v: + "${n} = ${toStr v}\n" + ) + settings + ) + ); + backupSetup = + if (cfg.backup.enable) + then + '' + init=''$(${pkgs.pgbackrest}/bin/pgbackrest info --output=json | ${pkgs.jq}/bin/jq '.[0].status.code == 0') + if [ "$init" != "true" ] + then + ${postgresql}/bin/pg_ctl -o "-c ssl=off -c listen_addresses=''' -p 55432" -w start + ${pkgs.pgbackrest}/bin/pgbackrest --pg1-port=55432 stanza-create + ${postgresql}/bin/pg_ctl -m fast -w stop + fi + '' + else + ""; + in + { + description = "PostgreSQL Server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + environment = { + PGDATA = dataDir; + } // ( + if (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) + then + pgbackrestEnvironment + else { } + ) // ( + if (cfg.replication.enable && cfg.replication.role == "replica") + then { + PGPASSFILE = "${dataDir}/.pgpass"; + } + else { } + ); + path = [ + postgresql + pgbackrest + ]; + + preStart = + if (!cfg.replication.enable || cfg.replication.role == "primary") + then + '' + if [ ! -s "${dataDir}/PG_VERSION" ] + then + ${postgresql}/bin/initdb \ + --username=${cfg.superuser.username} \ + --pwfile=<(echo -n "${escapeShell cfg.superuser.password}") \ + --data-checksums \ + --locale=en_US.utf8 \ + --auth-host=scram-sha-256 \ + --auth-local=trust + fi + + ln -sfn "${configFile}/postgresql.conf" "${dataDir}/postgresql.conf" + ln -sfn "${hbaFile}/pg_hba.conf" "${dataDir}/pg_hba.conf" + ln -sfn "${identFile}/pg_ident.conf" "${dataDir}/pg_ident.conf" + + ${backupSetup} + '' + else + '' + read -r password < ${cfg.replication.passwordFile} + (umask 077; echo "${cfg.replication.primary.hostname}:${builtins.toString cfg.replication.primary.port}:replication:${cfg.replication.username}:''${password}" > ${dataDir}/.pgpass) + chmod 0600 ${dataDir}/.pgpass + + if [ ! -s "${dataDir}/PG_VERSION" ] + then + ${postgresql}/bin/pg_basebackup \ + --verbose \ + --progress \ + --pgdata=${dataDir} \ + --write-recovery-conf \ + --slot=${config.networking.hostName} \ + --create-slot \ + --wal-method=stream \ + --host=${cfg.replication.primary.hostname} \ + --port=${builtins.toString cfg.replication.primary.port} \ + --username=${cfg.replication.username} \ + --no-password + fi + + ln -sfn "${configFile}/postgresql.conf" "${dataDir}/postgresql.conf" + ln -sfn "${hbaFile}/pg_hba.conf" "${dataDir}/pg_hba.conf" + ln -sfn "${identFile}/pg_ident.conf" "${dataDir}/pg_ident.conf" + ''; + + unitConfig = { + RequiresMountsFor = "${dataDir}"; + }; + serviceConfig = { + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + ExecStart = "${postgresql}/bin/postgres"; + Group = "postgres"; + KillMode = "mixed"; + KillSignal = "SIGINT"; + RuntimeDirectory = "postgresql"; + StateDirectory = "postgresql"; + StateDirectoryMode = "0750"; + TimeoutSec = 120; + Type = "notify"; + User = "postgres"; + LoadCredential = [ + "fullchain.pem:${config.security.acme.certs."${config.networking.hostName}.${config.networking.domain}".directory}/fullchain.pem" + "key.pem:${config.security.acme.certs."${config.networking.hostName}.${config.networking.domain}".directory}/key.pem" + ]; + }; + }; + + systemd.services.postgresql-setup = lib.mkIf (!cfg.replication.enable || cfg.replication.role == "primary") ( + let + replicationSetup = + if (cfg.replication.enable && cfg.replication.role == "primary") then + '' + if $PSQL --command "SELECT 1 FROM pg_roles WHERE rolname='${cfg.replication.username}';" | grep -q 1 + then + echo "alter replication user ${cfg.replication.username}" + $PSQL \ + --variable=v1="''$(<''${CREDENTIALS_DIRECTORY}/postgresql-replication-password)" \ + --command "ALTER ROLE ${cfg.replication.username} WITH REPLICATION LOGIN PASSWORD :'v1';" + else + echo "create replication user ${cfg.replication.username}" + $PSQL \ + --variable=v1="''$(<''${CREDENTIALS_DIRECTORY}/postgresql-replication-password)" \ + --command "CREATE ROLE ${cfg.replication.username} WITH REPLICATION LOGIN PASSWORD :'v1';" + fi + '' + else ""; + userSetup = lib.strings.concatStringsSep "\n" + ( + map + ( + user: + '' + if $PSQL --command "SELECT 1 FROM pg_roles WHERE rolname='${user.username}';" | grep -q 1 + then + echo "alter user ${user.username}" + $PSQL --command "ALTER ROLE ${user.username} WITH LOGIN PASSWORD '${escapeShell user.password}';" + else + echo "create user ${user.username}" + $PSQL --command "CREATE ROLE ${user.username} WITH LOGIN PASSWORD '${escapeShell user.password}';" + fi + '' + ) + cfg.users + ); + databaseSetup = lib.strings.concatStringsSep "\n" + ( + map + ( + database: + '' + if ! ( $PSQL --command "SELECT 1 FROM pg_database WHERE datname='${database.name}';" | grep -q 1 ) + then + echo "create database ${database.name}" + $PSQL --command "CREATE DATABASE ${database.name} WITH OWNER ${database.owner};" + fi + + echo "grant public schema priviliges to user ${database.owner}" + $PSQL --dbname ${database.name} --command "GRANT ALL PRIVILEGES ON SCHEMA public to ${database.owner};" + echo "grant priviliges on database ${database.name} to user ${database.owner}" + $PSQL --dbname ${database.name} --command "GRANT ALL PRIVILEGES ON DATABASE ${database.name} to ${database.owner};" + '' + ) + cfg.databases + ); + in + { + description = "PostgreSQL User/Database Setup"; + requiredBy = [ "postgresql.service" ]; + bindsTo = [ "postgresql.service" ]; + script = '' + while ! ${postgresql}/bin/psql -d postgres -c "" 2> /dev/null + do + sleep 0.1 + done + + PSQL="${postgresql}/bin/psql --tuples-only --no-align" + + $PSQL --command "ALTER ROLE ${cfg.superuser.username} WITH SUPERUSER LOGIN PASSWORD '${escapeShell cfg.superuser.password}';" + + ${replicationSetup} + ${userSetup} + ${databaseSetup} + ''; + serviceConfig = { + Type = "oneshot"; + User = "postgres"; + Group = "postgres"; + RemainAfterExit = true; + TimeoutSec = 120; + LoadCredential = [ + "postgresql-replication-password:${cfg.replication.passwordFile}" + ]; + }; + } + ); + + systemd.services.postgresql-backup-full = lib.mkIf (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) { + description = "PostgreSQL Full Backup"; + requires = [ "postgresql.service" ]; + script = '' + while ! ${postgresql}/bin/psql -d postgres -c "" 2> /dev/null + do + sleep 0.1 + done + + ${pkgs.pgbackrest}/bin/pgbackrest --type=full --start-fast --stop-auto --delta backup + ''; + environment = pgbackrestEnvironment; + serviceConfig = { + Type = "oneshot"; + User = "postgres"; + Group = "postgres"; + TimeoutSec = 3600; + }; + }; + + systemd.timers.postgresql-backup-full = lib.mkIf (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) { + description = "PostgreSQL Full Backup"; + timerConfig = { + OnCalendar = "Sun *-*-* 02:00:00"; + RandomizedDelaySec = "5m"; + }; + wantedBy = [ "multi-user.target" ]; + }; + + systemd.services.postgresql-backup-diff = lib.mkIf (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) { + description = "PostgreSQL Differential Backup"; + requires = [ "postgresql.service" ]; + script = '' + while ! ${postgresql}/bin/psql -d postgres -c "" 2> /dev/null + do + sleep 0.1 + done + + ${pkgs.pgbackrest}/bin/pgbackrest --type=diff --start-fast --stop-auto --delta backup + ''; + environment = pgbackrestEnvironment; + serviceConfig = { + Type = "oneshot"; + User = "postgres"; + Group = "postgres"; + TimeoutSec = 3600; + }; + }; + + systemd.timers.postgresql-backup-diff = lib.mkIf (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) { + description = "PostgreSQL Differential Backup"; + timerConfig = { + OnCalendar = "Mon,Tue,Wed,Thu,Fri,Sat *-*-* 02:00:00"; + RandomizedDelaySec = "5m"; + }; + wantedBy = [ "multi-user.target" ]; + }; + + systemd.services.postgresql-backup-incr = lib.mkIf (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) { + description = "PostgreSQL Incremental Backup"; + requires = [ "postgresql.service" ]; + script = '' + while ! ${postgresql}/bin/psql -d postgres -c "" 2> /dev/null + do + sleep 0.1 + done + + ${pkgs.pgbackrest}/bin/pgbackrest --type=incr --start-fast --stop-auto --delta backup + ''; + environment = pgbackrestEnvironment; + serviceConfig = { + Type = "oneshot"; + User = "postgres"; + Group = "postgres"; + TimeoutSec = 3600; + }; + }; + + systemd.timers.postgresql-backup-incr = lib.mkIf (cfg.backup.enable && (!cfg.replication.enable || cfg.replication.role == "primary")) { + description = "PostgreSQL Incremental Backup"; + timerConfig = { + OnCalendar = "*-*-* 06,10,14,18,22:00:00"; + RandomizedDelaySec = "5m"; + }; + wantedBy = [ "multi-user.target" ]; + }; + }; + }; + }; + }; +}