… including snaphot deletions
[gabe@dom0 ~]$ sudo mount dev/mapper/sda1_crypt /mnt [gabe@dom0 ~]$ sudo mkdir /mnt/backups [gabe@dom0 ~]$ sudo mkdir mnt/backups/qubes4 [gabe@dom0 ~]$ sudo mkdir -p /mnt/backups/qubes4/root.snapshots
… (switch to ‘root’)
[root@dom0 ~]# btrfs send .snapshots/2 | btrfs receive /mnt/backups/qubes4/root.snapshots/ ERROR: failed to get flags for subvolume /.snapshots/2: Invalid argument
… (uh doi, the actual snapshot is another directory “down” under ‘…/snapshot’)
[root@dom0 ~]# btrfs send -v .snapshots/2/snapshot | btrfs receive /mnt/backups/qubes4/root.snapshots/ At subvol /.snapshots/2/snapshot At subvol snapshot BTRFS_IOC_SEND returned 0 joining genl thread
(This worked. Nice! But it created a subvolume under ’mnt/backups/qubes4/root.snapshots/snapshot’, which is not where I want it.)
[root@dom0 ~]# mkdir mnt/backups/qubes4/root.snapshots/2 [root@dom0 ~]# btrfs send -v .snapshots/2/snapshot | btrfs receive /mnt/backups/qubes4/root.snapshots/2/ At subvol /.snapshots/2/snapshot At subvol snapshot BTRFS_IOC_SEND returned 0 joining genl thread
(OK, this seems to have worked fine (again). Now for the next snapshot in the row, this time trying for an incremental send/receive.)
[root@dom0 ~]# mkdir mnt/backups/qubes4/root.snapshots/96/ [root@dom0 ~]# btrfs send -p .snapshots/2/snapshot -v /.snapshots/96/snapshot | btrfs receive /mnt/backups/qubes4/root.snapshots/96/ At subvol /.snapshots/96/snapshot At snapshot snapshot BTRFS_IOC_SEND returned 0 joining genl thread
(This went great and finished very fast! Now for one more…)
[root@dom0 ~]# mkdir mnt/backups/qubes4/root.snapshots/108 [root@dom0 ~]# btrfs send -p .snapshots/96/snapshot /.snapshots/108/snapshot | btrfs receive /mnt/backups/qubes4/root.snapshots/108/ At subvol /.snapshots/108/snapshot At snapshot snapshot
(I felt that I didn’t need the verbose flag in that example.)
What happens when the parent is deleted? … on either side?
It’s not a problem, the snapshot stays unchanged. The parent only seems to be of interest for the actual send/receive part, to minimize the amount of data transferred I presume, and not after that.
For more experimentation cf. the system preparation for development on a new machine below.
Must find new snapshots, auto-determine the parent (numeric predecessor), set up target directory and send/receive it correctly
If no predecessor, transfer as full, rather than incremental.
pre-run/post-run hooks directory, as with `dehydrated`
> snapper –version snapper 0.10.2 flags btrfs,lvm,no-ext4,xattrs,rollback,btrfs-quota,no-selinux
> mktemp -d /tmp/snapplicator-test.XXXXXXXXXXXX /tmp/snapplicator-test.FFypCHtBoFbG
> cd /tmp/snapplicator-test.FFypCHtBoFbG > ls -Al total 0
> chroot /tmp/snapplicator-test.FFypCHtBoFbG chroot: cannot change root directory to ‘/tmp/snapplicator-test.FFypCHtBoFbG’: Operation not permitted
Doesn’t work, removed it again.
> mktemp -d ~/temp/snapplicator-test.XXXXXXXXXXXX /home/gabe/temp/snapplicator-test.pWwFum0UqUB8
> chroot /home/gabe/temp/snapplicator-test.pWwFum0UqUB8 chroot: cannot change root directory to ‘/home/gabe/temp/snapplicator-test.pWwFum0UqUB8’: Operation not permitted
Still doesn’t work, removed it again.
I think the problem is that there’s no shell in the chroot. (duh!)
I should try running snapper directly, but hardlink it first…
> which snapper /usr/bin/snapper
> mktemp -d /tmp/snapplicator-test.XXXXXXXXXXXX /tmp/snapplicator-test.AlV0146Y0xCZ
> ln usr/bin/snapper /tmp/snapplicator-test.AlV0146Y0xCZ/usr/bin ln: failed to create hard link ‘/tmp/snapplicator-test.AlV0146Y0xCZ/usr/bin/snapper’ => ‘/usr/bin/snapper’: Invalid cross-device link
OK, so that didn’t work, because tmp is on a different FS, actually.
Once again, now as root:
/root/tmp/snapplicator-test.FHHhGVuwMNVD
ln: failed to create hard link ‘/root/tmp/snapplicator-test.FHHhGVuwMNVD/usr/bin/snapper’ => ‘/usr/bin/snapper’: Invalid cross-device link
Apparently, even /root is on a different FS - who knew? (Well, I /should have/…)
Let’s try again, in a way that’s definitely not nice, but should at least work…
/snapplicator-test.OpNTdzQMKrmY
chroot: failed to run command ‘snapper’: No such file or directory
chroot: failed to run command ‘/usr/bin/snapper’: No such file or directory
It’s there, though, so I don’t get it.
snapplicator-test.OpNTdzQMKrmY └── usr └── bin └── snapper
I tried hardlinking ls into the chroot, whith the same result…
snapplicator-test.OpNTdzQMKrmY ├── bin └── usr └── bin ├── ls └── snapper
3 directories, 2 files
chroot: failed to run command ‘ls’: No such file or directory
chroot: failed to run command ‘/usr/bin/ls’: No such file or directory
Maybe all this hardlinking business is not such a great idea, anyway, so let’s try copying…
(removed /snapplicator-test.OpNTdzQMKrmY)
> mktemp -d /tmp/snapplicator-test.XXXXXXXXXXXX /tmp/snapplicator-test.kDGe06FzyABT
> mkdir -p /tmp/snapplicator-test.kDGe06FzyABT/usr/bin
> mkdir -p /tmp/snapplicator-test.kDGe06FzyABT/usr/bin > cp -v /usr/bin/snapper /tmp/snapplicator-test.kDGe06FzyABT/usr/bin ‘/usr/bin/snapper’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/usr/bin/snapper’
> chroot /tmp/snapplicator-test.kDGe06FzyABT snapper –version chroot: cannot change root directory to ‘/tmp/snapplicator-test.kDGe06FzyABT’: Operation not permitted
Still doesn’t work, because a regular user just isn’t allowed to do that.
There’s actually a reason for this and it’s security related: If a regular user were allowed to chroot, they could do so into a root with a custom sudoers file that would give them root privileges.
There is a way to do it with namespaces, though:
> unshare -r chroot /tmp/snapplicator-test.kDGe06FzyABT /usr/bin/snapper –version chroot: failed to run command ‘/usr/bin/snapper’: No such file or directory
At least now I’m back to the same error as I had for root, but I’m a bit baffled by it, nonetheless.
Searching the web, it looks like this error may also get raised when the command binary itself exists, but has missing dependencies inside the chroot (which it obviously does). I would have expected a different error, but let’s see if we cannot fix it…
> ldd /usr/bin/snapper linux-vdso.so.1 (0x00007ffd11d46000) libsnapper.so.6 => /lib64/libsnapper.so.6 (0x00007fa21332e000) libtinfo.so.6 => /lib64/libtinfo.so.6 (0x00007fa2132fa000) libdbus-1.so.3 => /lib64/libdbus-1.so.3 (0x00007fa2132a6000) libjson-c.so.5 => /lib64/libjson-c.so.5 (0x00007fa21328d000) libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fa213064000) libm.so.6 => /lib64/libm.so.6 (0x00007fa212f7e000) libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fa212f5d000) libc.so.6 => /lib64/libc.so.6 (0x00007fa212d2b000) libboost_thread.so.1.79.0 => /lib64/libboost_thread.so.1.79.0 (0x00007fa212d0d000) libxml2.so.2 => /lib64/libxml2.so.2 (0x00007fa212b80000) libacl.so.1 => /lib64/libacl.so.1 (0x00007fa212b76000) libz.so.1 => /lib64/libz.so.1 (0x00007fa212b58000) libmount.so.1 => /lib64/libmount.so.1 (0x00007fa212b14000) libbtrfs.so.0 => /lib64/libbtrfs.so.0 (0x00007fa212b0c000) libsystemd.so.0 => /lib64/libsystemd.so.0 (0x00007fa212a39000) /lib64/ld-linux-x86-64.so.2 (0x00007fa2134a4000) liblzma.so.5 => /lib64/liblzma.so.5 (0x00007fa212a04000) libblkid.so.1 => /lib64/libblkid.so.1 (0x00007fa2129cd000) libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fa21299d000) libzstd.so.1 => /lib64/libzstd.so.1 (0x00007fa2128ea000) liblz4.so.1 => /lib64/liblz4.so.1 (0x00007fa2128c7000) libcap.so.2 => /lib64/libcap.so.2 (0x00007fa2128ba000) libgcrypt.so.20 => /lib64/libgcrypt.so.20 (0x00007fa212779000) libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x00007fa2126c6000) libgpg-error.so.0 => /lib64/libgpg-error.so.0 (0x00007fa2126a0000)
> mkdir tmp/snapplicator-test.kDGe06FzyABT/lib64 > for name in libsnapper.so.6 libtinfo.so.6 libdbus-1.so.3 libjson-c.so.5 libstdc++.so.6 libm.so.6 libgcc_s.so.1 libc.so.6 libboost_thread.so.1.79.0 libxml2.so.2 libacl.so.1 libz.so.1 libmount.so.1 libbtrfs.so.0 libsystemd.so.0 ld-linux-x86-64.so.2 liblzma.so.5 libblkid.so.1 libselinux.so.1 libzstd.so.1 liblz4.so.1 libcap.so.2 libgcrypt.so.20 libpcre2-8.so.0 libgpg-error.so.0; do cp -v /lib64/${name} /tmp/snapplicator-test.kDGe06FzyABT/lib64; done ‘/lib64/libsnapper.so.6’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libsnapper.so.6’ ‘/lib64/libtinfo.so.6’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libtinfo.so.6’ ‘/lib64/libdbus-1.so.3’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libdbus-1.so.3’ ‘/lib64/libjson-c.so.5’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libjson-c.so.5’ ‘/lib64/libstdc++.so.6’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libstdc++.so.6’ ‘/lib64/libm.so.6’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libm.so.6’ ‘/lib64/libgcc_s.so.1’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libgcc_s.so.1’ ‘/lib64/libc.so.6’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libc.so.6’ ‘/lib64/libboost_thread.so.1.79.0’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libboost_thread.so.1.79.0’ ‘/lib64/libxml2.so.2’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libxml2.so.2’ ‘/lib64/libacl.so.1’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libacl.so.1’ ‘/lib64/libz.so.1’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libz.so.1’ ‘/lib64/libmount.so.1’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libmount.so.1’ ‘/lib64/libbtrfs.so.0’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libbtrfs.so.0’ ‘/lib64/libsystemd.so.0’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libsystemd.so.0’ ‘/lib64/ld-linux-x86-64.so.2’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/ld-linux-x86-64.so.2’ ‘/lib64/liblzma.so.5’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/liblzma.so.5’ ‘/lib64/libblkid.so.1’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libblkid.so.1’ ‘/lib64/libselinux.so.1’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libselinux.so.1’ ‘/lib64/libzstd.so.1’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libzstd.so.1’ ‘/lib64/liblz4.so.1’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/liblz4.so.1’ ‘/lib64/libcap.so.2’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libcap.so.2’ ‘/lib64/libgcrypt.so.20’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libgcrypt.so.20’ ‘/lib64/libpcre2-8.so.0’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libpcre2-8.so.0’ ‘/lib64/libgpg-error.so.0’ -> ‘/tmp/snapplicator-test.kDGe06FzyABT/lib64/libgpg-error.so.0’
> unshare -r chroot /tmp/snapplicator-test.kDGe06FzyABT /usr/bin/snapper –version Failed to set locale. snapper 0.10.2 flags btrfs,lvm,no-ext4,xattrs,rollback,btrfs-quota,no-selinux
So this way, I could create a chroot, inside of which to execute snapper. It’s a bit of a pain, though…
> snapper list-configs Konfiguration | Subvolumen --------------+-----------
> unshare -r chroot /tmp/snapplicator-test.kDGe06FzyABT /usr/bin/snapper list-configs Failed to set locale. Failure (dbus fatal exception).
Then I found the ‘–no-dbus’ option and got a step further…
> unshare -r chroot /tmp/snapplicator-test.kDGe06FzyABT /usr/bin/snapper –no-dbus list-configs Failed to set locale. Listing configs failed (reading sysconfig-file failed).
> mkdir -p /tmp/snapplicator-test.kDGe06FzyABT/etc/snapper/configs > unshare -r chroot /tmp/snapplicator-test.kDGe06FzyABT /usr/bin/snapper –no-dbus list-configs Failed to set locale. Listing configs failed (reading sysconfig-file failed).
Still, though, even if I get `snapper` to work in the unshared chroot, I might still need root access to create btrfs subvolumes for testing… maybe only a full container would work, but at the very least it would be a bunch more work to get this to work…
More than it’s worth, IMO, so I left the idea and will do manual testing, for now.
> mkdir Projekte/snapplicator > sudo btrfs subvolume create Projekte/snapplicator/original Create subvolume ‘Projekte/snapplicator/original’
> sudo btrfs subvolume create Projekte/snapplicator/backup Create subvolume ‘Projekte/snapplicator/backup’
> sudo chown gabe:users Projekte/snapplicator/original > sudo chown gabe:users Projekte/snapplicator/backup
> sudo snapper -c snapplicator create-config Projekte/snapplicator/original > sudo snapper list-configs Konfiguration | Subvolumen --------------+------------------------------------------ snapplicator | /home/gabe/Projekte/snapplicator/original
> sudo snapper -c snapplicator get-config Schlüssel | Wert -----------------------+------------------------------------------ ALLOW_GROUPS | ALLOW_USERS | BACKGROUND_COMPARISON | yes EMPTY_PRE_POST_CLEANUP | yes EMPTY_PRE_POST_MIN_AGE | 1800 FREE_LIMIT | 0.2 FSTYPE | btrfs NUMBER_CLEANUP | yes NUMBER_LIMIT | 50 NUMBER_LIMIT_IMPORTANT | 10 NUMBER_MIN_AGE | 1800 QGROUP | SPACE_LIMIT | 0.5 SUBVOLUME | /home/gabe/Projekte/snapplicator/original SYNC_ACL | no TIMELINE_CLEANUP | yes TIMELINE_CREATE | yes TIMELINE_LIMIT_DAILY | 10 TIMELINE_LIMIT_HOURLY | 10 TIMELINE_LIMIT_MONTHLY | 10 TIMELINE_LIMIT_WEEKLY | 0 TIMELINE_LIMIT_YEARLY | 10 TIMELINE_MIN_AGE | 1800
> sudo snapper -c snapplicator list
—+--------+----------+-------+----------+------------+--------------+-------------- 0 | single | | | root | | current |
> sudo snapper -c snapplicator create > sudo snapper -c snapplicator list
—+--------+----------+------------------------------+----------+------------+--------------+-------------- 0 | single | | | root | | current | 1 | single | | Di 28 Jun 2022 10:21:12 CEST | root | | |
So far, so good. Now let’s create some “data”…
> touch Projekte/snapplicator/original/bollocks > sudo snapper -c snapplicator create > echo “nonsense” > Projekte/snapplicator/original/bollocks > sudo snapper -c snapplicator create > echo “bollocks” > Projekte/snapplicator/original/bollocks > sudo snapper -c snapplicator create > sudo snapper -c snapplicator list
—+--------+----------+------------------------------+----------+------------+--------------+-------------- 0 | single | | | root | | current | 1 | single | | Di 28 Jun 2022 10:21:12 CEST | root | | | 2 | single | | Di 28 Jun 2022 10:28:00 CEST | root | | | 3 | single | | Di 28 Jun 2022 10:28:17 CEST | root | | | 4 | single | | Di 28 Jun 2022 10:30:21 CEST | root | | |
Now let’s do a few comparisons, just to see that things are working correctly here…
> sudo snapper -c snapplicator diff 0..4 (no output)
> rm Projekte/snapplicator/original/bollocks > sudo snapper -c snapplicator diff 0..4 — home/gabe/Projekte/snapplicator/original/bollocks 1970-01-01 01:00:00.000000000 +0100 +++ /home/gabe/Projekte/snapplicator/original.snapshots/4/snapshot/bollocks 2022-06-28 10:28:30.809851392 +0200 @@ -0,0 +1 @@ +bollocks
> sudo snapper -c snapplicator diff 4..0
— home/gabe/Projekte/snapplicator/original.snapshots/4/snapshot/bollocks 2022-06-28 10:28:30.809851392 0200
++ /home/gabe/Projekte/snapplicator/original/bollocks 1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-bollocks
This just confirms that snapshot 0 really always represents the current state of the subvolume, as hinted at by the description “current”.
This means that in “diff” comparisons, it makes more sense to specify 0 last, if one wants to compare some earlier snapshot with the current state.
> sudo snapper -c snapplicator diff 1..2 (no output)
Creation of empty files does not yield a diff. For that we have…
> sudo snapper -c snapplicator status 1..2 +..... /home/gabe/Projekte/snapplicator/original/bollocks
The snapper man page lists what the status bits mean. A + in the first place means creation, a c content change etc.
> sudo snapper -c snapplicator diff 2..3
— home/gabe/Projekte/snapplicator/original.snapshots/2/snapshot/bollocks 2022-06-28 10:27:53.157706713 0200
++ home/gabe/Projekte/snapplicator/original.snapshots/3/snapshot/bollocks 2022-06-28 10:28:14.665789362 +0200
@@ -0,0 +1 @@
+nonsense
> sudo snapper -c snapplicator status 2..3 c..... /home/gabe/Projekte/snapplicator/original/bollocks
Here, the diff is non-empty and so is the status, because the file contents have changed.
> sudo snapper -c snapplicator diff 3..4
— home/gabe/Projekte/snapplicator/original.snapshots/3/snapshot/bollocks 2022-06-28 10:28:14.665789362 0200
++ home/gabe/Projekte/snapplicator/original.snapshots/4/snapshot/bollocks 2022-06-28 10:28:30.809851392 +0200
@@ -1 +1 @@
-nonsense
+bollocks
> sudo snapper -c snapplicator status 3..4 c..... /home/gabe/Projekte/snapplicator/original/bollocks
Same. Now let’s see about several files were some will remain unchanged…
> echo foo > Projekte/snapplicator/original/foo > sudo snapper -c snapplicator create > echo bar > Projekte/snapplicator/original/bar > sudo snapper -c snapplicator create > echo baz > Projekte/snapplicator/original/baz > sudo snapper -c snapplicator create > mv Projekte/snapplicator/original/foo Projekte/snapplicator/original/foobar > sudo snapper -c snapplicator create > echo foobar > Projekte/snapplicator/original/foobar > sudo snapper -c snapplicator create > rm Projekte/snapplicator/original/bar > sudo snapper -c snapplicator create > sudo snapper -c snapplicator list
----+--------+----------+------------------------------+----------+------------+--------------+-------------- 0 | single | | | root | | current | 1 | single | | Di 28 Jun 2022 10:21:12 CEST | root | | | 2 | single | | Di 28 Jun 2022 10:28:00 CEST | root | | | 3 | single | | Di 28 Jun 2022 10:28:17 CEST | root | | | 4 | single | | Di 28 Jun 2022 10:30:21 CEST | root | | | 5 | single | | Di 28 Jun 2022 10:58:47 CEST | root | | | 6 | single | | Di 28 Jun 2022 10:58:57 CEST | root | | | 7 | single | | Di 28 Jun 2022 10:59:12 CEST | root | | | 8 | single | | Di 28 Jun 2022 10:59:43 CEST | root | | | 9 | single | | Di 28 Jun 2022 10:59:58 CEST | root | | | 10 | single | | Di 28 Jun 2022 11:00:00 CEST | root | timeline | timeline | 11 | single | | Di 28 Jun 2022 11:00:13 CEST | root | | |
Oh… a regular timeline snapshot slipped in here! Doesn’t matter for our testing, but good to know for when we look at the results…
> sudo snapper -c snapplicator diff 4..5
— home/gabe/Projekte/snapplicator/original.snapshots/4/snapshot/bollocks 2022-06-28 10:28:30.809851392 0200
++ home/gabe/Projekte/snapplicator/original.snapshots/5/snapshot/bollocks 1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-bollocks
— home/gabe/Projekte/snapplicator/original.snapshots/4/snapshot/foo 1970-01-01 01:00:00.000000000 0100
++ home/gabe/Projekte/snapplicator/original.snapshots/5/snapshot/foo 2022-06-28 10:58:39.664041374 +0200
@@ -0,0 +1 @@
+foo
> sudo snapper -c snapplicator status 4..5 -..... /home/gabe/Projekte/snapplicator/original/bollocks +..... /home/gabe/Projekte/snapplicator/original/foo
OK, so the diff has two parts and the status output two lines, because the ‘bollocks’ file had been removed in the current “snapshot” (no. 0), but that change was not actually snapshotted yet, and then we created a new file.
> sudo snapper -c snapplicator diff 5..6
— home/gabe/Projekte/snapplicator/original.snapshots/5/snapshot/bar 1970-01-01 01:00:00.000000000 0100
++ home/gabe/Projekte/snapplicator/original.snapshots/6/snapshot/bar 2022-06-28 10:58:55.680133393 +0200
@@ -0,0 +1 @@
+bar
> sudo snapper -c snapplicator status 5..6 +..... /home/gabe/Projekte/snapplicator/original/bar
Only a single new file created. Note how the file ‘foo’ from last snapshot is missing from this output, as it remains unchanged.
> sudo snapper -c snapplicator diff 6..7
— home/gabe/Projekte/snapplicator/original.snapshots/6/snapshot/baz 1970-01-01 01:00:00.000000000 0100
++ home/gabe/Projekte/snapplicator/original.snapshots/7/snapshot/baz 2022-06-28 10:59:03.736179679 +0200
@@ -0,0 +1 @@
+baz
> sudo snapper -c snapplicator status 6..7 +..... /home/gabe/Projekte/snapplicator/original/baz
Yet another file creation. The two existing files are unchanged and thus not listed here.
> sudo snapper -c snapplicator diff 7..8
— home/gabe/Projekte/snapplicator/original.snapshots/7/snapshot/foo 2022-06-28 10:58:39.664041374 0200
++ home/gabe/Projekte/snapplicator/original.snapshots/8/snapshot/foo 1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-foo
— home/gabe/Projekte/snapplicator/original.snapshots/7/snapshot/foobar 1970-01-01 01:00:00.000000000 0100
++ home/gabe/Projekte/snapplicator/original.snapshots/8/snapshot/foobar 2022-06-28 10:58:39.664041374 +0200
@@ -0,0 +1 @@
+foo
> sudo snapper -c snapplicator status 7..8 -..... /home/gabe/Projekte/snapplicator/original/foo +..... /home/gabe/Projekte/snapplicator/original/foobar
This is how a rename/move shows up.
> sudo snapper -c snapplicator diff 8..9
— home/gabe/Projekte/snapplicator/original.snapshots/8/snapshot/foobar 2022-06-28 10:58:39.664041374 0200
++ home/gabe/Projekte/snapplicator/original.snapshots/9/snapshot/foobar 2022-06-28 10:59:55.192464973 +0200
@@ -1 +1 @@
-foo
+foobar
> sudo snapper -c snapplicator status 8..9 c..... /home/gabe/Projekte/snapplicator/original/foobar
Content change (unchanged files not listed, of course).
> sudo snapper -c snapplicator diff 9..10 > sudo snapper -c snapplicator status 9..10
No change in the timeline snapshot (no surprise).
> sudo snapper -c snapplicator diff 10..11
— home/gabe/Projekte/snapplicator/original.snapshots/10/snapshot/bar 2022-06-28 10:58:55.680133393 0200
++ home/gabe/Projekte/snapplicator/original.snapshots/11/snapshot/bar 1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-bar
> sudo snapper -c snapplicator status 10..11 -..... /home/gabe/Projekte/snapplicator/original/bar
And now the ‘bar’ file is gone.
I think that’s enough faffing about, time to break out `snapplicator`…
I put snapplicator into a private repo on GitHub and cloned it onto the workstation. Then I created the /etc/snapplicator dir and copied examples/config.yml there, which I then edited to look like this: “` config: version: 1 duplication_pairs:
- source: /home/gabe/Projekte/snapplicator/original target: /home/gabe/Projekte/snapplicator/backup
“` … which should do the trick, I think.
> python3 snapplicator.py Traceback (most recent call last): File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 398, in <module> for duplication_pair in get_duplication_pairs(): File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 34, in get_duplication_pairs config = yaml.load(config_file.read()) TypeError: load() missing 1 required positional argument: ‘Loader’
Python yaml’s `load` now requires a loader attribute, but I was able to fix that using `safe_load` instead, which is the better and safer choice, anyway.
> python3 snapplicator.py Traceback (most recent call last): File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 399, in <module> duplicate(duplication_pair.source, duplication_pair.target) File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 373, in duplicate source = SnapshotDirectory(source_dir) File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 264, in __init__ super().__init__(path) File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 86, in __init__ path = self._validate_path(path) File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 100, in _validate_path self.error(‘Path “{}” is not readable!’.format(path)) File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 111, in error raise self.error_class(message) __main__.SnapshotDirectoryError: Path ”home/gabe/Projekte/snapplicator/original.snapshots” is not readable!
Yeah, that’ll happen… because snapper has to be run as root, the snapshots directory will then also only be readable by root.
So I’ll just have to run `snapplicator` as root, too.
> sudo python3 snapplicator.py -v Traceback (most recent call last): File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 399, in <module> duplicate(duplication_pair.source, duplication_pair.target) File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 374, in duplicate target = SnapshotDirectory(target_dir) File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 264, in __init__ super().__init__(path) File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 86, in __init__ path = self._validate_path(path) File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 98, in _validate_path self.error(‘Path “{}” is not a directory!’.format(path)) File “/home/gabe/git/github/snapplicator/snapplicator.py”, line 111, in error raise self.error_class(message) __main__.SnapshotDirectoryError: Path ”home/gabe/Projekte/snapplicator/backup.snapshots” is not a directory!
OK, this is a bug (but one I suspected might exist). Snapplicator should be able to intialise the target.
Fixed it by introducing a new kwarg `create_if_missing`, which allows PathWrapper to create missing directories and SnapshotDirectory to create missing btrfs subvolumes.
Had to fix a bunch of other stuff before the script would first run through successfully, but then…
> sudo python3 snapplicator.py -v Missing from target: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] Superfluous at target: nothing First missing snapshot is number 2 (predecessor: 1)
… and after that…
> sudo python3 snapplicator.py -v Missing from target: nothing Superfluous at target: nothing
So yeah, that seems to have worked (even with the new timeline snapshots that have cropped up in the meantime).
Now to test snapshot deletion. Let’s say I’ve grown up a bit and become thoroughly embarassed about the contents of my first (long gone from current) test file and I want to remove that from my snapshot history…
The file was introduced (but empty) in snapshot 2, but the offending content was written there in snapshot 4 and then the file got removed in snapshot 5, so if I only remove snapshot 4, I’m good.
> sudo snapper -c snapplicator delete 4 > sudo snapper -c snapplicator list
----+--------+----------+------------------------------+----------+------------+--------------+-------------- 0 | single | | | root | | current | 1 | single | | Di 28 Jun 2022 10:21:12 CEST | root | | | 2 | single | | Di 28 Jun 2022 10:28:00 CEST | root | | | 3 | single | | Di 28 Jun 2022 10:28:17 CEST | root | | | 5 | single | | Di 28 Jun 2022 10:58:47 CEST | root | | | 6 | single | | Di 28 Jun 2022 10:58:57 CEST | root | | | 7 | single | | Di 28 Jun 2022 10:59:12 CEST | root | | | 8 | single | | Di 28 Jun 2022 10:59:43 CEST | root | | | 9 | single | | Di 28 Jun 2022 10:59:58 CEST | root | | | 10 | single | | Di 28 Jun 2022 11:00:00 CEST | root | timeline | timeline | 11 | single | | Di 28 Jun 2022 11:00:13 CEST | root | | | 12 | single | | Di 28 Jun 2022 12:00:23 CEST | root | timeline | timeline | 13 | single | | Di 28 Jun 2022 13:00:01 CEST | root | timeline | timeline | 14 | single | | Di 28 Jun 2022 14:00:01 CEST | root | timeline | timeline | 15 | single | | Di 28 Jun 2022 15:00:01 CEST | root | timeline | timeline | 16 | single | | Di 28 Jun 2022 16:00:01 CEST | root | timeline | timeline | 17 | single | | Di 28 Jun 2022 17:00:01 CEST | root | timeline | timeline | > sudo cat ~/Projekte/snapplicator/original/.snapshots/4/snapshot/bollocks cat: home/gabe/Projekte/snapplicator/original.snapshots/4/snapshot/bollocks: No such file or directory
But uh oh, there is still a backup of this snapshot in the snapplicator target, so we need to run the script again to have that removed, as well.
> sudo python3 snapplicator.py -v Missing from target: nothing Superfluous at target: [4]
> sudo ls ~/Projekte/snapplicator/backup/.snapshots/ 1 10 11 12 13 14 15 16 17 2 3 5 6 7 8 9
So that worked, but now let’s pretend I relaise that the test file’s name is the same as the offensive content. Now I need to purge history of all the snapshots it was part of…
> sudo snapper -c snapplicator remove 2-5 > sudo find ~/Projekte/snapplicator/original -name bollocks (no output)
> sudo python3 snapplicator.py -v Missing from target: nothing Superfluous at target: [2, 3, 5]
> sudo find ~/Projekte/snapplicator/backup -name bollocks (no output)
So it really looks like I got the script to work correctly on another machine! :-)
“` [Unit] Description=Duplication of Snapper Snapshots
[Timer] OnBootSec=20m OnUnitActiveSec=20m
[Install] WantedBy=timers.target “`
“` [Unit] Description=Duplication of Snapper Snapshots
[Service] Type=oneshot ExecStart=/usr/bin/python3 /home/gabe/git/github/snapplicator/snapplicator.py -v “` Obviously, this is not what the final ExecStart line will be…
> sudo cp -v examples/snapplicator.*[^~] /usr/lib/systemd/system ‘examples/snapplicator.service’ -> ‘/usr/lib/systemd/system/snapplicator.service’ ‘examples/snapplicator.timer’ -> ‘/usr/lib/systemd/system/snapplicator.timer’
> systemctl status snapplicator.service ○ snapplicator.service - Duplication of Snapper Snapshots Loaded: loaded (/usr/lib/systemd/system/snapplicator.service; static) Active: inactive (dead)
> systemctl status snapplicator.timer ○ snapplicator.timer - Duplication of Snapper Snapshots Loaded: loaded (/usr/lib/systemd/system/snapplicator.timer; disabled; vendor preset: disabled) Active: inactive (dead) Trigger: n/a Triggers: ● snapplicator.service
So systemd found them w/o reloading, but obviously they’re disabled and dead. Let’s change that…
> sudo systemctl enable snapplicator.timer Created symlink /etc/systemd/system/timers.target.wants/snapplicator.timer → /usr/lib/systemd/system/snapplicator.timer.
> systemctl status snapplicator.timer ○ snapplicator.timer - Duplication of Snapper Snapshots Loaded: loaded (/usr/lib/systemd/system/snapplicator.timer; enabled; vendor preset: disabled) Active: inactive (dead) Trigger: n/a Triggers: ● snapplicator.service
Enabling alone isn’t enough…
> sudo systemctl start snapplicator.timer > systemctl status snapplicator.timer ● snapplicator.timer - Duplication of Snapper Snapshots Loaded: loaded (/usr/lib/systemd/system/snapplicator.timer; enabled; vendor preset: disabled) Active: active (waiting) since Wed 2022-06-29 09:55:42 CEST; 3s ago Until: Wed 2022-06-29 09:55:42 CEST; 3s ago Trigger: Wed 2022-06-29 10:15:42 CEST; 19min left Triggers: ● snapplicator.service
> systemctl status snapplicator.service ○ snapplicator.service - Duplication of Snapper Snapshots Loaded: loaded (/usr/lib/systemd/system/snapplicator.service; static) Active: inactive (dead) since Wed 2022-06-29 09:55:43 CEST; 11s ago TriggeredBy: ● snapplicator.timer Process: 30436 ExecStart=/usr/bin/python3 /home/gabe/git/github/snapplicator/snapplicator.py -v (code=exited, status=0/SUCCESS) Main PID: 30436 (code=exited, status=0/SUCCESS) CPU: 128ms
The service seems to have run. I added the ‘-v’ option to let it produce some output, but this is not reflected here.
I’ll see if I can find it, but I can also show the service’s effectiveness by just comparing the original and backup snapshot dirs.
> sudo ls ~/Projekte/snapplicator/original/.snapshots/ 1 10 11 12 13 14 15 16 17 18 19 20 21 6 7 8 9
> sudo ls ~/Projekte/snapplicator/backup/.snapshots/ 1 10 11 12 13 14 15 16 17 18 19 20 6 7 8 9
Whoops, there’s one missing!
But never fear…
> sudo snapper -c snapplicator list
----+--------+----------+------------------------------+----------+------------+--------------+-------------- 0 | single | | | root | | current | … 17 | single | | Di 28 Jun 2022 17:00:01 CEST | root | timeline | timeline | 18 | single | | Di 28 Jun 2022 18:00:01 CEST | root | timeline | timeline | 19 | single | | Di 28 Jun 2022 19:05:31 CEST | root | timeline | timeline | 20 | single | | Mi 29 Jun 2022 09:02:36 CEST | root | timeline | timeline | 21 | single | | Mi 29 Jun 2022 10:00:25 CEST | root | timeline | timeline |
Turns out no. 21 is a timeline snapshot, created at 10:00, but the service run was a bit earlier, at 09:55 (cf. above) so that all lines up and makes sense.
Notably, snapshot 21 is the only one that’s missing, but no. 20 was also created this morning and I did not run snapplicator manually today, so it definitely worked.
I’ll see again later, when the service runs again.
In the meantime, here’s the logged output:
> sudo journalctl -u snapplicator.service Jun 29 09:55:42 saturnus systemd[1]: Starting Duplication of Snapper Snapshots… Jun 29 09:55:43 saturnus python3[30436]: Missing from target: [20] Jun 29 09:55:43 saturnus python3[30436]: Superfluous at target: nothing Jun 29 09:55:43 saturnus python3[30436]: First missing snapshot is number 20 (predecessor: 19) Jun 29 09:55:43 saturnus systemd[1]: snapplicator.service: Deactivated successfully. Jun 29 09:55:43 saturnus systemd[1]: Finished Duplication of Snapper Snapshots.
So that looks good, too.
> systemctl status snapplicator.timer snapplicator.service ● snapplicator.timer - Duplication of Snapper Snapshots Loaded: loaded (/usr/lib/systemd/system/snapplicator.timer; enabled; vendor preset: disabled) Active: active (waiting) since Wed 2022-06-29 09:55:42 CEST; 23min ago Until: Wed 2022-06-29 09:55:42 CEST; 23min ago Trigger: Wed 2022-06-29 10:35:57 CEST; 17min left Triggers: ● snapplicator.service
○ snapplicator.service - Duplication of Snapper Snapshots Loaded: loaded (/usr/lib/systemd/system/snapplicator.service; static) Active: inactive (dead) since Wed 2022-06-29 10:15:58 CEST; 2min 47s ago TriggeredBy: ● snapplicator.timer Process: 31134 ExecStart=/usr/bin/python3 /home/gabe/git/github/snapplicator/snapplicator.py -v (code=exited, status=0/SUCCESS) Main PID: 31134 (code=exited, status=0/SUCCESS) CPU: 117ms > sudo journalctl -u snapplicator.service […] Jun 29 10:15:57 saturnus systemd[1]: Starting Duplication of Snapper Snapshots… Jun 29 10:15:58 saturnus python3[31134]: Missing from target: [21] Jun 29 10:15:58 saturnus python3[31134]: Superfluous at target: nothing Jun 29 10:15:58 saturnus python3[31134]: First missing snapshot is number 21 (predecessor: 20) Jun 29 10:15:58 saturnus systemd[1]: snapplicator.service: Deactivated successfully. Jun 29 10:15:58 saturnus systemd[1]: Finished Duplication of Snapper Snapshots.
It ran again and it worked again! Time to finish up…