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,3 +1,5 @@
.PHONY: all install uninstall
all: all:
install: install:
@@ -7,20 +9,20 @@ install:
install -d /etc/cron.weekly install -d /etc/cron.weekly
install -d /etc/cron.monthly install -d /etc/cron.monthly
install -m 0644 etc/zfs-auto-snapshot.cron.frequent /etc/cron.d/zfs-auto-snapshot install -m 0644 etc/zfs-auto-snapshot.cron.frequent /etc/cron.d/zfs-auto-snapshot
install -m 0744 etc/zfs-auto-snapshot.cron.hourly /etc/cron.hourly/zfs-auto-snapshot install -m 0755 etc/zfs-auto-snapshot.cron.hourly /etc/cron.hourly/zfs-auto-snapshot
install -m 0744 etc/zfs-auto-snapshot.cron.daily /etc/cron.daily/zfs-auto-snapshot install -m 0755 etc/zfs-auto-snapshot.cron.daily /etc/cron.daily/zfs-auto-snapshot
install -m 0744 etc/zfs-auto-snapshot.cron.weekly /etc/cron.weekly/zfs-auto-snapshot install -m 0755 etc/zfs-auto-snapshot.cron.weekly /etc/cron.weekly/zfs-auto-snapshot
install -m 0744 etc/zfs-auto-snapshot.cron.monthly /etc/cron.monthly/zfs-auto-snapshot install -m 0755 etc/zfs-auto-snapshot.cron.monthly /etc/cron.monthly/zfs-auto-snapshot
install -d /usr/local/sbin install -d /usr/local/sbin
install -m 0744 src/zfs-auto-snapshot.sh /usr/local/sbin/zfs-auto-snapshot install -m 0755 src/zfs-auto-snapshot.sh /usr/local/sbin/zfs-auto-snapshot
uninstall: uninstall:
rm /etc/cron.d/zfs-auto-snapshot rm -f /etc/cron.d/zfs-auto-snapshot
rm /etc/cron.hourly/zfs-auto-snapshot rm -f /etc/cron.hourly/zfs-auto-snapshot
rm /etc/cron.daily/zfs-auto-snapshot rm -f /etc/cron.daily/zfs-auto-snapshot
rm /etc/cron.weekly/zfs-auto-snapshot rm -f /etc/cron.weekly/zfs-auto-snapshot
rm /etc/cron.monthly/zfs-auto-snapshot rm -f /etc/cron.monthly/zfs-auto-snapshot
rm /usr/local/sbin/zfs-auto-snapshot rm -f /usr/local/sbin/zfs-auto-snapshot

View File

@@ -1,33 +1,36 @@
# README # # README #
zfs-auto-snapshot script creates automatic snapshots of all zfs filesystems and volumes. `zfs-auto-snapshot` automatically creates and rotates periodic snapshots of all ZFS filesystems and volumes.
The script will automatically rotate snapshots deleting old ones. The script automatically rotates snapshots by pruning older ones beyond the retention limit.
The script will temporarily avoid backups on damaged pools or pools with scrubbing going on. The script temporarily skips snapshots on damaged pools or pools with an active scrub.
It creates: It creates:
4 snapshots per hour to rotate every 15 minutes - 4 snapshots per hour (rotated every 15 minutes)
24 hourly snapshots - 24 hourly snapshots
30 daily snapshots - 31 daily snapshots
12 weekly snapshots - 12 weekly snapshots
12 yearly snapshots - 12 monthly snapshots
Script has been tested on zfs on linux 0.8.3, centos 7 Script is tested on Linux (AlmaLinux 9, CentOS/RHEL 7-9, OpenZFS 2.x / 0.8.x).
Original script from https://github.com/zfsonlinux/zfs-auto-snapshot Originally adapted from https://github.com/zfsonlinux/zfs-auto-snapshot
I semplified the code and fixed some bugs
# to install ## Installation
cd to current folder ```sh
make install make install
```
# to uninstall ## Uninstallation
cd to current folder ```sh
make uninstall make uninstall
```
# NOTE # > [!NOTE]
uninstalling the script doesn't delete made snapshots > Uninstalling the script does not delete existing snapshots.
## CLI Parameters
- `--keep`: Number of snapshots to preserve (default: 10).
- `--label`: Label tag for snapshots (default: `defaultLabel`).
- `--threshold`: Minimum bytes written to the dataset since the last snapshot to trigger a new one (default: 0).
- `--prefix`: Snapshot name prefix (default: `zfs-auto-snap`).
in src folder, before installing, you can customize some parameters
--keep indicates the number of snapshots to save. Default to 10
--label indicates the label of snapshots. Default to defaultLabel
--threshold indicates how many bytes need to be written in a dataset after the last snapshot to allow new snapshots. Default 0

