diff --git a/Makefile b/Makefile index 10df8b8..007baa0 100644 --- a/Makefile +++ b/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 diff --git a/README.md b/README.md index 2e1edb4..135e92a 100644 --- a/README.md +++ b/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. -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 +## 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`). + diff --git a/etc/zfs-auto-snapshot.cron.daily b/etc/zfs-auto-snapshot.cron.daily index 2403d8b..6f5c249 100644 --- a/etc/zfs-auto-snapshot.cron.daily +++ b/etc/zfs-auto-snapshot.cron.daily @@ -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 diff --git a/etc/zfs-auto-snapshot.cron.frequent b/etc/zfs-auto-snapshot.cron.frequent index d66c404..bb65ed0 100644 --- a/etc/zfs-auto-snapshot.cron.frequent +++ b/etc/zfs-auto-snapshot.cron.frequent @@ -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 diff --git a/etc/zfs-auto-snapshot.cron.hourly b/etc/zfs-auto-snapshot.cron.hourly index 3be78eb..5af13fc 100644 --- a/etc/zfs-auto-snapshot.cron.hourly +++ b/etc/zfs-auto-snapshot.cron.hourly @@ -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 diff --git a/etc/zfs-auto-snapshot.cron.monthly b/etc/zfs-auto-snapshot.cron.monthly index 6be7b12..b81c8dc 100644 --- a/etc/zfs-auto-snapshot.cron.monthly +++ b/etc/zfs-auto-snapshot.cron.monthly @@ -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 diff --git a/etc/zfs-auto-snapshot.cron.weekly b/etc/zfs-auto-snapshot.cron.weekly index 054ce68..30bb806 100644 --- a/etc/zfs-auto-snapshot.cron.weekly +++ b/etc/zfs-auto-snapshot.cron.weekly @@ -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 diff --git a/src/zfs-auto-snapshot.sh b/src/zfs-auto-snapshot.sh index 0d4104a..0c2c03c 100755 --- a/src/zfs-auto-snapshot.sh +++ b/src/zfs-auto-snapshot.sh @@ -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: _