btrfs, linux, OYB software, shell

Schedule BTRFS snapshots with btrfs-snp

To complement the last BTRFS tool btrfs-du, I would like to share a simple script for creating BTRFS snapshots that plays well with cron and systemd timers. This allows us to easily automate snapshot creation.

I was inspired by btrfs-snap by Birger Monsen.

Usage

btrfs-snp can be run manually, or summoned from cron. Invoke without arguments to see usage details.

# btrfs-snp
Usage: btrfs-snp <dir> (<tag>) (<limit>) (<seconds>) (<destdir>)

dir     │ create snapshot of <dir>
tag     │ name the snapshot <tag>_<timestamp>
limit   │ keep <limit> snapshots with this tag. 0 to disable
seconds │ don't create snapshots before <seconds> have passed from last with this tag. 0 to disable
destdir │ store snapshot in <destdir>, relative to <dir>

Snapshot of /home

# btrfs-snp /home

Tagged snapshot of root

# btrfs-snp / preupgrade

Tagged snapshot of root, but keep maximum 10 snapshots

# btrfs-snp / preupgrade 10

An example of running from cron: create and keep an hourly snapshot for one day, a daily snapshot for one week, a weekly snapshot for one month, and a monthly snapshot for one year.

#!/bin/bash
/usr/local/sbin/btrfs-snp /home hourly  24 3600
/usr/local/sbin/btrfs-snp /home daily    7 86400
/usr/local/sbin/btrfs-snp /home weekly   4 604800
/usr/local/sbin/btrfs-snp /     weekly   4 604800
/usr/local/sbin/btrfs-snp /home monthly 12 2592000

Don’t forget to run

# chmod +x /etc/cron.hourly/btrfs-snp

After some hours, you will see snapshots populating

$ tree -L 1 /home/.snapshots
/home/.snapshots
├── daily_2017-12-27_130102
├── hourly_2017-12-27_130101
├── hourly_2017-12-27_140101
├── hourly_2017-12-27_150101
├── hourly_2017-12-27_160101
├── monthly_2017-12-27_130104
└── weekly_2017-12-27_130103

7 directories, 0 files

Installation

Get the script and make it executable. You can do this in two lines, but better inspect it first. Don’t trust anyone blindly.

sudo wget https://raw.githubusercontent.com/nachoparker/btrfs-snp/master/btrfs-snp -O /usr/local/sbin/btrfs-snp
sudo chmod +x /usr/local/sbin/btrfs-snp

If you want to run it from cron, you might have to intall it first because some distributions have already completely replaced it by systemd timers. This was the case for me in Arch Linux.

In my case, I installed cronie

sudo pacman -S cronie

cronie logs the output to the system log by default, but you can set an email system if you want old style cron mails.

Also, note that you can use chronic if you only want logging to occur only if something goes wrong.

Details

There are a dizillion scripts to schedule snapshots, but I could not decide for one.

I didn’t want to use the available python scripts because in my eyes they are overcomplicated. This is a simple task, and I like simple code that I can read easily.

btrfs-snap was my favourite for several reasons: it uses the right tool for the job, which is bash, or maybe python. Also, I liked the idea of preventing snapshots from being created too often to simplify cron usage.

I liked the idea, but not the implementation. btrfs-snap has too much code for what it does, and several considered bad practices in the coding style. Also, some of the options just don’t work.

The idea of using transid is good, but in my testing, the transid changes by checking it, so doesn’t quite work. Also, btrfs-snap doesn’t detect changes outside of the mountpoint root, so it refuses to snapshot even when there have been changes.

I started writing a pull request, but I ended up changing all the code so I just started from zero.

Hope it helps!

Code

#!/bin/bash

#
# Script that creates BTRFS snapshots, manually or from cron
#
# Usage:
#          sudo btrfs-snp  <dir> (<tag>) (<limit>) (<seconds>) (<destdir>)
#
# Copyleft 2017 by Ignacio Nunez Hernanz <nacho _a_t_ ownyourbits _d_o_t_ com>
# GPL licensed (see end of file) * Use at your own risk!
#
# Based on btrfs-snap by Birger Monsen
#
# More at https://ownyourbits.com
#