View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
PATH=$PATH:/usr/local/sbin PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
# Only call zfs-auto-snapshot if it's available # Only call zfs-auto-snapshot if it's available
which zfs-auto-snapshot > /dev/null || exit 0 command -v zfs-auto-snapshot >/dev/null 2>&1 || exit 0
exec zfs-auto-snapshot --label daily --keep 31 exec zfs-auto-snapshot --label daily --keep 31

View File

@@ -1,3 +1,3 @@
PATH="/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin" PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
*/15 * * * * root which zfs-auto-snapshot > /dev/null || exit 0 ; zfs-auto-snapshot --label quarterhour --keep 4 */15 * * * * root command -v zfs-auto-snapshot >/dev/null 2>&1 && exec zfs-auto-snapshot --label quarterhour --keep 4

View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
PATH=$PATH:/usr/local/sbin PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
# Only call zfs-auto-snapshot if it's available # Only call zfs-auto-snapshot if it's available
which zfs-auto-snapshot > /dev/null || exit 0 command -v zfs-auto-snapshot >/dev/null 2>&1 || exit 0
exec zfs-auto-snapshot --label hourly --keep 24 exec zfs-auto-snapshot --label hourly --keep 24

View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
PATH=$PATH:/usr/local/sbin PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
# Only call zfs-auto-snapshot if it's available # Only call zfs-auto-snapshot if it's available
which zfs-auto-snapshot > /dev/null || exit 0 command -v zfs-auto-snapshot >/dev/null 2>&1 || exit 0
exec zfs-auto-snapshot --label monthly --keep 12 exec zfs-auto-snapshot --label monthly --keep 12

View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
PATH=$PATH:/usr/local/sbin PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
# Only call zfs-auto-snapshot if it's available # Only call zfs-auto-snapshot if it's available
which zfs-auto-snapshot > /dev/null || exit 0 command -v zfs-auto-snapshot >/dev/null 2>&1 || exit 0
exec zfs-auto-snapshot --label weekly --keep 12 exec zfs-auto-snapshot --label weekly --keep 12

View File

@@ -1,62 +1,28 @@
#!/bin/sh #!/bin/sh
opt_prefix='zfs-auto-snap' # ==============================================================================
opt_keep=10 # Phase 1: Default Configuration
opt_label="defaultLabel" # ==============================================================================
opt_threshold=0 # 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 () # ==============================================================================
{ # Phase 2: Command-Line Argument Parsing
local KEEP='' # ==============================================================================
# Parse options passed to the script (--label, --keep, --threshold, --prefix).
for ii in $TARGETS while [ "$#" -gt 0 ]; do
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 case "$1" in
--keep) --keep)
if [ ! "$2" -gt '0' ]; # Ensure keep count is a valid positive integer
then case "$2" in
echo "keep parameters need to be greater than 0" ''|*[!0-9]*|0)
echo "Error: --keep requires a number greater than 0" >&2
exit 1 exit 1
fi ;;
esac
opt_keep="$2" opt_keep="$2"
shift 2 shift 2
;; ;;
@@ -65,67 +31,132 @@ do
shift 2 shift 2
;; ;;
--threshold) --threshold)
if [ ! "$2" -ge '0' ]; # Ensure threshold is a valid non-negative integer (in bytes)
then case "$2" in
echo "threshold parameters need to be equal or greater than 0" ''|*[!0-9]*)
echo "Error: --threshold requires a number equal or greater than 0" >&2
exit 1 exit 1
fi ;;
esac
opt_threshold="$2" opt_threshold="$2"
shift 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 esac
done done
# ==============================================================================
# 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) 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)
# Get a list of pools that are being scrubbed. # Identify pools that should NOT receive snapshots:
ZPOOLS_SCRUBBING=$(echo "$ZPOOL_STATUS" | awk -F ': ' \ # - Pools currently running a scrub (to avoid additional I/O load).
'$1 ~ /^ *pool$/ { pool = $2 } ; \ # - Pools in unhealthy states (anything other than ONLINE or DEGRADED).
$1 ~ /^ *scan$/ && $2 ~ /scrub in progress/ { print pool }' \ ZPOOLS_EXCLUDED=$(echo "$ZPOOL_STATUS" | awk -F ': ' '
| sort ) $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. # Retrieve list of all candidate ZFS filesystems and volumes.
ZPOOLS_NOTREADY=$(echo "$ZPOOL_STATUS" | awk -F ': ' \ CANDIDATES=$(env LC_ALL=C zfs list -H -o name -t filesystem,volume)
'$1 ~ /^ *pool$/ { pool = $2 } ; \
$1 ~ /^ *state$/ && $2 !~ /ONLINE|DEGRADED/ { print pool } ' \
| sort)
# 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='' 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 for jj in $ZPOOLS_EXCLUDED; do
do if [ "$pool" = "$jj" ]; then
# Exclude datasets in pools that cannot do a snapshot. skip=1
break
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
fi fi
done done
if [ "$skip" -eq 0 ]; then
TARGETS="$TARGETS $ii" TARGETS="$TARGETS $ii"
fi
done 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" 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}_????-??-??-??-??" 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 exit 0
# }