46

I'm currently snapshotting my ZFS-based NAS nightly and weekly, a process that has saved my ass a few times. However, while the creation of the snapshot is automatic (from cron), the deletion of old snapshots is still a manual task. Obviously there's a risk that if I get hit by a bus, or the manual task isn't carried out, the NAS will run out of disk space.

Does anyone have any good ways / scripts they use to manage the number of snapshots stored on their ZFS systems? Ideally, I'd like a script that iterates through all the snapshots for a given ZFS filesystem and deletes all but the last n snapshots for that filesystem.

E.g. I've got two filesystems, one called tank and another called sastank. Snapshots are named with the date on which they were created: sastank@AutoD-2011-12-13 so a simple sort command should list them in order. I'm looking to keep the last 2 week's worth of daily snapshots on tank, but only the last two days worth of snapshots on sastank.

11 Answers 11

68

You may find something like this a little simpler

zfs list -t snapshot -o name | grep ^tank@Auto | tac | tail -n +16 | xargs -n 1 zfs destroy -r
  • Output the list of the snapshot (names only) with zfs list -t snapshot -o name
  • Filter to keep only the ones that match tank@Auto with grep ^tank@Auto
  • Reverse the list (previously sorted from oldest to newest) with tac
  • Limit output to the 16th oldest result and following with tail -n +16
  • Then destroy with xargs -n 1 zfs destroy -vr

Deleting snapshots in reverse order is supposedly more efficient or sort in reverse order of creation.

zfs list -t snapshot -o name -S creation | grep ^tank@Auto | tail -n +16 | xargs -n 1 zfs destroy -vr

Test it with ...|xargs -n 1 echo.

2
  • 1
    I think this needs a sort -r before the sed command. sed seems to output the bottom of the list beyond the first 15 lines, which in the default sort is the most recent. Flipping the list means I get the oldest snapshots at the bottom.
    – growse
    Dec 14, 2011 at 14:14
  • 4
    He stated "deleting snapshots in reverse order is supposedly more efficient", thus the sort order.
    – tgunr
    Mar 17, 2017 at 18:03
41

This totally doesn't answer the question itself, but don't forget you can delete ranges of snapshots.

zfs destroy zpool1/dataset@20160918%20161107

Would destroy all snapshots from "20160918" to "20161107" inclusive. Either end may be left blank, to mean "oldest" or "newest". So you could cook something up that figures out the "n" then destroy "...%n"..

Sorry to resurrect an old question.

0
27

More general case of getting most recent snapshot based on creation date, not by name.

zfs list -H -t snapshot -o name -S creation | head -1

Scoped to a specific filesystem name TestOne

zfs list -H -t snapshot -o name -S creation -d1 TestOne | head -1

-H:No header so that first line is a snapshot name

-t snapshot: List snapshots (list can list other things like pools and volumes)

-o name: Display the snapshot name property.

-S creation: Capital S denotes descending sort, based on creation time. This puts most recent snapshot as the first line.

-d1 TestOne: Says include children, which seems confusing but its because as far as this command is concerned, snapshots of TestOne are children. This will NOT list snapshots of volumes within TestOne such as TestOne/SubVol@someSnapshot.

| head -1: Pipe to head and only return first line.

1
  • 1
    Thanks for the -d1. That was the key to the question "How do I get all snapshots for a given dataset?" Dec 28, 2016 at 19:12
8

You might also want to check out zfs-prune-snapshots.

Remove snapshots from one or more zpools that match given criteria

It has a fairly robust time based mechanism for deleting snapshots, an example from the docs:

Remove snapshots older than two months on the tank pool that end with the string "_frequent"

zfs-prune-snapshots -s '_frequent' 2M tank

4
  • wow! nice&&neat. doing exactly what is needed
    – ataraxic
    Jan 2, 2021 at 13:18
  • This should be updated to be the accepted answer, much cleaner than all the bash-fu.
    – bgibson
    Jul 29, 2021 at 15:10
  • how do you install it on ubuntu?
    – tatsu
    Oct 3, 2022 at 19:16
  • @tatsu this is a script so you dont need to install, just download the script. You can do so with: git clone https://github.com/bahamas10/zfs-prune-snapshots.git Once you have the code, cd zfs-prune-snapshots and to run it: zfs-prune-snapshots --help You will likely want to add the path to where zfs-prune-snapshots is located to your path. You can also run make man to generate a manpage for it (see the 'Manpage' section github.com/bahamas10/zfs-prune-snapshots#manpage
    – DRAD
    Oct 6, 2022 at 12:23
