#!/usr/bin/env bash # # script to prune zfs snapshots over a given age # # Author: Dave Eddy # Date: November 20, 2015 # License: MIT VERSION='v1.0.1' usage() { local prog=${0##*/} cat <<-EOF usage: $prog [-hnv] [-p [[dataset1] ...] remove snapshots from one or more zpools that match given criteria examples # $prog 1w remove snapshots older than a week across all zpools # $prog -vn 1w same as above, but with increased verbosity and without actually deleting any snapshots (dry-run) # $prog 3w tank1 tank2/backup remove snapshots older than 3 weeks on tank1 and tank2/backup. note that this script will recurse through *all* of tank1 and *all* datasets below tank2/backup # $prog -p 'autosnap_' 1M zones remove snapshots older than a month on the zones pool that start with the string "autosnap_" timespec the first argument denotes how old a snapshot must be for it to be considered for deletion - possible specifiers are s seconds m minutes h hours d days w weeks M months y years options -h print this message and exit -n dry-run, don't actually delete snapshots -p snapshot prefix string to match -q quiet, do not printout removed snapshots -v increase verbosity -V print the version number and exit EOF } debug() { ((verbosity >= 1)) && echo "$@" return 0 } # given a time in seconds, return the "human readable" string human() { local seconds=$1 if ((seconds < 0)); then ((seconds *= -1)) fi local times=( $((seconds / 60 / 60 / 24 / 365)) # years $((seconds / 60 / 60 / 24 / 30)) # months $((seconds / 60 / 60 / 24 / 7)) # weeks $((seconds / 60 / 60 / 24)) # days $((seconds / 60 / 60)) # hours $((seconds / 60)) # minutes $((seconds)) # seconds ) local names=(year month week day hour minute second) local i for ((i = 0; i < ${#names[@]}; i++)); do if ((${times[$i]} > 1)); then echo "${times[$i]} ${names[$i]}s" return elif ((${times[$i]} == 1)); then echo "${times[$i]} ${names[$i]}" return fi done echo '0 seconds' } dryrun=false verbosity=0 prefix= quiet=false while getopts 'hnqp:vV' option; do case "$option" in h) usage; exit 0;; n) dryrun=true;; p) prefix=$OPTARG;; q) quiet=true;; v) ((verbosity++));; V) echo "$VERSION"; exit 0;; *) usage; exit 1;; esac done shift "$((OPTIND - 1))" # extract the first argument - the timespec - and # convert it to seconds t=$1 time_re='^([0-9]+)([smhdwMy])$' seconds= if [[ $t =~ $time_re ]]; then # ex: "21d" becomes num=21 spec=d num=${BASH_REMATCH[1]} spec=${BASH_REMATCH[2]} case "$spec" in s) seconds=$((num));; m) seconds=$((num * 60));; h) seconds=$((num * 60 * 60));; d) seconds=$((num * 60 * 60 * 24));; w) seconds=$((num * 60 * 60 * 24 * 7));; M) seconds=$((num * 60 * 60 * 24 * 30));; y) seconds=$((num * 60 * 60 * 24 * 365));; *) echo "error: unknown spec '$spec'" >&2; exit 1;; esac elif [[ -z $t ]]; then echo 'error: timespec must be specified as the first argument' >&2 exit 1 else echo "error: failed to parse timespec '$t'" >&2 exit 1 fi shift pools=("$@") now=$(date +%s) code=0 while read -r creation snapshot; do # ensure optional prefix matches snapname=${snapshot#*@} if [[ -n $prefix && $prefix != "${snapname:0:${#prefix}}" ]]; then debug "skipping $snapshot: doesn't match prefix $prefix" continue fi # ensure snapshot is older than the cutoff time delta=$((now - creation)) human=$(human "$delta") if ((delta <= seconds)); then debug "skipping $snapshot: $human old" continue fi # remove the snapshot if ! $quiet || $dryrun; then echo -n "removing $snapshot: $human old" fi if $dryrun; then echo ' ' else if ! $quiet; then echo fi zfs destroy "$snapshot" || code=1 fi done < <(zfs list -Hpo creation,name -t snapshot -r "${pools[@]}") exit "$code"