safer implementation
This commit is contained in:
24
Makefile
24
Makefile
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
45
README.md
45
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
# }
|
||||
|
||||
Reference in New Issue
Block a user