ZFS, flush snapshots older than

ZFS is the penultimate file system, but there exists one glaring flaw... there is no in-built way nor are there many open-source and free-as-in-beer and easily-used snapshot management applications. We're left with creating or co-opting and modifying a bash script to suit that purpose.

That's what I've done. I found this bash script online, but it wouldn't run, so I got it running.

Copy the code (below), paste it into a Text Editor window. Save it to your Desktop as zfsflush.sh

Right-click the file, select Properties, select Permissions, select the 'Allow executing file as program' checkbox, click 'x' at upper-right to exit Properties.

In Terminal: sudo nautilus
In Nautilus: navigate to Other Locations/Computer/usr/local/bin

Click-n-hold on the zfsflush.sh file on the Desktop and drag it into that /bin directory.

Delete the copy of zfsflush.sh on the Desktop.

Now you can run that bash script from within Terminal.

Here's how I use it:

zfsflush.sh -d 15 -p bpool
zfsflush.sh -d 15 -p rpool

-d: number of days old the snapshot must be to get flushed
-p: the name of the pool (in our case, bpool and rpool)

The code:

#!/usr/bin/env bash

usage="$(basename "$0") -s SECONDS -m MINUTES -h HOURS -d DAYS -p GREP_PATTERN [-t]\n

arguments:\n
    -s delete snapshots older than SECONDS\n
    -m delete snapshots older than MINUTES\n
    -h delete snapshots older than HOURS\n
    -d delete snapshots older than DAYS\n
    -p the grep pattern to use\n
    -t test run, do not destroy the snapshots just print them
"

while getopts ":ts:m:h:d:p:" opt; do
  case $opt in
    s)
      seconds=$OPTARG
      ;;
    m)
      minutes=$OPTARG
      ;;
    h)
      hours=$OPTARG
      ;;
    d)
      days=$OPTARG
      ;;
    p)
      pattern=$OPTARG
      ;;
    t)
      test_run=true
      ;;
    \?)
      echo "Invalid option: -$OPTARG" 1>&2
      echo -e $usage 1>&2
      exit 1
      ;;
    :)
      echo "Option -$OPTARG requires an argument." 1>&2
      echo -e $usage 1>&2
      exit 1
      ;;
  esac
done

if [[ -z $pattern ]]; then
  echo -e "No grep pattern supplied\n" 1>&2
  echo -e $usage 1>&2
  exit 1
fi

if [[ -z $days ]]; then
  days=0
fi

if [[ -z $hours ]]; then
  hours=0
fi

if [[ -z $minutes ]]; then
  minutes=0
fi

if [[ -z $seconds ]]; then
  seconds=0
fi

# Figure out which platform we are running on, more specifically, whic version of date
# we are using. GNU date behaves different thant date on OSX and FreeBSD
platform='unknown'
unamestr=$(uname)

if [[ "$unamestr" == 'Linux' ]]; then
  platform='linux'
elif [[ "$unamestr" == 'FreeBSD' ]]; then
  platform='bsd'
elif [[ "$unamestr" == 'OpenBSD' ]]; then
  platform='bsd'
elif [[ "$unamestr" == 'Darwin' ]]; then
  platform='bsd'
else
  echo -e "unknown platform $unamestr 1>&2"
  exit 1
fi

compare_seconds=$(($days * 24 * 60 * 60 + $hours * 60 * 60 + $minutes * 60 + $seconds))
if [ $compare_seconds -lt 1 ]; then
  echo -e time has to be in the past 1>&2
  echo -e $usage 1>&2
  exit 1
fi

if [[ "$platform" == 'linux' ]]; then
compare_timestamp=`date --date="-$(echo $compare_seconds) seconds" +"%s"`
else
compare_timestamp=`date -j -v-$(echo $compare_seconds)S +"%s"`
fi

# get a list of snapshots sorted by creation date, so that we get the oldest first
# This will allow us to skip the loop early
snapshots=`zfs list -H -t snapshot -o name,creation -s creation | grep $pattern`

if [[ -z $snapshots ]]; then
  echo "no snapshots found for pattern $pattern"
  exit 0
fi


# for in uses \n as a delimiter
old_ifs=$IFS
IFS=$'\n'
for line in $snapshots; do
  snapshot=`echo $line | cut -f 1`
  creation_date=`echo $line | cut -f 2`

  if [[ "$platform" == 'linux' ]]; then
    creation_date_timestamp=`date --date="$creation_date" "+%s"`
  else
    creation_date_timestamp=`date -j -f "%a %b %d %H:%M %Y" "$creation_date" "+%s"`
  fi

  # Check if the creation date of a snapshot is less than our compare date
  # Meaning if it is older than our compare date
  # It is younger, we can stop processing since we the list is sorted by
  # compare date '-s creation'
  if [ $creation_date_timestamp -lt $compare_timestamp ]
  then
    if [[ -z $test_run ]]; then
      echo "DELETE: $snapshot from $creation_date"
      sudo zfs destroy $snapshot
    else
      echo "WOULD DELETE: $snapshot from $creation_date"
    fi
  else
    echo "KEEP: $snapshot from $creation_date"
    echo "No more snapshots to be processed for $pattern. Skipping.."
    break
  fi
done
IFS=$old_ifs

You can also set it up as a keyboard shortcut, so all you have to do is press a key combination and it'll run.

Zorin start menu > Settings > Keyboard Shortcuts

Scroll all the way to the bottom and click the button marked '+'.

Enter the following:

Name: ZFS Flush Snapshots

Command: gnome-terminal -- /bin/sh -c 'zfsflush.sh -d 15 -p bpool; zfsflush.sh -d 15 -p rpool; sleep 5'

Shortcut: Super+Z

Now, when you hit the Super+Z key combination, it'll run, then automatically exit when it's done.

Some additional code, which I named zfspurge.sh:

#!/bin/bash
keep=1
num_snaps=$(zfs list -t snapshot -o name -S creation | grep -Po '@autozsys_[a-zA-Z0-9]+$' | uniq | wc -l)
printf "Found %d autosys versions. Configured to keep %d.\n" $num_snaps $keep
if [[ $num_snaps -le $keep ]]; then
    printf "no need to prune.\n"
    exit 0
fi

num_to_prune=$(( num_snaps - keep ))
printf "Pruning %d zsys versions.\n" $num_to_prune
if [[ $num_to_prune -le 0 ]]; then
    printf "Error - no snapshots to prune.\n"
    exit 127
fi

if [[ $num_to_prune -ge $num_snaps ]]; then
    printf "Error - won't remove all snapshots.\n"
    exit 127
fi

for zsys_snap in $(zfs list -t snapshot -o name -S creation | grep -Po '@autozsys_[a-zA-Z0-9]+$' | uniq | tail -n $num_to_prune); do
    printf "Removing $zsys_snap\n"
    sudo zfs list -t snapshot -o name | sudo grep "${zsys_snap}$" | sudo xargs -n 1 zfs destroy -vr
done