safer implementation

This commit is contained in:
2026-10-10 17:12:17 +02:00
parent 4a8181edc8
commit 9e698ca02a
8 changed files with 174 additions and 138 deletions

View File

@@ -1,62 +1,28 @@
#!/bin/sh
opt_prefix='zfs-auto-snap'
opt_keep=10
opt_label="defaultLabel"
opt_threshold=0
# ==============================================================================
# Phase 1: Default Configuration
# ==============================================================================
# Default fallback settings if not provided via command-line flags.
opt_prefix='zfs-auto-snap' # Prefix prepended to all snapshot names
opt_keep=10 # Number of snapshots to retain
opt_label='defaultLabel' # Snapshot label tag (e.g. quarterhour, hourly, daily)
opt_threshold=0 # Written bytes required to trigger snapshot (0 = disabled)
do_snapshots ()
{
local KEEP=''
for ii in $TARGETS
do
KEEP="$opt_keep"
if [ "$opt_threshold" -eq 0 ]
then
zfs snapshot "$ii@$SNAPNAME"
else
bytes_written=`zfs get -Hp -o value written $ii`
if [ "$bytes_written" -ge "$opt_threshold" ]
then
zfs snapshot "$ii@$SNAPNAME"
else
KEEP=$(( $KEEP + 1 ))
fi
fi
# ASSERT: The old snapshot list is sorted by increasing age.
for jj in $SNAPSHOTS_OLD
do
# Check whether this is an old snapshot of the filesystem.
if [ -z "${jj#$ii@$SNAPGLOB}" ]
then
KEEP=$(( $KEEP - 1 ))
if [ "$KEEP" -le '0' ]
then
zfs destroy -d "$jj"
fi
fi
done
done
}
# main ()
# {
while [ "$#" -gt '0' ]
do
case "$1" in
# ==============================================================================
# Phase 2: Command-Line Argument Parsing
# ==============================================================================
# Parse options passed to the script (--label, --keep, --threshold, --prefix).
while [ "$#" -gt 0 ]; do
case "$1" in
--keep)
if [ ! "$2" -gt '0' ];
then
echo "keep parameters need to be greater than 0"
exit 1
fi
# Ensure keep count is a valid positive integer
case "$2" in
''|*[!0-9]*|0)
echo "Error: --keep requires a number greater than 0" >&2
exit 1
;;
esac
opt_keep="$2"
shift 2
;;
@@ -65,67 +31,132 @@ do
shift 2
;;
--threshold)
if [ ! "$2" -ge '0' ];
then
echo "threshold parameters need to be equal or greater than 0"
exit 1
fi
# Ensure threshold is a valid non-negative integer (in bytes)
case "$2" in
''|*[!0-9]*)
echo "Error: --threshold requires a number equal or greater than 0" >&2
exit 1
;;
esac
opt_threshold="$2"
shift 2
;;
--prefix)
opt_prefix="$2"
shift 2
;;
*)
# Display usage and exit if an unknown option is encountered
echo "Usage: $0 [--label LABEL] [--keep NUM] [--threshold BYTES] [--prefix PREFIX]" >&2
exit 1
;;
esac
done
ZPOOL_STATUS=$(env LC_ALL=C zpool status 2>&1 )
ZFS_LIST=$(env LC_ALL=C zfs list -t volume,filesystem -H)
CANDIDATES=$(echo "$ZFS_LIST" | awk -F '\t' '{print $1}')
SNAPSHOTS_OLD=$(env LC_ALL=C zfs list -H -t snapshot -S creation -o name)
# ==============================================================================
# Phase 3: Inspect ZFS Pools and Datasets
# ==============================================================================
# Query zpool status once to check pool states and active operations.
ZPOOL_STATUS=$(env LC_ALL=C zpool status 2>&1)
# Get a list of pools that are being scrubbed.
ZPOOLS_SCRUBBING=$(echo "$ZPOOL_STATUS" | awk -F ': ' \
'$1 ~ /^ *pool$/ { pool = $2 } ; \
$1 ~ /^ *scan$/ && $2 ~ /scrub in progress/ { print pool }' \
| sort )
# Identify pools that should NOT receive snapshots:
# - Pools currently running a scrub (to avoid additional I/O load).
# - Pools in unhealthy states (anything other than ONLINE or DEGRADED).
ZPOOLS_EXCLUDED=$(echo "$ZPOOL_STATUS" | awk -F ': ' '
$1 ~ /^ *pool$/ { pool = $2 }
$1 ~ /^ *scan$/ && $2 ~ /scrub in progress/ { print pool }
$1 ~ /^ *state$/ && $2 !~ /ONLINE|DEGRADED/ { print pool }
' | sort -u)
# Get a list of pools that cannot do a snapshot.
ZPOOLS_NOTREADY=$(echo "$ZPOOL_STATUS" | awk -F ': ' \
'$1 ~ /^ *pool$/ { pool = $2 } ; \
$1 ~ /^ *state$/ && $2 !~ /ONLINE|DEGRADED/ { print pool } ' \
| sort)
# Retrieve list of all candidate ZFS filesystems and volumes.
CANDIDATES=$(env LC_ALL=C zfs list -H -o name -t filesystem,volume)
# Initialize the list of datasets that will get snapshots.
# Retrieve all existing snapshots, sorted newest first (-S creation).
SNAPSHOTS_OLD=$(env LC_ALL=C zfs list -H -o name -t snapshot -S creation)
# ==============================================================================
# Phase 4: Filter Target Datasets
# ==============================================================================
# Filter candidate datasets, skipping any that belong to an excluded pool.
TARGETS=''
for ii in $CANDIDATES; do
# Extract root pool name from the dataset path (e.g. 'tank' from 'tank/data')
pool="${ii%%/*}"
skip=0
for ii in $CANDIDATES
do
# Exclude datasets in pools that cannot do a snapshot.
for jj in $ZPOOLS_NOTREADY
do
if [ "$(echo "$ii" | awk -F/ '{print $1}')" = "$jj" ]
then
continue 2
fi
done
for jj in $ZPOOLS_SCRUBBING
do
if [ "$(echo "$ii" | awk -F/ '{print $1}')" = "$jj" ]
then
continue 2
for jj in $ZPOOLS_EXCLUDED; do
if [ "$pool" = "$jj" ]; then
skip=1
break
fi
done
TARGETS="$TARGETS $ii"
if [ "$skip" -eq 0 ]; then
TARGETS="$TARGETS $ii"
fi
done
# ==============================================================================
# Phase 5: Generate Snapshot Names and Patterns
# ==============================================================================
# UTC timestamp formatted as YYYY-MM-DD-HH-MM (16 characters)
DATE=$(date -u +%F-%H-%M)
DATE=$(date --utc +%F-%H-%M)
# Name for the new snapshot: <prefix>_<label>_<timestamp>
SNAPNAME="${opt_prefix}_${opt_label}_$DATE"
# The expression for matching old snapshots.
# Pattern matching old snapshots with this prefix and label
SNAPGLOB="${opt_prefix}_${opt_label}_????-??-??-??-??"
do_snapshots "$SNAPNAME" "$SNAPGLOB" "$TARGETS"
# ==============================================================================
# Phase 6: Snapshot Creation and Pruning Function
# ==============================================================================
# For each target dataset: create new snapshot and prune old snapshots.
do_snapshots ()
{
for ii in $TARGETS; do
KEEP="$opt_keep"
# Check write threshold if specified, or snapshot directly
if [ "$opt_threshold" -eq 0 ]; then
if ! zfs snapshot "$ii@$SNAPNAME"; then
# If snapshot creation fails, skip pruning this dataset to prevent data loss
continue
fi
else
# Conditional snapshot: check bytes written since the last snapshot
bytes_written=$(zfs get -Hp -o value written "$ii" 2>/dev/null)
if [ -n "$bytes_written" ] && [ "$bytes_written" -ge "$opt_threshold" ] 2>/dev/null; then
# Threshold reached: create new snapshot
if ! zfs snapshot "$ii@$SNAPNAME"; then
# If snapshot creation fails, skip pruning
continue
fi
else
# Threshold not met: no new snapshot taken, so increment KEEP
# to preserve existing retained snapshots without deleting one
KEEP=$(( KEEP + 1 ))
fi
fi
# Prune older snapshots matching the current label and prefix.
# Note: SNAPSHOTS_OLD is sorted newest to oldest.
for jj in $SNAPSHOTS_OLD; do
# Match snapshots belonging to dataset $ii with matching prefix and label
if [ -z "${jj#$ii@$SNAPGLOB}" ]; then
KEEP=$(( KEEP - 1 ))
# Once the retention quota is exhausted, destroy excess older snapshots
if [ "$KEEP" -le 0 ]; then
zfs destroy -d "$jj"
fi
fi
done
done
}
# ==============================================================================
# Phase 7: Execution
# ==============================================================================
do_snapshots
exit 0
# }