|
1 | 1 | { |
2 | 2 | config, |
3 | 3 | lib, |
| 4 | + pkgs, |
4 | 5 | ... |
5 | 6 | }: |
6 | 7 | let |
7 | 8 | cfg = config.services.borgbackup-zfs-snapshots; |
8 | 9 | zfs = config.boot.zfs.package; |
9 | 10 |
|
10 | | - # Get all ZFS filesystems |
11 | 11 | zfsFileSystems = lib.filter (fs: fs.fsType == "zfs") (lib.attrValues config.fileSystems); |
12 | 12 |
|
13 | 13 | # Map paths to their ZFS datasets (find the most specific match) |
|
20 | 20 | in |
21 | 21 | if sorted != [ ] then lib.head sorted else null; |
22 | 22 |
|
23 | | - # Get all backup paths from clan.core.state |
24 | 23 | allBackupPaths = lib.unique ( |
25 | 24 | lib.flatten (map (state: state.folders or [ ]) (lib.attrValues config.clan.core.state)) |
26 | 25 | ); |
27 | 26 |
|
28 | | - # Get all unique ZFS datasets used for backups |
29 | 27 | allBackupDatasets = lib.unique (lib.filter (d: d != null) (map pathToDataset allBackupPaths)); |
30 | 28 |
|
| 29 | + # Mount parents before children, unmount in reverse order |
| 30 | + datasetsByDepth = lib.sort ( |
| 31 | + a: b: lib.stringLength a.mountPoint < lib.stringLength b.mountPoint |
| 32 | + ) allBackupDatasets; |
| 33 | + |
31 | 34 | # Find root datasets (datasets that are not children of other datasets in our list) |
32 | 35 | rootDatasets = lib.filter ( |
33 | 36 | ds: !lib.any (other: ds != other && lib.hasPrefix "${other.device}/" ds.device) allBackupDatasets |
34 | 37 | ) allBackupDatasets; |
35 | 38 |
|
36 | | - # Check if we have any ZFS datasets to backup |
37 | 39 | hasZfsBackups = allBackupDatasets != [ ]; |
38 | 40 |
|
39 | | - # Transform a backup path to use the snapshot directory |
| 41 | + # Snapshots are mounted explicitly under /run/borgbackup/<job> instead of |
| 42 | + # using the .zfs/snapshot automount, whose device/inode can change |
| 43 | + # mid-backup and make borg skip the tree with "file type or inode changed". |
| 44 | + snapshotMountPoint = |
| 45 | + name: dataset: |
| 46 | + "/run/borgbackup/${name}" + (if dataset.mountPoint == "/" then "" else dataset.mountPoint); |
| 47 | + |
40 | 48 | transformPathToSnapshot = |
41 | 49 | name: path: |
42 | 50 | let |
43 | 51 | dataset = pathToDataset path; |
44 | 52 | in |
45 | 53 | if dataset != null then |
46 | | - # Replace the mount point with the .zfs/snapshot path |
47 | 54 | let |
48 | 55 | relativePath = lib.removePrefix dataset.mountPoint path; |
49 | 56 | # Ensure we have a leading slash for non-empty relative paths |
|
55 | 62 | else |
56 | 63 | "/${relativePath}"; |
57 | 64 | in |
58 | | - "${dataset.mountPoint}/.zfs/snapshot/borg-${name}${relativePathWithSlash}" |
| 65 | + "${snapshotMountPoint name dataset}${relativePathWithSlash}" |
59 | 66 | else |
60 | 67 | path; |
61 | 68 | in |
|
81 | 88 | description = "Use ZFS snapshots for this backup job"; |
82 | 89 | }; |
83 | 90 |
|
84 | | - # Override the paths option to add apply function |
85 | 91 | paths = lib.mkOption { |
86 | 92 | apply = paths: if config.useZfsSnapshots then map (transformPathToSnapshot name) paths else paths; |
87 | 93 | }; |
88 | 94 | }; |
89 | 95 |
|
90 | | - # Add hooks for snapshot management |
91 | 96 | config = lib.mkIf config.useZfsSnapshots { |
92 | | - # Add pre-hook to create recursive snapshots |
93 | 97 | preHook = lib.mkBefore '' |
94 | | - echo "Creating ZFS snapshots for borgbackup job ${name}" |
95 | 98 | set -e |
96 | 99 |
|
97 | | - # Create recursive snapshots for root datasets only |
| 100 | + # clean up leftovers from a previous failed run |
| 101 | + ${lib.concatMapStringsSep "\n" (fs: '' |
| 102 | + ${pkgs.util-linux}/bin/umount "${snapshotMountPoint name fs}" 2>/dev/null || true |
| 103 | + '') (lib.reverseList datasetsByDepth)} |
| 104 | + ${lib.concatMapStringsSep "\n" (fs: '' |
| 105 | + ${zfs}/bin/zfs destroy -r "${fs.device}@borg-${name}" 2>/dev/null || true |
| 106 | + '') rootDatasets} |
| 107 | +
|
98 | 108 | ${lib.concatMapStringsSep "\n" (fs: '' |
99 | 109 | if ${zfs}/bin/zfs list -H -o name "${fs.device}" >/dev/null 2>&1; then |
100 | | - ${zfs}/bin/zfs snapshot -r "${fs.device}@borg-${name}" || { |
101 | | - echo "Failed to create recursive snapshot for ${fs.device}" |
102 | | - exit 1 |
103 | | - } |
104 | | - echo "Created recursive snapshot: ${fs.device}@borg-${name}" |
| 110 | + ${zfs}/bin/zfs snapshot -r "${fs.device}@borg-${name}" |
105 | 111 | fi |
106 | 112 | '') rootDatasets} |
107 | 113 |
|
108 | | - # Ensure snapshot directories are accessible (trigger automount) |
109 | | - echo "Ensuring snapshot directories are accessible..." |
110 | 114 | ${lib.concatMapStringsSep "\n" (fs: '' |
111 | | - ls "${fs.mountPoint}/.zfs/snapshot/borg-${name}/" > /dev/null || { |
112 | | - echo "Warning: Could not access snapshot directory ${fs.mountPoint}/.zfs/snapshot/borg-${name}/" |
113 | | - } |
114 | | - '') allBackupDatasets} |
| 115 | + mkdir -p "${snapshotMountPoint name fs}" |
| 116 | + ${pkgs.util-linux}/bin/mount -t zfs -o ro "${fs.device}@borg-${name}" "${snapshotMountPoint name fs}" |
| 117 | + '') datasetsByDepth} |
115 | 118 |
|
116 | 119 | set +e |
117 | 120 | ''; |
118 | 121 |
|
119 | | - # Add post-hook to destroy recursive snapshots |
120 | 122 | postHook = lib.mkAfter '' |
121 | | - echo "Cleaning up ZFS snapshots for borgbackup job ${name}" |
| 123 | + ${lib.concatMapStringsSep "\n" (fs: '' |
| 124 | + ${pkgs.util-linux}/bin/umount "${snapshotMountPoint name fs}" || true |
| 125 | + '') (lib.reverseList datasetsByDepth)} |
122 | 126 |
|
123 | | - # Destroy recursive snapshots (only need to destroy root datasets) |
| 127 | + # only root datasets carry the recursive snapshot |
124 | 128 | ${lib.concatMapStringsSep "\n" (fs: '' |
125 | | - if ${zfs}/bin/zfs list -H -o name "${fs.device}@borg-${name}" >/dev/null 2>&1; then |
126 | | - ${zfs}/bin/zfs destroy -r "${fs.device}@borg-${name}" || echo "Warning: Failed to destroy recursive snapshot ${fs.device}@borg-${name}" |
127 | | - fi |
| 129 | + ${zfs}/bin/zfs destroy -r "${fs.device}@borg-${name}" 2>/dev/null || true |
128 | 130 | '') rootDatasets} |
129 | 131 | ''; |
130 | 132 | }; |
|
134 | 136 | }; |
135 | 137 |
|
136 | 138 | config = lib.mkIf cfg.enable { |
137 | | - # Extend systemd services for borgbackup jobs that use ZFS snapshots |
138 | 139 | systemd.services = lib.mapAttrs' ( |
139 | 140 | name: job: |
140 | 141 | lib.nameValuePair "borgbackup-job-${name}" ( |
141 | 142 | lib.mkIf (job.useZfsSnapshots or false) { |
142 | 143 | serviceConfig = { |
143 | 144 | PrivateDevices = lib.mkForce false; # ZFS needs access to /dev/zfs |
| 145 | + # writable mountpoints for the ZFS snapshots (rest of /run is read-only) |
| 146 | + RuntimeDirectory = "borgbackup/${name}"; |
144 | 147 | }; |
145 | 148 |
|
146 | 149 | path = [ zfs ]; |
|
0 commit comments