Since I’m running Arch again and I have the [testing] & [gnome-unstable] repositories active I thought it would be a good idea to start taking automatic daily snapshots in case an upgrade goes wrong.
The systemd unit files are /etc/systemd/system/snapshot.timer
:
[Unit]
Description=Daily snapshot
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
And /etc/systemd/systemd/snapshot.service
:
[Unit]
Description=Daily snapshot
[Service]
Type=oneshot
ExecStart=/usr/local/bin/snapshot
With the heavy lifting done by the script at /usr/local/bin/snapshot
:
#!/bin/sh
time=$(date +'%Y-%m-%d@%T')
grub_dir=/boot/grub
config_file="$grub_dir"/arch-snapshots.cfg
snapshot_dir=/snapshots
subvol_dir=/snapshots/arch
uuid=$(findmnt -o uuid -n /)
parameters='rw quiet'
get_snap_info() {
snap_list=$(for snap in "$snapshot_dir"/* ; do printf '%s ' "$(basename -- "$snap")" ; done)
snap_number=$(printf '%s\n' "$snap_list" | awk '{print NF}')
old_snap=$(printf '%s\n' "$snap_list" | awk '{print $1}')
}
if grep -q ' / .*snapshots.*' /proc/self/mounts ; then
printf 'Booted into snapshot, no action taken.\n'
else
printf 'Removing old configuration file...\n'
if [ -e "$config_file" ] ; then
rm "$config_file"
else
printf 'No configuration file found.\n'
fi
printf 'Creating new snapshot...\n'
btrfs subvolume snapshot / "$snapshot_dir"/"$time"
get_snap_info
while [ "$snap_number" -gt 5 ] ; do
printf 'Removing excess snapshot...\n'
btrfs subvolume delete "$snapshot_dir"/"$old_snap"
get_snap_info
done
printf 'Creating new configuration file...\n'
printf '#\n' > "$config_file"
for entry in "$snapshot_dir"/* ; do
for kernel in "$entry"/boot/vmlinuz-* ; do
image=/boot/"${kernel##*/}"
initrd=/boot/initramfs-"${image#*-}".img
set -- "$entry"/boot/*-ucode.img
if [ -e "$1" ] ; then
initrd_line=$(printf 'initrd %s/%s/boot/%s %s/%s%s' "$subvol_dir" "${entry##*/}" "${1##*/}" "$subvol_dir" "${entry##*/}" "$initrd")
else
initrd_line=$(printf 'initrd %s/%s%s' "$subvol_dir" "${entry##*/}" "$initrd")
fi
ed "$config_file" > /dev/null <<!
1i
menuentry '${entry##*/} (${image#*-})' {
search --fs-uuid --set=root $uuid
linux $subvol_dir/${entry##*/}$image root=UUID=$uuid rootflags=subvol=$subvol_dir/${entry##*/} $parameters
$initrd_line
}
.
w
!
done
done
printf 'All done!\n'
fi
I had to make the script POSIX-compliant because I have /bin/sh
linked to dash
in Arch and I’m also using the script in Alpine, hence the use of printf
piped to ed
(sed -i
is undefined in POSIX). A non-POSIX version would work just fine for Arch systems in which /bin/sh
is linked to bash
(ArchLabs is so configured by default AFAIUI). You could also replace printf | ed
with tee -a
and a here document to make the menuentry creation bit easier and more readable but that would make the oldest snapshot be the first in the boot list, which I don’t like (for my version the most recent snapshot is the top of the list).
I store the snapshots in the /snapshot/arch
top-level subvolume, which is mounted under /snapshot
in the running system.
GRUB is controlled from Alpine Linux, which is mounted under /alpine
in my Arch system (hence the $grub_dir
path) and I write my own grub.cfg
and use this stanza to call the /alpine/boot/grub/arch-snapshots.cfg
file:
submenu 'Arch snapshots' {
source $prefix/arch-snapshots.cfg
}
^ That could be added to the end of /etc/grub.d/40_custom
if you use grub-mkconfig
(or update-grub
in Debian) to generate grub.cfg
— $prefix
defaults to the directory containing grub.cfg
.
Not sure how useful this will be because it’s tailored to my system and the configuration is fairly idiosyncratic but I thought I’d share it anyway.
Anybody wanting a less hacky solution to this problem should check out Snapper and the grub-btrfs package. I’m sure they work just fine but I prefer a 40 line shell script that I wrote myself
EDIT: forgot to mention that for this to work then there must be no root partition line in /etc/fstab
(because the subvolume rootflag will be different for the snapshots). Use the bootloader to set any custom options for the root partition.
EDIT2: replaced printf | ed
with plain ed
and a here document, which is much easier to read and write.
EDIT3: rewrite for more general usage.