summaryrefslogtreecommitdiff
path: root/modules/borgbackup
diff options
context:
space:
mode:
Diffstat (limited to 'modules/borgbackup')
-rw-r--r--modules/borgbackup/btrfs-snapshots.nix52
-rw-r--r--modules/borgbackup/default.nix199
-rw-r--r--modules/borgbackup/lvm-snapshots.nix133
3 files changed, 384 insertions, 0 deletions
diff --git a/modules/borgbackup/btrfs-snapshots.nix b/modules/borgbackup/btrfs-snapshots.nix
new file mode 100644
index 00000000..96d2b2ba
--- /dev/null
+++ b/modules/borgbackup/btrfs-snapshots.nix
@@ -0,0 +1,52 @@
1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.btrfs-snapshots;
7
8 snapshotMount = str: "${str}${cfg.mountSuffix}";
9in {
10 options = {
11
12 services.btrfs-snapshots = {
13 enable = mkEnableOption "a systemd unit for btrfs snapshots";
14
15 mountSuffix = mkOption {
16 type = types.str;
17 default = ".snapshot";
18 };
19
20 readOnly = mkOption {
21 type = types.bool;
22 default = true;
23 };
24
25 persist = mkOption {
26 type = types.bool;
27 default = false;
28 };
29 };
30
31 };
32
33
34 config = mkIf cfg.enable {
35 systemd.services."btrfs-snapshot@" = {
36 enable = true;
37
38 unitConfig = {
39 StopWhenUnneeded = !cfg.persist;
40 };
41
42 serviceConfig = with pkgs; {
43 Type = "oneshot";
44 ExecStartPre = "-${btrfs-progs}/bin/btrfs subvolume delete -c ${snapshotMount "%f"}";
45 ExecStart = "${btrfs-progs}/bin/btrfs subvolume snapshot ${optionalString cfg.readOnly "-r"} %f ${snapshotMount "%f"}";
46 RemainAfterExit = true;
47 ExecStop = "${btrfs-progs}/bin/btrfs subvolume delete -c ${snapshotMount "%f"}";
48 };
49 };
50
51 };
52}
diff --git a/modules/borgbackup/default.nix b/modules/borgbackup/default.nix
new file mode 100644
index 00000000..47f8e06d
--- /dev/null
+++ b/modules/borgbackup/default.nix
@@ -0,0 +1,199 @@
1{ config, lib, utils, pkgs, ... }:
2
3with utils;
4with lib;
5
6let
7 cfg = config.services.borgbackup;
8
9 lvmPath = {
10 options = {
11 LV = mkOption {
12 type = types.str;
13 };
14 VG = mkOption {
15 type = types.str;
16 };
17 };
18 };
19
20 pathType = if cfg.snapshots == "lvm" then types.submodule lvmPath else types.path;
21
22 systemdPath = path: escapeSystemdPath (if cfg.snapshots == "lvm" then "${path.VG}-${path.LV}" else path);
23
24 withSuffix = path: path + (if cfg.snapshots == "btrfs" then config.services.btrfs-snapshots.mountSuffix else config.services.lvm-snapshots.mountSuffix);
25
26 mountPoint = if cfg.snapshots == "lvm" then config.services.lvm-snapshots.mountPoint else "";
27
28 targetOptions = {
29 options = {
30 repo = mkOption {
31 type = types.str;
32 };
33
34 paths = mkOption {
35 type = types.listOf pathType;
36 default = [];
37 };
38
39 prune = mkOption {
40 type = types.attrsOf (types.listOf types.str);
41 default = {};
42 };
43
44 interval = mkOption {
45 type = types.str;
46 default = "6h";
47 };
48
49 jitter = mkOption {
50 type = with types; nullOr str;
51 default = "6h";
52 };
53
54 lock = mkOption {
55 type = types.nullOr types.str;
56 default = "backup";
57 };
58
59 network = mkOption {
60 type = types.bool;
61 default = true;
62 };
63
64 lockWait = mkOption {
65 type = types.int;
66 default = 600;
67 };
68 };
69 };
70in {
71 disabledModules = [ "services/backup/borgbackup.nix" ];
72
73 options = {
74 services.borgbackup = {
75 snapshots = mkOption {
76 type = types.nullOr (types.enum ["btrfs" "lvm"]);
77 default = null;
78 };
79
80 targets = mkOption {
81 type = types.attrsOf (types.submodule targetOptions);
82 default = {};
83 };
84
85 prefix = mkOption {
86 type = types.str;
87 };
88 };
89 };
90
91 imports =
92 [ ./lvm-snapshots.nix
93 ./btrfs-snapshots.nix
94 ];
95
96 config = mkIf (any (t: t.paths != []) (attrValues cfg.targets)) {
97 services.btrfs-snapshots.enable = mkIf (cfg.snapshots == "btrfs") true;
98
99 services.lvm-snapshots.snapshots = mkIf (cfg.snapshots == "lvm") (listToAttrs (map (path: nameValuePair (path.VG + "-" + path.LV) {
100 inherit (path) LV VG;
101 mountName = withSuffix (path.VG + "-" + path.LV);
102 }) (unique (flatten (mapAttrsToList (target: tCfg: tCfg.paths) cfg.targets)))));
103
104 systemd.targets."timers-borg" = {
105 wantedBy = [ "timers.target" ];
106 };
107
108 systemd.slices."system-borgbackup" = {};
109
110 systemd.timers = (listToAttrs (map ({ target, path, tCfg }: nameValuePair "borgbackup-${target}@${systemdPath path}" {
111 requiredBy = [ "timers-borg.target" ];
112
113 timerConfig = {
114 Persistent = false;
115 OnBootSec = tCfg.interval;
116 OnUnitActiveSec = tCfg.interval;
117 RandomizedDelaySec = mkIf (tCfg.jitter != null) tCfg.jitter;
118 };
119 }) (flatten (mapAttrsToList (target: tCfg: map (path: { inherit target path tCfg; }) tCfg.paths) cfg.targets)))) // (mapAttrs' (target: tCfg: nameValuePair "borgbackup-prune-${target}" {
120 enable = tCfg.prune != {};
121
122 requiredBy = [ "timers-borg.target" ];
123
124 timerConfig = {
125 Persistent = false;
126 OnBootSec = tCfg.interval;
127 OnUnitActiveSec = tCfg.interval;
128 RandomizedDelaySec = mkIf (tCfg.jitter != null) tCfg.jitter;
129 };
130 }) cfg.targets);
131
132 systemd.services = (mapAttrs' (target: tCfg: nameValuePair "borgbackup-${target}@" (let
133 deps = flatten [
134 (optional (cfg.snapshots == "btrfs") "btrfs-snapshot@%i.service")
135 (optional tCfg.network "network-online.target")
136 ];
137 in {
138 bindsTo = deps;
139 after = deps;
140
141 path = with pkgs; [borgbackup] ++ optional (tCfg.lock != null) utillinux;
142
143 script = let
144 borgCmd = ''
145 borg create \
146 --lock-wait ${toString tCfg.lockWait} \
147 --stats \
148 --list \
149 --filter 'AME' \
150 --exclude-caches \
151 --keep-exclude-tags \
152 --patterns-from .backup-${target} \
153 --one-file-system \
154 --compression auto,lzma \
155 ${tCfg.repo}::${cfg.prefix}$1-{utcnow}
156 '';
157 in if tCfg.lock == null then borgCmd else "flock -xo /var/lock/${tCfg.lock} ${borgCmd}";
158 scriptArgs = if cfg.snapshots == "lvm" then "%I" else "%i";
159
160 unitConfig = {
161 AssertPathIsDirectory = mkIf (tCfg.lock != null) "/var/lock";
162 DefaultDependencies = false;
163 RequiresMountsFor = mkIf (cfg.snapshots == "lvm") [ "${mountPoint}/${withSuffix "%I"}" ];
164 };
165
166 serviceConfig = {
167 Type = "oneshot";
168 WorkingDirectory = if (cfg.snapshots == null) then "%I" else (if (cfg.snapshots == "lvm") then "${mountPoint}/${withSuffix "%I"}" else "${withSuffix "%f"}");
169 Nice = 15;
170 IOSchedulingClass = 2;
171 IOSchedulingPriority = 7;
172 SuccessExitStatus = [1 2];
173 Slice = "system-borgbackup.slice";
174 };
175 })) cfg.targets) // (mapAttrs' (target: tCfg: nameValuePair "borgbackup-prune-${target}" {
176 enable = tCfg.prune != {};
177
178 bindsTo = ["network-online.target"];
179 after = ["network-online.target"];
180
181 path = with pkgs; [borgbackup];
182
183 script = concatStringsSep "\n" (mapAttrsToList (path: args: ''
184 borg prune \
185 --lock-wait ${toString tCfg.lockWait} \
186 --list \
187 --stats \
188 --prefix ${escapeShellArg "${cfg.prefix}${path}"} \
189 ${escapeShellArgs args} \
190 ${tCfg.repo}
191 '') tCfg.prune);
192
193 serviceConfig = {
194 Type = "oneshot";
195 Slice = "system-borgbackup.slice";
196 };
197 }) cfg.targets);
198 };
199}
diff --git a/modules/borgbackup/lvm-snapshots.nix b/modules/borgbackup/lvm-snapshots.nix
new file mode 100644
index 00000000..9b2a6562
--- /dev/null
+++ b/modules/borgbackup/lvm-snapshots.nix
@@ -0,0 +1,133 @@
1{ config, lib, utils, pkgs, ... }:
2
3with utils;
4with lib;
5
6let
7 cfg = config.services.lvm-snapshots;
8
9 snapshotMount = name: "${cfg.mountPoint}/${if isNull cfg.snapshots."${name}".mountName then name else cfg.snapshots."${name}".mountName}";
10 snapshotName = name: "${name}-${cfg.mountSuffix}";
11
12 snapshotConfig = {
13 options = {
14 LV = mkOption {
15 type = types.str;
16 };
17
18 VG = mkOption {
19 type = types.str;
20 };
21
22 mountName = mkOption {
23 type = types.nullOr types.str;
24 default = null;
25 };
26
27 cowSize = mkOption {
28 type = types.str;
29 default = "-l20%ORIGIN";
30 };
31
32 readOnly = mkOption {
33 type = types.bool;
34 default = true;
35 };
36
37 persist = mkOption {
38 type = types.bool;
39 default = false;
40 };
41 };
42 };
43in {
44 options = {
45
46 services.lvm-snapshots = {
47 snapshots = mkOption {
48 type = types.attrsOf (types.submodule snapshotConfig);
49 default = {};
50 };
51
52 mountPoint = mkOption {
53 type = types.path;
54 default = "/mnt";
55 };
56
57 mountSuffix = mkOption {
58 type = types.str;
59 default = "-snapshot";
60 };
61 };
62 };
63
64
65 config = mkIf (cfg != {}) {
66
67 boot.kernelModules = [ "dm_snapshot" ];
68
69 # system.activationScripts = mapAttrs' (name: scfg: nameValuePair ("lvm-mountpoint" + name) ''
70 # mkdir -p ${snapshotMount name}
71 # '') cfg.snapshots;
72
73 systemd.services = mapAttrs' (name: scfg: nameValuePair ("lvm-snapshot@" + escapeSystemdPath name) {
74 enable = true;
75
76 description = "LVM-snapshot of ${scfg.VG}/${scfg.LV}";
77
78 bindsTo = ["${escapeSystemdPath "/dev/${scfg.VG}/${scfg.LV}"}.device"];
79 after = ["${escapeSystemdPath "/dev/${scfg.VG}/${scfg.LV}"}.device"];
80
81 unitConfig = {
82 StopWhenUnneeded = !scfg.persist;
83 AssertPathIsDirectory = "/var/lock";
84 };
85
86 path = with pkgs; [ devicemapper utillinux ];
87
88 script = ''
89 (
90 flock -xn -E 4 9
91 if [[ "$?" -ne 0 ]]; then
92 exit $?
93 fi
94
95 lvcreate -s ${scfg.cowSize} --name ${snapshotName name} ${scfg.VG}/${scfg.LV}
96
97 sleep infinity &
98 ) 9>/var/lock/lvm-snapshot.${scfg.VG}
99 '';
100
101 preStart = ''
102 lvremove -f ${scfg.VG}/${snapshotName name}
103 '';
104
105 preStop = ''
106 lvremove -f ${scfg.VG}/${snapshotName name}
107 '';
108
109 serviceConfig = with pkgs; {
110 Type = "forking";
111 RestartForceExitStatus = [ "4" ];
112 RestartSec = "5min";
113 };
114 }) cfg.snapshots;
115
116 systemd.mounts = mapAttrsToList (name: scfg: {
117 enable = true;
118
119 unitConfig = {
120 # AssertPathIsDirectory = snapshotMount name;
121 StopWhenUnneeded = !scfg.persist;
122 };
123
124 bindsTo = [ ("lvm-snapshot@" + escapeSystemdPath name + ".service") ];
125 after = [ ("lvm-snapshot@" + escapeSystemdPath name + ".service") ];
126
127 options = concatStringsSep "," ([ "noauto" ] ++ optional scfg.readOnly "ro");
128
129 where = snapshotMount name;
130 what = "/dev/" + scfg.VG + "/" + snapshotName name;
131 }) cfg.snapshots;
132 };
133}