safer implementation
This commit is contained in:
24
Makefile
24
Makefile
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
45
README.md
45
README.md
@@ -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.
|
||||||
|
|
||||||
in src folder, before installing, you can customize some parameters
|
## CLI Parameters
|
||||||
--keep indicates the number of snapshots to save. Default to 10
|
- `--keep`: Number of snapshots to preserve (default: 10).
|
||||||
--label indicates the label of snapshots. Default to defaultLabel
|
- `--label`: Label tag for snapshots (default: `defaultLabel`).
|
||||||
--threshold indicates how many bytes need to be written in a dataset after the last snapshot to allow new snapshots. Default 0
|
- `--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`).
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
#!/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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
case "$1" in
|
||||||
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
|
|
||||||
--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)
|
||||||
exit 1
|
echo "Error: --keep requires a number greater than 0" >&2
|
||||||
fi
|
exit 1
|
||||||
|
;;
|
||||||
|
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]*)
|
||||||
exit 1
|
echo "Error: --threshold requires a number equal or greater than 0" >&2
|
||||||
fi
|
exit 1
|
||||||
|
;;
|
||||||
|
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
|
||||||
|
|
||||||
ZPOOL_STATUS=$(env LC_ALL=C zpool status 2>&1 )
|
# ==============================================================================
|
||||||
ZFS_LIST=$(env LC_ALL=C zfs list -t volume,filesystem -H)
|
# Phase 3: Inspect ZFS Pools and Datasets
|
||||||
CANDIDATES=$(echo "$ZFS_LIST" | awk -F '\t' '{print $1}')
|
# ==============================================================================
|
||||||
SNAPSHOTS_OLD=$(env LC_ALL=C zfs list -H -t snapshot -S creation -o name)
|
# 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.
|
# 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
|
||||||
|
|
||||||
TARGETS="$TARGETS $ii"
|
if [ "$skip" -eq 0 ]; then
|
||||||
|
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
|
||||||
# }
|
|
||||||
|
|||||||
Reference in New Issue
Block a user