aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenjamin Linskey2025-07-30 14:11:27 -0400
committerBenjamin Linskey2025-07-30 14:11:27 -0400
commitc018affc0098d4a74b215f91296e50be089ec29f (patch)
tree3b07eb9bec3f01eed100ae9232ae071a136d68b4
downloadzfs-snapshots-c018affc0098d4a74b215f91296e50be089ec29f.tar.gz

Add script and sample crontab

-rw-r--r--example.cron16
-rwxr-xr-xsnapshots.sh167
2 files changed, 183 insertions, 0 deletions
diff --git a/example.cron b/example.cron
new file mode 100644
index 0000000..853aad3
--- /dev/null
+++ b/example.cron
@@ -0,0 +1,16 @@
+# Example crontab for /etc/cron.d. See crontab(5).
+
+SHELL=/bin/sh
+
+# Make recursive snapshots of zroot and tank/foo, keeping 24
+# hourly, seven daily, eight weekly, 24 monthly, and unlimited yearly
+# snapshots.
+@hourly root snapshots.sh -c -p -t hourly -k 24 -r zroot tank/foo
+@daily root snapshots.sh -c -p -t daily -k 7 -r zroot tank/foo
+@weekly root snapshots.sh -c -p -t weekly -k 8 -r zroot tank/foo
+@monthly root snapshots.sh -c -p -t monthly -k 24 -r zroot tank/foo
+@yearly root snapshots.sh -c -t yearly -r zroot tank/foo
+
+# Make a non-recursive snapshot of tank/bar daily at 5:00,
+# preserving the three most recent snapshots.
+0 5 * * * root snapshots.sh -c -p -t my-custom-tag -k 3 tank/bar
diff --git a/snapshots.sh b/snapshots.sh
new file mode 100755
index 0000000..e4faaf0
--- /dev/null
+++ b/snapshots.sh
@@ -0,0 +1,167 @@
+#!/bin/sh
+
+# Manages ZFS snapshots.
+#
+# At least one of the following options must be provided to specify an
+# operation to perform:
+#
+# -c create snapshot(s)
+# -p prune snapshots
+# -l list snapshots
+#
+# If the -c or -p option is specified, at least one dataset must be specified
+# as well.
+#
+# If the -r option is specified, child datasets of the specified dataset(s)
+# will be recursively created, deleted, or listed.
+#
+# If the -c or -p option is specified, the -t option must be provided to
+# specify a tag.
+#
+# If the -p option is specified, the -k option must be provided to specify the
+# number of snapshots to keep for each dataset (including any newly created
+# snapshots).
+#
+# Usage: snapshots.sh [-cplrnvh] [-t tag] [-k num] [dataset ...]"
+#
+# -c create snapshot(s) (requires -t)
+# -p prune snapshots (requires -t and -k)
+# -l list snapshots
+# -r recursively create or delete snapshots
+# -n dry run: print commands that would be executed, but do not
+# actually modify data
+# -v verbose mode
+# -h print usage
+# -k num keep *num* snapshots for each dataset, including any newly
+# created snapshots
+# -t tag use *tag* as the snapshot name prefix
+
+set -e
+
+usage() {
+ printf "usage: %s [-cplrnvh] [-t tag] [-k num] [dataset ...]\n" "$0"
+}
+
+tag=
+keep=
+recursive=
+dry_run=
+verbose=
+create=
+prune=
+list=
+
+while getopts t:k:cplrknvh name; do
+ case $name in
+ t) tag="$OPTARG";;
+ k) keep="$OPTARG";;
+ c) create=1;;
+ p) prune=1;;
+ l) list=1;;
+ r) recursive=1;;
+ n) dry_run=1;;
+ v) verbose=1;;
+ h) usage
+ exit 0;;
+ ?) usage
+ exit 2;;
+ esac
+done
+shift $((OPTIND - 1))
+
+if [ -z "$create" ] && [ -z "$prune" ] && [ -z "$list" ]; then
+ printf "At least one of -c, -p, and -l must be specified.\n"
+ usage
+ exit 1
+fi
+
+if [ "$#" -eq 0 ] && { [ -n "$create" ] || [ -n "$prune" ]; }; then
+ printf "At least one dataset must be specified\n"
+ usage
+ exit 1
+fi
+
+if { [ -n "$create" ] || [ -n "$prune" ]; } && [ -z "$tag" ]; then
+ printf "Missing -t option\n"
+ usage
+ exit 1
+fi
+
+if [ -n "$prune" ] && [ -z "$keep" ]; then
+ printf "Missing -k option\n"
+ usage
+ exit 1
+fi
+
+if [ -z "$prune" ] && [ -n "$keep" ]; then
+ printf "\-k option is only valid with -p\n"
+ usage
+ exit 1
+fi
+
+create_cmd='zfs snapshot'
+if [ -n "$recursive" ]; then
+ create_cmd="$create_cmd -r"
+fi
+readonly create_cmd
+
+destroy_cmd='zfs destroy'
+if [ -n "$recursive" ]; then
+ destroy_cmd="$destroy_cmd -R"
+fi
+readonly destroy_cmd
+
+# Create snapshots.
+if [ -n "$create" ]; then
+ for dataset in "$@"; do
+ # FreeBSD's date -I option uses a "+00:00" suffix rather than "Z", and
+ # the + character is illegal in snapshot names, so we have to specify
+ # the format manually.
+ cmd="$create_cmd ${dataset}@${tag}-$(date -z utc +%Y-%m-%dT%H:%M:%SZ)"
+
+ if [ -n "$dry_run" ] || [ -n "$verbose" ]; then
+ printf "%s\n" "$cmd"
+ fi
+
+ if [ -z "$dry_run" ]; then
+ $cmd
+ fi
+ done
+fi
+
+# Prune snapshots.
+if [ -n "$prune" ]; then
+ if [ -n "$prune_only" ]; then
+ keep=$((keep + 1))
+ fi
+
+ for dataset in "$@"; do
+ snapshots=$(zfs list -t snapshot -o name -S name -H "$dataset")
+ to_delete=$(printf "%s\n" "$snapshots" | grep "@${tag}-" | tail -n +"$((keep + 1))")
+ for s in $to_delete; do
+ cmd="$destroy_cmd $s"
+ if [ -n "$dry_run" ] || [ -n "$verbose" ]; then
+ printf "%s\n" "$cmd"
+ fi
+
+ if [ -z "$dry_run" ]; then
+ $cmd
+ fi
+ done
+ done
+fi
+
+# List snapshots.
+if [ -n "$list" ]; then
+ cmd="zfs list"
+ if [ -n "$recursive" ]; then
+ cmd="$cmd -r"
+ fi
+ snapshots=$($cmd -t snapshot -o name -s name -H "$@")
+
+ if [ -n "$tag" ]; then
+ printf "%s\n" "$snapshots" | grep "@${tag}-"
+ else
+ printf "%s\n" "$snapshots"
+ fi
+fi