Skip to content
View in the app

A better way to browse. Learn more.

Unraid

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Is this ZFS snapshot cleanup script a good solution?

Featured Replies

Hi everyone,

 

I’m running my drives in a ZFS RAID on my Unraid server, and I’ve noticed that the number of snapshots keeps growing. To manage this, I want to periodically clean up old snapshots while keeping a specified number of recent ones.

 

I found the following script, which seems to address the problem by pruning snapshots and limiting their quantity to a defined number (24 in this case).

 

Could you let me know if this is a good approach or if there’s a better alternative for managing ZFS snapshots on Unraid?

 

Here’s the full script:

 

#!/bin/bash

 

#v0.10.2

########################unraid-snapshot-zfs#######################

###################### User Defined Options ######################

# List your ZFS datasets.  You can add/remove sets

# readarray -t DATASETS < <(zfs list -o name -H)  # when replacing DATASETS below, should return all pools/Datasets, not thoroughly tested yet.  If you test this let me know!

DATASET=("zfsbackup/cachebackup")

mapfile -t DATASETS < <(zfs list -r -o name -H "${DATASET}")

 

# Set Number of Snapshots to Keep

SHANPSHOT_QTY=24

 

# Snapshot Name Format

SNAPSHOTNAME=$(date "+simplesnap_%Y-%m-%d-%H:%M:%S")

 

###### Don't change below unless you know what you're doing ######

##################################################################

 

echo "Starting Snapshot ${SNAPSHOTNAME}"

echo "_____________________________________________________________"

 

## Validation Steps

# Validate SHANPSHOT_QTY

if ! [[ $SHANPSHOT_QTY =~ ^[0-9]+$ ]]; then

  echo "Error: SHANPSHOT_QTY must be a positive integer."

  exit 1

fi

 

# Validate DATASETS

for dataset in "${DATASETS[@]}"; do

  if ! zfs list "$dataset" &>/dev/null; then

    echo "Error: Dataset '$dataset' does not exist."

    exit 1

  fi

done

 

# Function to handle errors

handle_error() {

  local error_message="$1"

  echo "Error: $error_message"

  exit 1

}

 

# Function to create snapshot if there is changed data

create_snapshot_if_changed() {

  local DATASET="$1"

  local WRITTEN

  WRITTEN=$(zfs get -H -o value written "${DATASET}")

 

  if [[ "${WRITTEN}" != "0" ]]; then

    if ! zfs snapshot "${DATASET}@${SNAPSHOTNAME}"; then

      handle_error "Failed to create snapshot for ${DATASET}"

    fi

    echo "Snapshot created: ${DATASET}@${SNAPSHOTNAME}"

  else

    echo "No changes detected in ${DATASET}. No snapshot created."

  fi

}

 

# Function to prune snapshots