6

growse's didn't work on OpenIndiana for me. It didn't understand -0 for xargs.

If using sort, be aware that it sorts alphabetically which may not be desired as you are probably wanting to find the most recent.

Here is code that will delete all but the last snapshots.

Remove the 'echo' to go live.

RETENTION=5
FS=tank1/test
SNAPNAME=daily-

zfs list -t snapshot -o name | grep ^$FS@${SNAPNAME} |  sed -n -e :a -e '1,${RETENTION}!{P;N;D;};N;ba' | xargs -n 1 echo zfs destroy -r

Sources: http://sed.sourceforge.net/sed1line.txt

4
  • 4
    Upvote because anyone who can use sed like that deserves it.
    – growse
    May 25, 2012 at 23:05
  • 3
    After a recent software update, that sed string stopped working for me and it started deleting all snapshots! bad sed! Luckily the production server was old and stayed safe. I now use sed -n -e :a -e '1,${RETENTION}!{P;N;D;};N;ba'
    – Dan Buhler
    Aug 17, 2012 at 19:58
  • I've never seen anyone SED like that.
    – Jason
    Aug 8, 2017 at 2:44
  • I have only become aware of this thread a few days ago, but it appears some time in the last few years there has been another software update that broke this script. This is the sed line that worked for me: sed -n -e :a -e "1,${RETENTION}!{P;N;D;};N;ba" . In my case, at least, double quote marks were needed.
    – Stephen
    Dec 5, 2022 at 1:10
3

I may have solved this with some bash-fu.

 zfs list -t snapshot -o name | grep ^tank@AutoD- | sort -r | wc -l | xargs -n 1 expr -$NUM_TO_KEEP + | tr -d '\n' | xargs -0 -i bash -c "zfs list -t snapshot -o name | grep ^tank@AutoD- | sort -r | tail -n{} | sort |xargs -t -n 1 zfs destroy -r"

Wow. It feels so wrong.

2

Just wanted to chime in on how I'm doing this on FreeBSD and OmniOS:

Get number of snapshots:

zfs list -t snapshot -o name | grep ^tank@Auto | wc -l
141

Subtract number you want to leave for n (e.g. 30 for a month of latest daily snapshots):

zfs list -t snapshot -o name | grep ^tank@Auto |  head -n +111 | xargs -n 1 zfs destroy -vr

Note how I replaced tail with head to delete in order from oldest to newest, since there's no tac command on FreeBSD

That's it! Works great for me...

1

The path for head is needed on Solaris, but should work without the path on other distros.

retention=14
dataset=vmstorage-17/824

zfs list -rt snap -H -o name ${dataset} | \
/usr/gnu/bin/head -n -${retention} | xargs -n 1 zfs destroy -r
1

Identify the latest two snapshots for a given dataset (creation, newest to oldest)

zfs_latest=`zfs list -H -t snapshot -o name -S creation | grep ^tank/example_dataset@ | head -2`

Identify ALL snapshots for a given dataset (creation, newest to oldest)

zfs_delete=`zfs list -H -t snapshot -o name -S creation | grep ^tanks/example_dataset@`

Remove latest snapshots from all set; creating a list of snapshots to delete.

for keep_snap in ${zfs_latest[@]}; do
  zfs_delete=( "${zfs_delete[@]/$keep_snap}" );
done

Remove the old snapshots

for snap in ${zfs_delete[@]}; do
  zfs destroy ${snap}
done
1

If someone wants to do recursive snapshots of the entire pool's datasets, I wrote this script to do just that:

#!/bin/bash

