Cleanup helper script to destroy old ZFS snapshots

August 6th, 2021 by Philip Iezzi 4 min read
cover image

For years now, I am using this little helper script written in Python, which solves a very basic task: Destroy legacy ZFS snapshots. We all know ZFS snapshots are so lightweight and we tend to create so many snapshots, either manually (e.g. before a major system upgrade) or automated (e.g. by our replication or backup jobs). It can somtimes be cumbersome to destroy legacy ZFS snapshots that are no longer needed.

So, here comes help with zfs-destroy-snapshots.py which you could deploy to /usr/local/sbin to have that handy script ready. I know, this is like a really small snipped I could just post to Github Gist (ok, here you go: onlime/zfs-destroy-snapshots.py) or Pastebin, but I think it still deserves a full blown blog post.

Below script supports the following options:

$ zfs-destroy-snapshots.py -h
usage: zfs-destroy-snapshots.py [-h] [--prefix PREFIX] [--exclude EXCLUDE]
                                [--dataset DATASET] [--destroy]
                                age

positional arguments:
  age                List snapshots that are older than age (hours).

optional arguments:
  -h, --help         show this help message and exit
  --prefix PREFIX    Only list snapshot with this prefix.
  --exclude EXCLUDE  Exclude snapshots that contain this string.
  --dataset DATASET  Limit to a specific dataset.
  --destroy          Destroy snapshots that are older than age.

So, basic use would be (run this as root):

# list all snapshots that are older than 48hrs
$ zfs-destroy-snapshots.py 48
# destroy them for good
$ zfs-destroy-snapshots.py 48 --destroy

By default, the script does a dry-run, only listing snapshots it is going to destroy. It will only actually destroy them, if you explicitely use the --destroy argument. For your safety, and your daughter's.

You could further limit the lookup of snapshots by the following:

Remember to always run this script without --destroy first to verify the snapshots which are going to be deleted.

Here you go, zfs-destroy-snapshots.py / Gist onlime/zfs-destroy-snapshots.py

zfs-destroy-snapshots.py
#!/usr/bin/env python3
"""
Helper script to remove old ZFS snapshots
Copyright (c) Onlime GmbH, https://www.onlime.ch
"""
import argparse
from re import compile
from datetime import datetime, timedelta
from subprocess import check_output
import sys

def list_snapshots(age: int, prefix: str = None, exclude: str = None, dataset: str = None, destroy: bool = False):
    # List all snapshot names of a dataset sorted by creation date; sample command:
    # $ zfs list -H -t snapshot -o name,creation -s creation [<dataset>]
    cmd = ['zfs', 'list', '-H', '-t', 'snapshot', '-o', 'name,creation', '-s', 'creation']
    if dataset:
        cmd.append(dataset)
    snap_list = check_output(cmd).decode(sys.stdout.encoding)
    snap_lines = snap_list.splitlines()
    # reverse sort order (`zfs list` only supports asc sort order), newest snapshots on top
    snap_lines.reverse()

    for line in snap_lines:
        snapshot, creation = line.split('\t')
        if prefix and '@{}'.format(prefix) not in snapshot:
            continue
        if exclude and exclude in snapshot:
            continue
        snap_date = datetime.strptime(creation, '%a %b %d %H:%M %Y')
        if snap_date < datetime.now()-timedelta(hours=age):
            print('{}{} {}'.format(
                'DESTROYING ' if destroy else '',
                snap_date.strftime('%Y-%m-%d %H:%M'), 
                snapshot
            ))
            if destroy:
                check_output(['zfs', 'destroy', snapshot])

if __name__ == "__main__": # Only use the argument parsing if the script was called as script and not imported
    # argument parsing
    parser = argparse.ArgumentParser()
    parser.add_argument("age", type=int, help="List snapshots that are older than age (hours).")
    parser.add_argument("--prefix", help="Only list snapshot with this prefix.")
    parser.add_argument("--exclude", help="Exclude snapshots that contain this string.")
    parser.add_argument("--dataset", help="Limit to a specific dataset.")
    parser.add_argument("--destroy", action='store_true', help="Destroy snapshots that are older than age.")
    args = parser.parse_args()

    list_snapshots(args.age, args.prefix, args.exclude, args.dataset, args.destroy)

some final thoughts...

Besides, Onlime GmbH webhosting services run 100% on Debian Linux, mostly in LXC containers on Proxmox VE (PVE). If you don't know PVE yet - I can just strongly recommend it. They offer stunning support for OpenZFS - formerly known as ZFS-on-Linux, which lately runs super robust (which was definitely not the case 4-5 yrs ago!). So really, if you're not into ZFS yet, look into it. It is so much more than just another file system. Enough promo for today. Cheers!