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:
install:
@@ -7,20 +9,20 @@ install:
install -d /etc/cron.weekly
install -d /etc/cron.monthly
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 0744 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 0744 etc/zfs-auto-snapshot.cron.monthly /etc/cron.monthly/zfs-auto-snapshot
install -m 0755 etc/zfs-auto-snapshot.cron.hourly /etc/cron.hourly/zfs-auto-snapshot
install -m 0755 etc/zfs-auto-snapshot.cron.daily /etc/cron.daily/zfs-auto-snapshot
install -m 0755 etc/zfs-auto-snapshot.cron.weekly /etc/cron.weekly/zfs-auto-snapshot
install -m 0755 etc/zfs-auto-snapshot.cron.monthly /etc/cron.monthly/zfs-auto-snapshot
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:
rm /etc/cron.d/zfs-auto-snapshot
rm /etc/cron.hourly/zfs-auto-snapshot
rm /etc/cron.daily/zfs-auto-snapshot
rm /etc/cron.weekly/zfs-auto-snapshot
rm /etc/cron.monthly/zfs-auto-snapshot
rm /usr/local/sbin/zfs-auto-snapshot
rm -f /etc/cron.d/zfs-auto-snapshot
rm -f /etc/cron.hourly/zfs-auto-snapshot
rm -f /etc/cron.daily/zfs-auto-snapshot
rm -f /etc/cron.weekly/zfs-auto-snapshot
rm -f /etc/cron.monthly/zfs-auto-snapshot
rm -f /usr/local/sbin/zfs-auto-snapshot

View File

@@ -1,33 +1,36 @@
# README #
zfs-auto-snapshot script creates automatic snapshots of all zfs filesystems and volumes.
The script will automatically rotate snapshots deleting old ones.
The script will temporarily avoid backups on damaged pools or pools with scrubbing going on.
`zfs-auto-snapshot` automatically creates and rotates periodic snapshots of all ZFS filesystems and volumes.
The script automatically rotates snapshots by pruning older ones beyond the retention limit.
The script temporarily skips snapshots on damaged pools or pools with an active scrub.
It creates:
4 snapshots per hour to rotate every 15 minutes
24 hourly snapshots
30 daily snapshots
12 weekly snapshots
12 yearly snapshots
- 4 snapshots per hour (rotated every 15 minutes)
- 24 hourly snapshots
- 31 daily snapshots
- 12 weekly snapshots
- 12 monthly snapshots
Script has been tested on zfs on linux 0.8.3, centos 7
Original script from https://github.com/zfsonlinux/zfs-auto-snapshot
I semplified the code and fixed some bugs
Script is tested on Linux (AlmaLinux 9, CentOS/RHEL 7-9, OpenZFS 2.x / 0.8.x).
Originally adapted from https://github.com/zfsonlinux/zfs-auto-snapshot
# to install
cd to current folder
## Installation
```sh
make install
```
# to uninstall
cd to current folder
## Uninstallation
```sh
make uninstall
```
# NOTE #
uninstalling the script doesn't delete made snapshots
> [!NOTE]
> 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
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
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

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
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
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

View File

@@ -1,7 +1,7 @@
#!/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
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

View File

@@ -1,7 +1,7 @@
#!/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
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

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
# ==============================================================================
# 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"
# 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
fi
;;
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"
# 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
fi
;;
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
# ==============================================================================
# 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)
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.
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
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
# }