diff options
Diffstat (limited to 'modules/borgbackup')
-rw-r--r-- | modules/borgbackup/btrfs-snapshots.nix | 52 | ||||
-rw-r--r-- | modules/borgbackup/default.nix | 206 | ||||
-rw-r--r-- | modules/borgbackup/lvm-snapshots.nix | 133 | ||||
-rw-r--r-- | modules/borgbackup/repokeys/borg_munin__borg.yaml | 33 |
4 files changed, 424 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 | |||
3 | with lib; | ||
4 | |||
5 | let | ||
6 | cfg = config.services.btrfs-snapshots; | ||
7 | |||
8 | snapshotMount = str: "${str}${cfg.mountSuffix}"; | ||
9 | in { | ||
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..a0419d0e --- /dev/null +++ b/modules/borgbackup/default.nix | |||
@@ -0,0 +1,206 @@ | |||
1 | { config, lib, utils, pkgs, ... }: | ||
2 | |||
3 | with utils; | ||
4 | with lib; | ||
5 | |||
6 | let | ||
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 | keyFile = mkOption { | ||
70 | type = types.nullOr types.path; | ||
71 | default = null; | ||
72 | }; | ||
73 | }; | ||
74 | }; | ||
75 | in { | ||
76 | disabledModules = [ "services/backup/borgbackup.nix" ]; | ||
77 | |||
78 | options = { | ||
79 | services.borgbackup = { | ||
80 | snapshots = mkOption { | ||
81 | type = types.nullOr (types.enum ["btrfs" "lvm"]); | ||
82 | default = null; | ||
83 | }; | ||
84 | |||
85 | targets = mkOption { | ||
86 | type = types.attrsOf (types.submodule targetOptions); | ||
87 | default = {}; | ||
88 | }; | ||
89 | |||
90 | prefix = mkOption { | ||
91 | type = types.str; | ||
92 | }; | ||
93 | }; | ||
94 | }; | ||
95 | |||
96 | imports = | ||
97 | [ ./lvm-snapshots.nix | ||
98 | ./btrfs-snapshots.nix | ||
99 | ]; | ||
100 | |||
101 | config = mkIf (any (t: t.paths != []) (attrValues cfg.targets)) { | ||
102 | services.btrfs-snapshots.enable = mkIf (cfg.snapshots == "btrfs") true; | ||
103 | |||
104 | services.lvm-snapshots.snapshots = mkIf (cfg.snapshots == "lvm") (listToAttrs (map (path: nameValuePair (path.VG + "-" + path.LV) { | ||
105 | inherit (path) LV VG; | ||
106 | mountName = withSuffix (path.VG + "-" + path.LV); | ||
107 | }) (unique (flatten (mapAttrsToList (target: tCfg: tCfg.paths) cfg.targets))))); | ||
108 | |||
109 | systemd.targets."timers-borg" = { | ||
110 | wantedBy = [ "timers.target" ]; | ||
111 | }; | ||
112 | |||
113 | systemd.slices."system-borgbackup" = {}; | ||
114 | |||
115 | systemd.timers = (listToAttrs (map ({ target, path, tCfg }: nameValuePair "borgbackup-${target}@${systemdPath path}" { | ||
116 | requiredBy = [ "timers-borg.target" ]; | ||
117 | |||
118 | timerConfig = { | ||
119 | Persistent = false; | ||
120 | OnBootSec = tCfg.interval; | ||
121 | OnUnitActiveSec = tCfg.interval; | ||
122 | RandomizedDelaySec = mkIf (tCfg.jitter != null) tCfg.jitter; | ||
123 | }; | ||
124 | }) (flatten (mapAttrsToList (target: tCfg: map (path: { inherit target path tCfg; }) tCfg.paths) cfg.targets)))) // (mapAttrs' (target: tCfg: nameValuePair "borgbackup-prune-${target}" { | ||
125 | enable = tCfg.prune != {}; | ||
126 | |||
127 | requiredBy = [ "timers-borg.target" ]; | ||
128 | |||
129 | timerConfig = { | ||
130 | Persistent = false; | ||
131 | OnBootSec = tCfg.interval; | ||
132 | OnUnitActiveSec = tCfg.interval; | ||
133 | RandomizedDelaySec = mkIf (tCfg.jitter != null) tCfg.jitter; | ||
134 | }; | ||
135 | }) cfg.targets); | ||
136 | |||
137 | systemd.services = (mapAttrs' (target: tCfg: nameValuePair "borgbackup-${target}@" (let | ||
138 | deps = flatten [ | ||
139 | (optional (cfg.snapshots == "btrfs") "btrfs-snapshot@%i.service") | ||
140 | (optional tCfg.network "network-online.target") | ||
141 | ]; | ||
142 | in { | ||
143 | bindsTo = deps; | ||
144 | after = deps; | ||
145 | |||
146 | path = with pkgs; [borgbackup] ++ optional (tCfg.lock != null) utillinux; | ||
147 | |||
148 | script = let | ||
149 | borgCmd = '' | ||
150 | borg create \ | ||
151 | --lock-wait ${toString tCfg.lockWait} \ | ||
152 | --stats \ | ||
153 | --list \ | ||
154 | --filter 'AME' \ | ||
155 | --exclude-caches \ | ||
156 | --keep-exclude-tags \ | ||
157 | --patterns-from .backup-${target} \ | ||
158 | --one-file-system \ | ||
159 | --compression auto,lzma \ | ||
160 | ${tCfg.repo}::${cfg.prefix}$1-{utcnow} | ||
161 | ''; | ||
162 | in if tCfg.lock == null then borgCmd else "flock -xo /var/lock/${tCfg.lock} ${borgCmd}"; | ||
163 | scriptArgs = if cfg.snapshots == "lvm" then "%I" else "%i"; | ||
164 | |||
165 | unitConfig = { | ||
166 | AssertPathIsDirectory = mkIf (tCfg.lock != null) "/var/lock"; | ||
167 | DefaultDependencies = false; | ||
168 | RequiresMountsFor = mkIf (cfg.snapshots == "lvm") [ "${mountPoint}/${withSuffix "%I"}" ]; | ||
169 | }; | ||
170 | |||
171 | serviceConfig = { | ||
172 | Type = "oneshot"; | ||
173 | WorkingDirectory = if (cfg.snapshots == null) then "%I" else (if (cfg.snapshots == "lvm") then "${mountPoint}/${withSuffix "%I"}" else "${withSuffix "%f"}"); | ||
174 | Nice = 15; | ||
175 | IOSchedulingClass = 2; | ||
176 | IOSchedulingPriority = 7; | ||
177 | SuccessExitStatus = [1 2]; | ||
178 | Slice = "system-borgbackup.slice"; | ||
179 | Environment = lib.mkIf (tCfg.keyFile != null) "BORG_KEY_FILE=${tCfg.keyFile}"; | ||
180 | }; | ||
181 | })) cfg.targets) // (mapAttrs' (target: tCfg: nameValuePair "borgbackup-prune-${target}" { | ||
182 | enable = tCfg.prune != {}; | ||
183 | |||
184 | bindsTo = ["network-online.target"]; | ||
185 | after = ["network-online.target"]; | ||
186 | |||
187 | path = with pkgs; [borgbackup]; | ||
188 | |||
189 | script = concatStringsSep "\n" (mapAttrsToList (path: args: '' | ||
190 | borg prune \ | ||
191 | --lock-wait ${toString tCfg.lockWait} \ | ||
192 | --list \ | ||
193 | --stats \ | ||
194 | --prefix ${escapeShellArg "${cfg.prefix}${path}"} \ | ||
195 | ${escapeShellArgs args} \ | ||
196 | ${tCfg.repo} | ||
197 | '') tCfg.prune); | ||
198 | |||
199 | serviceConfig = { | ||
200 | Type = "oneshot"; | ||
201 | Slice = "system-borgbackup.slice"; | ||
202 | Environment = lib.mkIf (tCfg.keyFile != null) "BORG_KEY_FILE=${tCfg.keyFile}"; | ||
203 | }; | ||
204 | }) cfg.targets); | ||
205 | }; | ||
206 | } | ||
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 | |||
3 | with utils; | ||
4 | with lib; | ||
5 | |||
6 | let | ||
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 | }; | ||
43 | in { | ||
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 | } | ||
diff --git a/modules/borgbackup/repokeys/borg_munin__borg.yaml b/modules/borgbackup/repokeys/borg_munin__borg.yaml new file mode 100644 index 00000000..f302fe06 --- /dev/null +++ b/modules/borgbackup/repokeys/borg_munin__borg.yaml | |||
@@ -0,0 +1,33 @@ | |||
1 | key: ENC[AES256_GCM,data:mxh+Jtxx+HyD246yPwo0vy7vSTz3IG8VmfbxPMwqJRreh9ZwkGnH5aCTDOvWOHIrkmzaRMF3oCi1P8D29+abMUZdt0MuJ3UE6iL8+SXlflR+WACgALM2Df+x9B3BwQM3yeoCiWG+ebr0iQPHM3jqqpkjoRv1CcythxG2deZueur9lzgC2CwG1g3O8Prnl9z0JQGOa+gjic8Zwfn38B1BECeNPrbjzICGBOrSbN/6EnfBDygI2QzseamzK2I6R6jT+QxHvkl+Zi1m2TRB+4o82VgTjPhIReJyT7PrlDnUyrKObhCOlb3v+LiSdp16IPIDVs968kyDzgyi7QPOpGr+5tutWCZrau5xhPDrONKByl/0nVVwEZfRIYATvEXtn5okJru/mglcpeD0I7AtLt+Vfv9CB9pQczvkHo0cDtgudQDf9ADt/nkmqHugm5VfMg9m9aGbKqzXt6pPOMsXSbS43K7wgDaduLZ/PW4Ookx9gTNLtJHnZ64GBorOv4QSrZIZF8pE1FsQdUhmp/YzVhaNBnjCr+Jh77sYjoOwzF77Xy+VP2C/yVIf492P+FcgkSj6XhYYqHffpFW9l/xmUvyQF5gjj2k5T21UvgChhI1HeLPzQ7W9+xuGSMtg58aD/VPe1loCy8zLITNl71bneararRS5vItoZyzMdmIRMLAZD1klPmDNe1yufTpubOXzNYbWUqFUZtwH/mDL5GRZBD9dqs2b3F26c1CUyw==,iv:NJBHesKSZ1zuKk8qHnYKqIwMnFkH+rkQD1bam5XpLXU=,tag:EiYbIFY/r/eTSTJIhYV+GA==,type:str] | ||
2 | sops: | ||
3 | kms: [] | ||
4 | gcp_kms: [] | ||
5 | azure_kv: [] | ||
6 | hc_vault: [] | ||
7 | lastmodified: '2021-01-02T20:38:48Z' | ||
8 | mac: ENC[AES256_GCM,data:3rkFTOk3r2dx3hOqu1u7XIIibTDfqNlRcWY9X2N/LFa/BKojgDt5tcpbphV4HqWvl8nS+fPcVrIElJfQ/QGFEOx68G95BhByntT9+JhSbHJt73dGnCSroZCw5QefdydREGvA5n00Vo9yT9IMvQsQbmpRzo6hcrSSUvagZqmZckA=,iv:F/HllDzyxgulIWZbfz9bFKR+SFg4PoaUYZ5N5hfIzw0=,tag:h2NXmvj/thhBg1rIkwdXXA==,type:str] | ||
9 | pgp: | ||
10 | - created_at: '2021-01-02T20:38:09Z' | ||
11 | enc: | | ||
12 | -----BEGIN PGP MESSAGE----- | ||
13 | |||
14 | hF4Dgwm4NZSaLAcSAQdAwmvyXlr9MyfPfLgkfQkoktKBV2WA2xhZrGL7NeeGfhAw | ||
15 | REk+clJ9WgiJ0iceRAONPnEjeiK0J6Fsj+5Ulq8flFGkoj5Pta0pm/9fudKmcPdC | ||
16 | 0l4BF0G5LSpG1EmY+LmVdSdas16rWgthnojoXPvbbHG6jZs3aDETshdiN8Bdlqsf | ||
17 | aVhq2LYzscnYezNcdernR4uojtiFny8qcmdF3tFacr+mkgfgIQr0W9yWFhDH15gm | ||
18 | =4TwU | ||
19 | -----END PGP MESSAGE----- | ||
20 | fp: F1AF20B9511B63F681A14E8D51AEFBCD1DEF68F8 | ||
21 | - created_at: '2021-01-02T20:38:09Z' | ||
22 | enc: | | ||
23 | -----BEGIN PGP MESSAGE----- | ||
24 | |||
25 | hF4DXxoViZlp6dISAQdAruPXj9IsllEN7R5jk4gF7bW0ZirhvX7qsu22/6HbSw8w | ||
26 | 66RwN3WGjYO1CcVbHKuLqVVaUBCnrR/4XHN0JYUaqjubrSZBTWFKTBFsKSTT0LZq | ||
27 | 0l4BKcsXrbGpYC5+yQvg0RHJ7LplxpKOmqMY8KGckvGnVf2xg7k6wuWQREFzqwt+ | ||
28 | lOa3x+xFy9c0JwE8AafyKjb/cgqJiMb96lhsH57BpXJa2E39ImQbXqzDzdx2jEUt | ||
29 | =3rxi | ||
30 | -----END PGP MESSAGE----- | ||
31 | fp: 30D3453B8CD02FE2A3E7C78C0FB536FB87AE8F51 | ||
32 | unencrypted_suffix: _unencrypted | ||
33 | version: 3.6.1 | ||