Btrfs snapshot timer

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 :slight_smile:

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.

4 Likes

Oh, thx for sharing @Head_on_a_Stick

I have AL on a btrfs partition but still using timeshift with rsync instead of snapshot, shame on me :frowning_face:

Snapshots != backups :wink:

They’re useful for rolling back the system in case of b0rked upgrades but you should still have a proper backup (or preferably three).

Three is too much. It’s good to have a backup. But then, it’s good to have a backup of that backup, and if you think about it, it feels allot safer to have a backup of that backup of that backup of…yeah, three should suffice :grin:

I’ve changed the /usr/local/bin/snapshot script posted in the OP to make it more generalised — it now auto-detects the root partition UUID and any installed CPU µcode package and generates menu entries for each installed kernel type (ie, for the linux, linux-lts, linux-zen or linux-hardened packages).