#Method to purge old snapshots. $1 is the dataset that snapshots are being purged for. $2 is the number of snapshots to retain.
purge_snapshots() {
  echo -e "\n  Purging snapshots for: $1"

  #We search for snapshots and return jsut the name sorted by creation time. We filter with grep just for snapshots created with this script with prefix "$1@auto"
  snapshots=$(zfs list -t snapshot -H -o name -S creation "$1" | grep ^"$1"@auto)
  if [[ -z "$snapshots" ]]; then
    echo -e "\n    No snapshots found for dataset"
    return 0
  fi
  echo -e "\n    Existing snapshots for $1:\n"
  echo "$snapshots"

  #We use "tail -n +" to skip the first $2 snapshots. We need + 1 to begin after that many snapshots. This gives us the list of snapshots to purge
  old_snapshots=$(echo "$snapshots" | tail -n +"$(($2 + 1))")
  if [[ -z "$old_snapshots" ]]; then
    echo -e "\n    Not enough old snapshots too purge"
    return 0
  fi
  echo -e "\n    Deleting the following old snapshots:\n"
  echo "$old_snapshots"
  echo "$old_snapshots" | xargs -n 1 zfs destroy -r
}

#Function to create snapshots. it receives the list of snapshots in $1 and retention count in $2.
create_snapshots() {
  formatted_date="$(date +"%Y-%m-%d_%H-%M-%S")"

  #We loop the list of datasets and create and purge snapshots for each of them with the same timestamp. this way all child datasets will have the same timestamp and retention count as the root dataset.
  while read -r dataset; do
    echo -e "\nCreating snapshot for: $dataset"
    snapshot_name="${dataset}@auto-${formatted_date}"
    zfs snapshot "$snapshot_name"
    echo -e "\n  Snapshot created: $snapshot_name"
    purge_snapshots "$dataset" "$2"
  done <<< "$1"
}

#We validate the input arguments. $1 is the pool name and $2 is the retention count.
if [ "$#" -ne 2 ]; then
  echo "Usage: $0 poolname retentioncount"
  exit 1
fi

if [[ -z "$1" ]]; then
  echo "poolname cannot be empty"
  exit 2
fi

if [[ "$2" -le 0 ]]; then
  echo "retentioncount must be a positive number"
  exit 3
fi

#We get the list of data sets in the pool recursively so that we include every single child dataset in the pool.
datasets=$(zfs list -H -o name -r "$1")
echo -e "Datasets in pool $1:\n"
echo "$datasets"
create_snapshots "$datasets" "$2"
2
  • Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
    – djdomi
    Mar 13 at 9:33
  • 1
    I am an avid supporter that good code is its own documentation. I believe the code is self evident. but I will add some comments to clarify some bash syntax that may be confusing.
    – superjugy
    Mar 15 at 18:07
0

Here is my version of Dan Buhler's script:

\# Ref: https://manpages.ubuntu.com/manpages/kinetic/man8/zfs-destroy.8.html

\# To get a list of datasets:
\# zfs list -t snapshot -o name -S creation

\# To get a list of memory usage:
\# zfs list -r -o space

\# The number of the most recent snapshots to be retained:
RETENTION=5

\# The datasets to prune with filesystem and snapshot name:
\# Format: <filesystem>@<snapshot name> 
declare -a SNAPS=(
\# Example array elements:
bpool/BOOT/ubuntu_7pdn8o@autozsys_
rpool/ROOT/ubuntu_7pdn8o/usr@autozsys_
rpool/ROOT/ubuntu_7pdn8o/var/cache@autozsys_
rpool/USERDATA/root_kpsn24@autozsys_
)

echo Deleting all but the last $RETENTION system snapshots.
for SNAP in ${SNAPS[@]}
do
echo SNAP=${SNAP}
\# The dry run (comment out when satisfied all is well):
zfs list -t snapshot -o name | grep ^${SNAP} | sed -n -e :a -e "1,${RETENTION}!{P;N;D;};N;ba" | xargs -n 1 zfs destroy -nprd
\# For real (uncomment when all is well):
\# zfs list -t snapshot -o name | grep ^${SNAP} | sed -n -e :a -e "1,${RETENTION}!{P;N;D;};N;ba" | xargs -n 1 zfs destroy -rd
done

After running this it may be necessary to reboot before all is finally deleted.

I configured my Ubuntu 22.04 OS to run this script whenever it boots.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .