aboutsummaryrefslogtreecommitdiff
path: root/modules/backup
diff options
context:
space:
mode:
authorMax Audron <audron@cocaine.farm>2026-01-07 15:28:01 +0100
committerMax Audron <audron@cocaine.farm>2026-01-07 15:28:01 +0100
commit84739ac2345265e518a50bc2e9a239eb442e6e22 (patch)
treee289c856e5465f0c713e97a0ba86e1f734c3484e /modules/backup
parentadd kopia module (diff)
setup backups for mail
Diffstat (limited to 'modules/backup')
-rw-r--r--modules/backup/default.nix6
-rw-r--r--modules/backup/maintenance.nix75
-rw-r--r--modules/backup/policy.nix410
-rw-r--r--modules/backup/repositories.nix6
-rw-r--r--modules/backup/snapshot.nix40
-rw-r--r--modules/backup/web.nix4
6 files changed, 325 insertions, 216 deletions
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";
};
};