diff options
| author | Max Audron <audron@cocaine.farm> | 2026-01-07 15:28:01 +0100 |
|---|---|---|
| committer | Max Audron <audron@cocaine.farm> | 2026-01-07 15:28:01 +0100 |
| commit | 84739ac2345265e518a50bc2e9a239eb442e6e22 (patch) | |
| tree | e289c856e5465f0c713e97a0ba86e1f734c3484e | |
| parent | add kopia module (diff) | |
setup backups for mail
| -rw-r--r-- | flake.nix | 1 | ||||
| -rw-r--r-- | machines/mail/default.nix | 85 | ||||
| -rw-r--r-- | modules/backup/default.nix | 6 | ||||
| -rw-r--r-- | modules/backup/maintenance.nix | 75 | ||||
| -rw-r--r-- | modules/backup/policy.nix | 410 | ||||
| -rw-r--r-- | modules/backup/repositories.nix | 6 | ||||
| -rw-r--r-- | modules/backup/snapshot.nix | 40 | ||||
| -rw-r--r-- | modules/backup/web.nix | 4 | ||||
| m--------- | secrets | 0 |
9 files changed, 398 insertions, 229 deletions
@@ -133,6 +133,7 @@ overlays common + backup users wireguard crypto diff --git a/machines/mail/default.nix b/machines/mail/default.nix index 36de86e..a08d419 100644 --- a/machines/mail/default.nix +++ b/machines/mail/default.nix @@ -1,6 +1,12 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: -let endpoint = "2a01:4f8:1c1c:3ce7::1"; +let + endpoint = "116.203.26.228"; in { networking = { @@ -10,21 +16,36 @@ in interfaces.eth0 = { ipv4 = { addresses = [ - { address="116.203.26.228"; prefixLength=32; } + { + address = "116.203.26.228"; + prefixLength = 32; + } ]; routes = [ - { address = "172.31.1.1"; prefixLength = 32; } + { + address = "172.31.1.1"; + prefixLength = 32; + } ]; }; ipv6 = { addresses = [ - { address="2a01:4f8:1c1c:3ce7::1"; prefixLength=64; } - { address="fe80::9000:6ff:fe53:14ce"; prefixLength=64; } + { + address = "2a01:4f8:1c1c:3ce7::1"; + prefixLength = 64; + } + { + address = "fe80::9000:6ff:fe53:14ce"; + prefixLength = 64; + } ]; - + routes = [ - { address = "fe80::1"; prefixLength = 128; } + { + address = "fe80::1"; + prefixLength = 128; + } ]; }; }; @@ -36,16 +57,54 @@ in }; }; - pubKey = - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPVwt+/sB77NZnjwqgwtkcqKsIYyMnYh5qlqYoY9dLEd"; + pubKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPVwt+/sB77NZnjwqgwtkcqKsIYyMnYh5qlqYoY9dLEd"; wireguard = { enable = true; inherit endpoint; - v4 = { address = "10.10.0.6"; }; - v6 = { address = "6"; }; + v4 = { + address = "10.10.0.6"; + }; + v6 = { + address = "6"; + }; publicKey = "lk0mN1R5Uf5iwvWe/4mOmrMap7xtsieQaJSHcXQ7+VY="; - allowedIPs = []; + allowedIPs = [ ]; + }; + + services.kopia = { + enable = true; + instances = { + b2 = { + enable = true; + environmentFile = config.secrets.b2.dest; + repository.b2.bucket = "mail-vapor-systems"; + + snapshots = { + schedule = "daily"; + paths = [ + "/var/lib/stalwart-mail" + ]; + }; + + policy = [{ + retention = { + keepLatest = 5; + keepDaily = 30; + keepWeekly = 4; + keepMonthly = 3; + keepAnnual = 0; + }; + }]; + }; + }; + }; + + secrets = { + b2 = { + source = ../../secrets/backup/mail.vapor.systems.env; + dest = "/etc/secrets/b2.env"; + }; }; deploy = { diff --git a/modules/backup/default.nix b/modules/backup/default.nix index ed319f2..9b2ec57 100644 --- a/modules/backup/default.nix +++ b/modules/backup/default.nix @@ -20,8 +20,7 @@ let }; }; mkInstanceServices = - instances: - serviceCreator: + instances: serviceCreator: lib.pipe instances [ (lib.attrsets.mapAttrs' serviceCreator) (lib.recursiveUpdate { }) @@ -33,6 +32,7 @@ in _module.args.mkInstanceServices = mkInstanceServices; imports = [ ./repositories.nix + ./maintenance.nix ./snapshot.nix ./policy.nix ./web.nix @@ -47,4 +47,6 @@ in default = { }; }; }; + + config.environment.systemPackages = [ pkgs.kopia ]; } diff --git a/modules/backup/maintenance.nix b/modules/backup/maintenance.nix new file mode 100644 index 0000000..5f6c971 --- /dev/null +++ b/modules/backup/maintenance.nix @@ -0,0 +1,75 @@ +{ + config, + lib, + pkgs, + mkInstanceServices, + ... +}: +let + instanceType = lib.types.submodule { + options = { + maintenance = { + schedule = lib.mkOption { + type = lib.types.str; + default = "*-*-* 6:00:00"; + description = "kopia full maintenance schedule"; + }; + }; + }; + }; +in +{ + options.services.kopia.instances = lib.mkOption { + type = lib.types.attrsOf instanceType; + }; + + config = lib.mkIf config.services.kopia.enable ( + let + mkMaintenanceService = + name: instance: + lib.attrsets.nameValuePair "kopia-maintenance-${name}" { + description = "Kopia ${name} maintenance service"; + wants = [ + "kopia-repository-${name}.service" + ]; + after = [ "kopia-repository-${name}.service" ]; + conflicts = [ "kopia-snapshot-${name}.service" ]; + script = '' + ${pkgs.kopia}/bin/kopia maintenance run --full + ''; + serviceConfig = { + Type = "simple"; + User = "${instance.user}"; + WorkingDirectory = "~"; + SetLoginEnvironment = true; + # retry on failure + Restart = "on-failure"; + # wait 30 seconds before restarting + RestartSec = "30"; + # lower priority + Nice = "-19"; + IOSchedulingClass = "idle"; + }; + unitConfig = { + # limit the number of restarts to 5 in 1 day + StartLimitInterval = "1d"; + StartLimitBurst = "5"; + }; + }; + mkMaintenanceTimer = + name: instance: + lib.attrsets.nameValuePair "kopia-maintenance-${name}" { + description = "Kopia ${name} maintenance timer"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = instance.maintenance.schedule; + }; + }; + in + { + # systemd service for repositories open + systemd.services = mkInstanceServices config.services.kopia.instances mkMaintenanceService; + systemd.timers = mkInstanceServices config.services.kopia.instances mkMaintenanceTimer; + } + ); +} diff --git a/modules/backup/policy.nix b/modules/backup/policy.nix index 5486cac..ab7fb7d 100644 --- a/modules/backup/policy.nix +++ b/modules/backup/policy.nix @@ -28,221 +28,232 @@ let "zstd-fastest" ]; + policyListType = lib.mkOption { + type = lib.types.listOf policySubmodule; + default = []; + }; + policyType = lib.mkOption { - type = lib.types.nullOr ( - lib.types.submodule { - options = { - retention = { - keepLatest = lib.mkOption { - type = lib.types.int; - default = 5; - description = "Number of latest snapshots to keep."; - }; - keepHourly = lib.mkOption { - type = lib.types.int; - default = 48; - description = "Number of hourly snapshots to keep."; - }; - keepDaily = lib.mkOption { - type = lib.types.int; - default = 7; - description = "Number of daily snapshots to keep."; - }; - keepWeekly = lib.mkOption { - type = lib.types.int; - default = 4; - description = "Number of weekly snapshots to keep."; - }; - keepMonthly = lib.mkOption { - type = lib.types.int; - default = 3; - description = "Number of monthly snapshots to keep."; - }; - keepAnnual = lib.mkOption { - type = lib.types.int; - default = 0; - description = "Number of yearly snapshots to keep."; - }; - }; + type = lib.types.nullOr policySubmodule; + default = null; + }; - files = { - ignoreDotFiles = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ - ".gitignore" - ".kopiaignore" - ]; - description = "List of files to source ignore lists from."; - }; - noParentDotFiles = lib.mkOption { - type = lib.types.nullOr lib.types.bool; - default = false; - description = "Do not use parent ignore dot files."; - }; - ignoreCacheDirs = lib.mkOption { - type = lib.types.nullOr lib.types.bool; - default = false; - description = "Ignore cache directories."; - }; - maxFileSize = lib.mkOption { - type = lib.types.nullOr lib.types.int; - default = null; - description = "Maximum file size to include in backup."; - }; - oneFileSystem = lib.mkOption { - type = lib.types.nullOr lib.types.bool; - default = null; - description = "Stay in parent filesystem when finding files."; - }; - }; + policySubmodule = lib.types.submodule { + options = { + path = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path for which the policy applies"; + }; - errorHandling = { - ignoreFileErrors = lib.mkOption { - type = lib.types.nullOr lib.types.bool; - default = null; - description = "Ignore errors reading ignore files."; - }; - ignoreDirectoryErrors = lib.mkOption { - type = lib.types.nullOr lib.types.bool; - default = null; - description = "Ignore errors reading directories."; - }; - ignoreUnknownTypes = lib.mkOption { - type = lib.types.nullOr lib.types.bool; - default = null; - description = "Ignore unknown file types."; - }; - }; + retention = { + keepLatest = lib.mkOption { + type = lib.types.int; + default = 5; + description = "Number of latest snapshots to keep."; + }; + keepHourly = lib.mkOption { + type = lib.types.int; + default = 48; + description = "Number of hourly snapshots to keep."; + }; + keepDaily = lib.mkOption { + type = lib.types.int; + default = 7; + description = "Number of daily snapshots to keep."; + }; + keepWeekly = lib.mkOption { + type = lib.types.int; + default = 4; + description = "Number of weekly snapshots to keep."; + }; + keepMonthly = lib.mkOption { + type = lib.types.int; + default = 3; + description = "Number of monthly snapshots to keep."; + }; + keepAnnual = lib.mkOption { + type = lib.types.int; + default = 0; + description = "Number of yearly snapshots to keep."; + }; + }; - compression = { - compressorName = lib.mkOption { - type = compressionType; - default = "none"; - description = "Name of the compressor to use."; - }; + files = { + ignoreDotFiles = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + ".gitignore" + ".kopiaignore" + ]; + description = "List of files to source ignore lists from."; + }; + noParentDotFiles = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = false; + description = "Do not use parent ignore dot files."; + }; + ignoreCacheDirs = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = false; + description = "Ignore cache directories."; + }; + maxFileSize = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "Maximum file size to include in backup."; + }; + oneFileSystem = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = null; + description = "Stay in parent filesystem when finding files."; + }; + }; - onlyCompress = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = "List of file extensions to compress."; - }; + errorHandling = { + ignoreFileErrors = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = null; + description = "Ignore errors reading ignore files."; + }; + ignoreDirectoryErrors = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = null; + description = "Ignore errors reading directories."; + }; + ignoreUnknownTypes = lib.mkOption { + type = lib.types.nullOr lib.types.bool; + default = null; + description = "Ignore unknown file types."; + }; + }; - noParentOnlyCompress = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Do not use parent only compress list."; - }; + compression = { + compressorName = lib.mkOption { + type = compressionType; + default = "none"; + description = "Name of the compressor to use."; + }; - neverCompress = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = "List of file extensions to never compress."; - }; + onlyCompress = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of file extensions to compress."; + }; - noParentNeverCompress = lib.mkOption { - type = lib.types.bool; - default = false; - description = "Do not use parent never compress list."; - }; + noParentOnlyCompress = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Do not use parent only compress list."; + }; - minSize = lib.mkOption { - type = lib.types.int; - default = 0; - description = "Minimum file size to compress."; - }; + neverCompress = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "List of file extensions to never compress."; + }; - maxSize = lib.mkOption { - type = lib.types.int; - default = 0; - description = "Maximum file size to compress."; - }; - }; + noParentNeverCompress = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Do not use parent never compress list."; + }; - metadataCompression = { - compressorName = lib.mkOption { - type = compressionType; - default = "zstd-fastest"; - description = "Name of the compressor to use."; - }; - }; + minSize = lib.mkOption { + type = lib.types.int; + default = 0; + description = "Minimum file size to compress."; + }; - splitter = { - algorithm = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - description = "Name of the splitter algorithm to use."; - }; - }; + maxSize = lib.mkOption { + type = lib.types.int; + default = 0; + description = "Maximum file size to compress."; + }; + }; - # FIXME: add action definition afterward (maybe implement it during implement at zfs, btrfs snapshot) + metadataCompression = { + compressorName = lib.mkOption { + type = compressionType; + default = "zstd-fastest"; + description = "Name of the compressor to use."; + }; + }; - osSnapshots = { - volumeShadowCopy = { - enable = lib.mkOption { - type = lib.types.nullOr ( - lib.types.enum [ - "never" - "always" - "when-available" - "inherit" - ] - ); - default = null; - description = "Enable volume shadow copy"; - }; - }; - }; + splitter = { + algorithm = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Name of the splitter algorithm to use."; + }; + }; + + # FIXME: add action definition afterward (maybe implement it during implement at zfs, btrfs snapshot) - logging = { - directories = { - snapshotted = lib.mkOption { - type = lib.types.nullOr lib.types.int; - default = null; - description = "Log detail when a directory is snapshotted"; - }; - ignored = lib.mkOption { - type = lib.types.nullOr lib.types.int; - default = null; - description = "Log detail when a directory is ignored"; - }; - }; + osSnapshots = { + volumeShadowCopy = { + enable = lib.mkOption { + type = lib.types.nullOr ( + lib.types.enum [ + "never" + "always" + "when-available" + "inherit" + ] + ); + default = null; + description = "Enable volume shadow copy"; + }; + }; + }; - entries = { - snapshotted = lib.mkOption { - type = lib.types.nullOr lib.types.int; - default = null; - description = "Log detail when an entry is snapshotted"; - }; - ignored = lib.mkOption { - type = lib.types.nullOr lib.types.int; - default = null; - description = "Log detail when an entry is ignored"; - }; - }; + logging = { + directories = { + snapshotted = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "Log detail when a directory is snapshotted"; }; + ignored = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "Log detail when a directory is ignored"; + }; + }; - upload = { - # maxParallelSnapshots - GUI only - maxParallelFileReads = lib.mkOption { - type = lib.types.nullOr lib.types.int; - default = null; - description = "Maximum number of parallel file reads(GUI Only)"; - }; - parallelUploadAboveSize = lib.mkOption { - type = lib.types.nullOr lib.types.int; - default = null; - description = "Use parallel uploads above size(GUI Only)"; - }; + entries = { + snapshotted = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "Log detail when an entry is snapshotted"; + }; + ignored = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "Log detail when an entry is ignored"; }; }; - } - ); - default = null; + }; + + upload = { + # maxParallelSnapshots - GUI only + maxParallelFileReads = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "Maximum number of parallel file reads(GUI Only)"; + }; + parallelUploadAboveSize = lib.mkOption { + type = lib.types.nullOr lib.types.int; + default = null; + description = "Use parallel uploads above size(GUI Only)"; + }; + }; + }; }; instanceType = lib.types.submodule { options = { - policy = policyType; + policy = policyListType; }; }; @@ -252,18 +263,24 @@ let # generate policy name for policy file generation mkPolicyName = user: hostname: path: - "${user}@${hostname}${if path != "" then ":${path}" else ""}"; + "${user}@${hostname}${if path != null then ":${path}" else ""}"; mkPolicyFile = policy: (jsonFormat.generate "kopia-policy.json" policy); mkInstancePolicyService = name: instance: let - policyName = mkPolicyName instance.user config.networking.hostName instance.path; - policyFile = mkPolicyFile ( - { - "${policyName}" = instance.policy; + policies = lib.lists.foldr ( + policy: a: + a + // { + "${mkPolicyName instance.user config.networking.hostName policy.path}" = lib.attrsets.filterAttrs ( + n: v: n != "path" + ) policy; } + ) { } instance.policy; + policyFile = mkPolicyFile ( + policies // lib.optionalAttrs (config.services.kopia.globalPolicy != null) { "(global)" = config.services.kopia.globalPolicy; } @@ -275,6 +292,7 @@ let wantedBy = [ "kopia-snapshot-${name}.service" ]; after = [ "kopia-repository-${name}.service" ]; before = [ "kopia-snapshot-${name}.service" ]; + restartTriggers = [ policyFile ]; script = '' ${pkgs.kopia}/bin/kopia policy import --from-file=${policyFile} ''; diff --git a/modules/backup/repositories.nix b/modules/backup/repositories.nix index 9ee2b3e..f900b0f 100644 --- a/modules/backup/repositories.nix +++ b/modules/backup/repositories.nix @@ -79,7 +79,7 @@ in ] ++ (lib.optional (instance.repository.s3.disableTLS) "--disable-tls") else if lib.hasAttr "b2" instance.repository then - [ "--bucket" instance.repository.s3.bucket ] + [ "--bucket" instance.repository.b2.bucket ] else throw "Unsupported repository type for Kopia instance ${name}" ); @@ -122,8 +122,8 @@ in }; }; - mkS3Repository = mkTemplateRepository "S3" [ "AWS_ACCESS_KEY_ID" "AWS_SECRET_ACCESS_KEY" ]; - mkB2Repository = mkTemplateRepository "B2" [ "B2_KEY_ID" "B2_KEY" ]; + mkS3Repository = mkTemplateRepository "s3" [ "AWS_ACCESS_KEY_ID" "AWS_SECRET_ACCESS_KEY" ]; + mkB2Repository = mkTemplateRepository "b2" [ "B2_KEY_ID" "B2_KEY" ]; dispatch = { s3 = mkS3Repository; diff --git a/modules/backup/snapshot.nix b/modules/backup/snapshot.nix index 91076cf..a1fba73 100644 --- a/modules/backup/snapshot.nix +++ b/modules/backup/snapshot.nix @@ -6,17 +6,25 @@ ... }: let - instanceType = lib.types.submodule { + snapshotType = lib.types.submodule { options = { - path = lib.mkOption { - type = lib.types.str; - default = "/persistent"; - description = "snapshoted path for kopia instance."; + paths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "snapshoted paths"; }; schedule = lib.mkOption { type = lib.types.str; - default = "daily"; - description = "Snapshot schedule for the Kopia instance."; + default = "*-*-* 4:00:00"; + description = "Snapshot schedule"; + }; + }; + }; + + instanceType = lib.types.submodule { + options = { + snapshots = lib.mkOption { + type = snapshotType; }; }; }; @@ -37,9 +45,11 @@ in "kopia-repository-${name}.service" ]; after = [ "kopia-repository-${name}.service" ]; - script = '' - ${pkgs.kopia}/bin/kopia snapshot create ${instance.path} --description "Snapshot for ${name}" - ''; + script = lib.strings.concatLines ( + lib.lists.forEach instance.snapshots.paths (x: '' + ${pkgs.kopia}/bin/kopia snapshot create ${x} --description "Snapshot for ${name}" + '') + ); serviceConfig = { Type = "simple"; User = "${instance.user}"; @@ -49,13 +59,15 @@ in Restart = "on-failure"; # wait 30 seconds before restarting RestartSec = "30"; - # limit the number of restarts to 5 in 1 day - StartLimitInterval = "1d"; - StartLimitBurst = "5"; # lower priority Nice = "-19"; IOSchedulingClass = "idle"; }; + unitConfig = { + # limit the number of restarts to 5 in 1 day + StartLimitInterval = "1d"; + StartLimitBurst = "5"; + }; }; mkSnapshotTimer = name: instance: @@ -63,7 +75,7 @@ in description = "Kopia S3 snapshot timer"; wantedBy = [ "timers.target" ]; timerConfig = { - OnCalendar = instance.schedule; + OnCalendar = instance.snapshots.schedule; }; }; in diff --git a/modules/backup/web.nix b/modules/backup/web.nix index 9937315..b89d0a9 100644 --- a/modules/backup/web.nix +++ b/modules/backup/web.nix @@ -64,8 +64,10 @@ in Restart = "on-failure"; # wait 30 seconds before restarting RestartSec = "30"; + }; + unitConfig = { # limit the number of restarts to 5 in 1 day - StartLimitIntervalSec = "1d"; + StartLimitInterval = "1d"; StartLimitBurst = "5"; }; }; diff --git a/secrets b/secrets -Subproject c3829d52e773536493c6d12136893c842d13fdd +Subproject ba100958ff3b2295e812a2bd1e8f14e3db0d06c |