prune_snapshots() {

  local DATASET="$1"

  local KEEP="${SHANPSHOT_QTY}"

 

  # Declare the SNAPSHOTS array

  declare -a SNAPSHOTS

  # Use mapfile to split the output into the SNAPSHOTS array

  if ! mapfile -t SNAPSHOTS < <(zfs list -t snapshot -o name -s creation -r "${DATASET}" | grep "^${DATASET}@"); then

    echo "Error: Failed to list snapshots for ${DATASET}"

    return 1

  fi

 

  local SNAPSHOTS_COUNT=${#SNAPSHOTS[@]}

  echo "Total snapshots for ${DATASET}: ${SNAPSHOTS_COUNT}"

 

  local SNAPSHOTS_SPACE

  if ! SNAPSHOTS_SPACE=$(zfs get -H -o value usedbysnapshots "${DATASET}"); then

    echo "Error: Failed to get space used by snapshots for ${DATASET}"

    return 1

  fi

  echo "Space used by snapshots for ${DATASET}: ${SNAPSHOTS_SPACE}"

 

  if [[ ${SNAPSHOTS_COUNT} -gt ${KEEP} ]]; then

    local TO_DELETE=$((SNAPSHOTS_COUNT - KEEP))

    for i in "${SNAPSHOTS[@]:0:${TO_DELETE}}"; do

      if ! zfs destroy "${i}"; then

        echo "Error: Failed to delete snapshot: ${i}"

        return 1

      fi

      echo "Deleted snapshot: ${i}"

      echo "_____________________________________________________________"

    done

  else

    echo "_____________________________________________________________"

  fi

}

 

# Iterate over each dataset and call the functions

for dataset in "${DATASETS[@]}"; do

#  create_snapshot_if_changed "${dataset}"

  prune_snapshots "${dataset}"

done

 

echo "----------------------------Done!----------------------------"

 

Do you think this script is safe and efficient? Can it potentially cause any issues?

Or is there a more reliable/better way to automate snapshot management in a ZFS setup on Unraid?

 

Thanks for your help!

  • 1 month later...

Thank you for the script - unfortunately, I can give you the answer you've asked for.
Did you get any feedback on the script or do you experienced any issues while using it?

Chat-GPT told me that there is maybe a typo:
The variable SHANPSHOT_QTY contains a typo ("SHANPSHOT" instead of "SNAPSHOT"):

## Validation Steps

# Validate SHANPSHOT_QTY

if ! [[ $SHANPSHOT_QTY =~ ^[0-9]+$ ]]; then

  echo "Error: SHANPSHOT_QTY must be a positive integer."

  exit 1

 

I played around a bit with what ChatGPT gave me and tested it using the dry run function (to be honest, I'm a novice when it comes to these scripts).

Added:

  • Dry Run Mode
  • Age-Based Pruning (optional)
  • Error Handling
Quote

Key Features

 

 Dataset Selection:
    Operates on the ZFS datasets specified in the DATASET array and dynamically generates a list of snapshots for each dataset.

   

Snapshot Retention:
    Retains the most recent snapshots, as defined by SNAPSHOT_QTY (e.g., 24 snapshots). Optionally deletes snapshots older than a specified number of months (DELETE_OLDER_THAN_MONTHS).

   

Dry Run Mode:
    When DRY_RUN=true, the script simulates deletions and lists snapshots it would delete without actually removing them.

   

Age-Based Pruning:
    Identifies snapshots older than DELETE_OLDER_THAN_MONTHS for deletion, provided the total number exceeds SNAPSHOT_QTY. Extracts dates from snapshot names (e.g., after @autosnap_YYYY-MM-DD) and calculates the snapshot age.

   

Error Handling:
    Skips snapshots with invalid or unparseable dates and provides warnings for any issues (e.g., missing datasets or invalid snapshot names).

 

----------------------------------------------------------------------------------------------------------------------------------------------------------------

How It Works

   

Dataset Validation:
    Checks that the datasets listed in the DATASET array exist and are valid ZFS datasets.

    Snapshot Listing:
    Uses zfs list to fetch snapshots for each dataset, filtering snapshots matching the @autosnap_YYYY-MM-DD pattern.

   

Age Calculation:
        Extracts the date from each snapshot name (e.g., after @autosnap_).
        Converts the date to a timestamp and calculates its age in months relative to the current date.

   

Pruning Logic:
    Snapshots are deleted if they meet both conditions:
        Age exceeds DELETE_OLDER_THAN_MONTHS.
        Total number of snapshots exceeds SNAPSHOT_QTY.

   

Dry Run or Deletion:
        If DRY_RUN=true, the script lists snapshots that would be deleted without actually deleting them.
        If DRY_RUN=false, the script deletes the qualifying snapshots using zfs destroy.

 

----------------------------------------------------------------------------------------------------------------------------------------------------------------

Usage Instructions

   

Set Configuration Variables:
        Update the DATASET array with your ZFS datasets.
        Adjust SNAPSHOT_QTY (number of snapshots to retain) and DELETE_OLDER_THAN_MONTHS (age threshold) to match your retention policy.
        Set DRY_RUN to true (for testing) or false (for live execution).

   

Run the Script:
    Use the User Scripts add-on to test the script and let it run manual or via cronjob

    Review Output: Check the output to see which snapshots are pruned (or would be pruned in dry run mode).

 

----------------------------------------------------------------------------------------------------------------------------------------------------------------

Example Snapshot Naming Convention

This script assumes snapshot names follow this format:
@autosnap_YYYY-MM-DD

Examples:

    cache_nvme_2tb/zfs_domains_backup/Windows@autosnap_2024-12-30
    cache_nvme_2tb/zfs_domains_backup/Windows@autosnap_2025-01-11

#!/bin/bash

# v0.10.4

########################unraid-snapshot-zfs#######################

###################### User Defined Options ######################

# Enable or disable dry-run mode (set to true for testing without making changes)
DRY_RUN=true  # Set to false to perform actual operations

# Enable or disable deletion of snapshots older than a specified number of months
DELETE_OLD_SNAPSHOTS=false  # Set to false to skip deleting old snapshots

# Set the age threshold for old snapshot deletion (in months)
DELETE_OLDER_THAN_MONTHS=12  # Change this to the desired number of months

# List your ZFS datasets. You can add/remove sets
DATASET=("/mnt/my_path_to_DATASET")
mapfile -t DATASETS < <(zfs list -r -o name -H "${DATASET}")

# Set Number of Snapshots to Keep
SNAPSHOT_QTY=24

# Snapshot Name Format
SNAPSHOTNAME=$(date "+simplesnap_%Y-%m-%d-%H:%M:%S")

###### Don't change below unless you know what you're doing ######
##################################################################

echo "Starting Snapshot ${SNAPSHOTNAME}"
echo "Dry-run mode: ${DRY_RUN}"
echo "Delete old snapshots: ${DELETE_OLD_SNAPSHOTS}"
echo "Delete snapshots older than: ${DELETE_OLDER_THAN_MONTHS} months"
echo "_____________________________________________________________"

## Validation Steps
# Validate SNAPSHOT_QTY
if ! [[ $SNAPSHOT_QTY =~ ^[0-9]+$ ]]; then
  echo "Error: SNAPSHOT_QTY must be a positive integer."
  exit 1
fi

# Validate DATASETS
for dataset in "${DATASETS[@]}"; do
  if ! zfs list "$dataset" &>/dev/null; then
    echo "Error: Dataset '$dataset' does not exist."
    exit 1
  fi
done

# Function to handle errors
handle_error() {
  local error_message="$1"
  echo "Error: $error_message"
  exit 1
}

# Function to create snapshot if there is changed data
create_snapshot_if_changed() {
  local DATASET="$1"
  local WRITTEN
  WRITTEN=$(zfs get -H -o value written "${DATASET}")

  if [[ "${WRITTEN}" != "0" ]]; then
    if [[ "$DRY_RUN" == "true" ]]; then
      echo "[DRY RUN] Would create snapshot: ${DATASET}@${SNAPSHOTNAME}"
    else
      if ! zfs snapshot "${DATASET}@${SNAPSHOTNAME}"; then
        handle_error "Failed to create snapshot for ${DATASET}"
      fi
      echo "Snapshot created: ${DATASET}@${SNAPSHOTNAME}"
    fi
  else
    echo "No changes detected in ${DATASET}. No snapshot created."
  fi
}

# Function to prune snapshots
prune_snapshots() {
  local DATASET="$1"
  local KEEP="${SNAPSHOT_QTY}"

  # Declare the SNAPSHOTS array
  declare -a SNAPSHOTS
  # Use mapfile to split the output into the SNAPSHOTS array
  if ! mapfile -t SNAPSHOTS < <(zfs list -t snapshot -o name,creation -s creation -r "${DATASET}" | grep "^${DATASET}@"); then
    echo "Error: Failed to list snapshots for ${DATASET}"
    return 1
  fi

  local SNAPSHOTS_COUNT=${#SNAPSHOTS[@]}
  echo "Total snapshots for ${DATASET}: ${SNAPSHOTS_COUNT}"

  local SNAPSHOTS_SPACE
  if ! SNAPSHOTS_SPACE=$(zfs get -H -o value usedbysnapshots "${DATASET}"); then
    echo "Error: Failed to get space used by snapshots for ${DATASET}"
    return 1
  fi
  echo "Space used by snapshots for ${DATASET}: ${SNAPSHOTS_SPACE}"

  local NOW=$(date +%s)

  # Delete snapshots older than the specified number of months
  if [[ "$DELETE_OLD_SNAPSHOTS" == "true" ]]; then
    echo "Checking for snapshots older than ${DELETE_OLDER_THAN_MONTHS} months..."
    for snapshot_info in "${SNAPSHOTS[@]}"; do
      # Extract the snapshot name and creation date
      local snapshot_name=$(echo "$snapshot_info" | awk '{print $1}')
      local snapshot_creation_date=$(echo "$snapshot_info" | awk '{print $2}')
      local snapshot_timestamp=$(date -d "$snapshot_creation_date" +%s)

      # Calculate age in months
      local age_in_months=$(( (NOW - snapshot_timestamp) / (60 * 60 * 24 * 30) ))  # Approximate 30 days per month

      if [[ $age_in_months -ge $DELETE_OLDER_THAN_MONTHS ]]; then
        if [[ "$DRY_RUN" == "true" ]]; then
          echo "[DRY RUN] Would delete old snapshot: ${snapshot_name} (Age: ${age_in_months} months)"
        else
          if ! zfs destroy "${snapshot_name}"; then
            echo "Error: Failed to delete snapshot: ${snapshot_name}"
            return 1
          fi
          echo "Deleted old snapshot: ${snapshot_name} (Age: ${age_in_months} months)"
        fi
      fi
    done
  fi

  # Prune snapshots to retain only the desired quantity
  if [[ ${SNAPSHOTS_COUNT} -gt ${KEEP} ]]; then
    local TO_DELETE=$((SNAPSHOTS_COUNT - KEEP))
    for i in "${SNAPSHOTS[@]:0:${TO_DELETE}}"; do
      local snapshot_name=$(echo "$i" | awk '{print $1}')
      if [[ "$DRY_RUN" == "true" ]]; then
        echo "[DRY RUN] Would delete snapshot: ${snapshot_name}"
      else
        if ! zfs destroy "${snapshot_name}"; then
          echo "Error: Failed to delete snapshot: ${snapshot_name}"
          return 1
        fi
        echo "Deleted snapshot: ${snapshot_name}"
        echo "_____________________________________________________________"
      fi
    done
  else
    echo "_____________________________________________________________"
  fi
}

# Iterate over each dataset and call the functions
for dataset in "${DATASETS[@]}"; do
  create_snapshot_if_changed "${dataset}"  # Uncommented to enable snapshot creation
  prune_snapshots "${dataset}"
done

echo "----------------------------Done!----------------------------"

 

Edited by gilladur

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.