function btrfs-snp()
{
  local   BIN="${0##*/}"
  local   DIR="${1}"
  local   TAG="${2:-snapshot}"
  local LIMIT="${3:-0}"
  local  TIME="${4:-0}"
  local   DST="${5:-.snapshots}"

  ## usage
  [[ "$*" == "" ]] || [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]] && {
    echo "Usage: $BIN <dir> (<tag>) (<limit>) (<seconds>) (<destdir>)

  dir     │ create snapshot of <dir>
  tag     │ name the snapshot <tag>_<timestamp>
  limit   │ keep <limit> snapshots with this tag. 0 to disable
  seconds │ don't create snapshots before <seconds> have passed from last with this tag. 0 to disable
  destdir │ store snapshot in <destdir>, relative to <dir>

Cron example: Hourly snapshot for one day, daily for one week, weekly for one month, and monthly for one year.

cat > /etc/cron.hourly/$BIN <<EOF
#!/bin/bash
/usr/local/sbin/$BIN /home hourly  24 3600
/usr/local/sbin/$BIN /home daily    7 86400
/usr/local/sbin/$BIN /home weekly   4 604800
/usr/local/sbin/$BIN /     weekly   4 604800
/usr/local/sbin/$BIN /home monthly 12 2592000
EOF
chmod +x /etc/cron.hourly/$BIN"
    return 0
  }

  ## checks
  local SNAPSHOT=${TAG}_$( date +%F_%H%M%S )

  [[ ${EUID} -ne 0  ]] && { echo "Must be run as root. Try 'sudo $BIN'" ; return 1; }
  [[ -d "$SNAPSHOT" ]] && { echo "$SNAPSHOT already exists"             ; return 1; }

  mount -t btrfs | cut -d' ' -f3 | grep -q "^${DIR}$" || {
    btrfs subvolume show "$DIR" | grep -q "${DIR}$" || {
      echo "$DIR is not a BTRFS mountpoint or snapshot"
      return 1
    }
  }

  DST="$DIR/$DST"
  mkdir -p "$DST"
  local SNAPS=( $( btrfs subvolume list -s --sort=gen "$DST" | awk '{ print $14 }' | grep "${TAG}_" ) )

  ## check time of the last snapshot for this tag
  [[ "$TIME" != 0 ]] && [[ "${#SNAPS[@]}" != 0 ]] && {
    local LATEST=$( sed -r "s|.*_(.*_.*)|\\1|;s|_([0-9]{2})([0-9]{2})([0-9]{2})| \\1:\\2:\\3|" <<< "${SNAPS[-1]}" )
    LATEST=$( date +%s -d "$LATEST" ) || return 1

    [[ $(( LATEST + TIME )) -gt $( date +%s ) ]] && { echo "No new snapshot needed for $TAG"; return 0; }
  }

  ## do it
  btrfs subvolume snapshot -r "$DIR" "$DST/$SNAPSHOT" || return 1

  ## prune older backups
  [[ "$LIMIT" != 0 ]] && \
  [[ ${#SNAPS[@]} -ge $LIMIT ]] && \
    echo "Pruning old snapshots..." && \
    for (( i=0; i <= $(( ${#SNAPS[@]} - LIMIT )); i++ )); do
      btrfs subvolume delete "$DIR/${SNAPS[$i]}"
    done

  echo "snapshot $SNAPSHOT generated"
}

btrfs-snp "$@"

# License
#
# This script is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This script is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this script; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,
# Boston, MA  02111-1307  USA

Author: nachoparker

Humbly sharing things that I find useful [ github dockerhub ]

5 Comments on “Schedule BTRFS snapshots with btrfs-snp

  1. Hi I’m trying to move to using btrfs-snp. Previously I was using btrfs-snap but it no longer works for my kernel,

    I like the idea of using cron to schedule this. But I stuck a problem, Unlike btrfs-snap I cant snapshot the contents of a subvolume within mounting it

    i.e /mnt is my only btrfs mount point and the snapshots work but I cant specify a subvolume to snapshot that isnt mounted like /mnt/Data/Doumentation. as these are subvolumes of /mnt.

    Do I have to mount each subvolume to be able to snapshot them? or is there a recursive option?

    Thanks
    Tristan

    I hope this makes sense,

    1. Mmm I haven’t encountered that situation before. I don’t know what btrfs-snap does, probably just mount it automatically. If someone else has this use case, you can open a feature requst on github, or send a PR.

Leave a Reply to nachoparker Cancel reply

Your email address will not be published. Required fields are marked *