Skip to content

Instantly share code, notes, and snippets.

@frederikstroem
Last active June 9, 2024 10:06
Show Gist options
  • Save frederikstroem/cfbaa95296b25860523d20509eccf20f to your computer and use it in GitHub Desktop.
Save frederikstroem/cfbaa95296b25860523d20509eccf20f to your computer and use it in GitHub Desktop.
ZFS snapshot to Backblaze B2 rclone remote backup script. Work in progress code snapshot from self-hosted infrastructure development.
#!/bin/bash
# _ _ _
# (_)_ __ (_) |_
# | | '_ \| | __|
# | | | | | | |_
# |_|_| |_|_|\__|
# Make sure running as root.
if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root." 1>&2
exit 1
fi
# Set the working directory to the directory of the script.
# https://stackoverflow.com/a/3355423/2612383
cd "$(dirname "$0")" || exit
# _
# _ __ ___| | ___ _ __ ___
# | '__/ __| |/ _ \| '_ \ / _ \
# | | | (__| | (_) | | | | __/
# |_| \___|_|\___/|_| |_|\___|
# Set rclone configuration file path.
RCLONE_CONFIG_FILE="./rclone.conf"
# The number of file transfers to run in parallel.
# https://rclone.org/docs/#transfers-n
# https://rclone.org/b2/
# https://www.backblaze.com/blog/increasing-thread-count-useful-in-sheets-and-cloud-storage-speed/
RCLONE_PARALLEL_TRANSFERS=32
# Set rclone base sync command with config flag.
# https://rclone.org/docs/
# https://rclone.org/flags/
# https://rclone.org/commands/rclone_sync/
RCLONE_CMD="rclone --config $RCLONE_CONFIG_FILE sync"
# Set rclone base sync flags.
# https://rclone.org/crypt/
# https://rclone.org/flags/
RCLONE_SYNC_FLAGS="--copy-links --fast-list --transfers $RCLONE_PARALLEL_TRANSFERS"
# Define rclone sync function.
# Example usage:
# `rclone_sync "/path/to/source" "remote:/path/to/destination" "--filter-from filter-file"`
# or without optional flags
# `rclone_sync "/path/to/source" "remote:/path/to/destination"`
rclone_sync() {
# Set source and destination paths.
SOURCE_PATH="$1"
DESTINATION_PATH="$2"
# Set optional rclone sync flags.
# https://rclone.org/filtering/
RCLONE_SYNC_FLAGS_OPTIONAL="$3"
# Create an array to hold the command and its flags.
RCLONE_CMD_ARRAY=($RCLONE_CMD $RCLONE_SYNC_FLAGS)
# Append optional flags if provided.
if [ -n "$RCLONE_SYNC_FLAGS_OPTIONAL" ]; then
RCLONE_CMD_ARRAY+=($RCLONE_SYNC_FLAGS_OPTIONAL)
fi
# Append source and destination paths.
RCLONE_CMD_ARRAY+=("$SOURCE_PATH" "$DESTINATION_PATH")
# Run the rclone sync command.
"${RCLONE_CMD_ARRAY[@]}"
# Check if the rclone command was successful.
if [ $? -eq 0 ]; then
echo "Rclone sync nominal for $SOURCE_PATH to $DESTINATION_PATH."
else
echo "Rclone sync failed for $SOURCE_PATH to $DESTINATION_PATH." 1>&2
fi
}
# __________ ____
# |__ / ___/ ___|
# / /| |_ \___ \
# / /_| _| ___) |
# /____|_| |____/
# Set ZFS snapshot read-only mount root directory.
SNAPSHOT_MOUNT_ROOT="/mnt/cloud_backup_snapshots"
# ZFS snapshot prefix.
SNAPSHOT_PREFIX="service_cloud_backup"
# ZFS snapshot creation function.
create_zfs_snapshot() {
# The ZFS dataset path to snapshot.
DATASET_PATH="$1"
# Remove the leading slash from the dataset path, if present.
DATASET_PATH="${DATASET_PATH#/}"
# Generate a timestamp to uniquely identify the snapshot.
TIMESTAMP=$(date +"%Y%m%d%H%M%S")
# Construct the snapshot name using the dataset path and timestamp.
SNAPSHOT_NAME="${DATASET_PATH}@${SNAPSHOT_PREFIX}_${TIMESTAMP}"
# Create the ZFS snapshot.
zfs snapshot "$SNAPSHOT_NAME"
if [ $? -ne 0 ]; then
echo "Failed to create ZFS snapshot for $DATASET_PATH" 1>&2
return 1
fi
echo "$SNAPSHOT_NAME"
}
# ZFS snapshot read-only mount function.
mount_zfs_snapshot() {
# The name of the snapshot to mount.
SNAPSHOT_NAME="$1"
# Construct the mount path using the snapshot name, replacing '@' with '/'.
MOUNT_PATH="${SNAPSHOT_MOUNT_ROOT}/$(echo "$SNAPSHOT_NAME" | tr '@' '/')"
# Create the mount directory.
mkdir -p "$MOUNT_PATH"
if [ $? -ne 0 ]; then
echo "Failed to create mount directory $MOUNT_PATH" 1>&2
return 1
fi
# Mount the ZFS snapshot as read-only.
mount -t zfs -o ro "$SNAPSHOT_NAME" "$MOUNT_PATH"
if [ $? -ne 0 ]; then
echo "Failed to mount ZFS snapshot $SNAPSHOT_NAME to $MOUNT_PATH" 1>&2
return 1
fi
echo "$MOUNT_PATH"
}
# ZFS cleanup function.
# Unmount ZFS snapshot, remove nested mount directory, and destroy snapshots with the ZFS snapshot prefix.
zfs_cleanup() {
# The mount path to unmount.
MOUNT_PATH="$1"
# The ZFS dataset path to snapshot.
DATASET_PATH="$2"
# Unmount the ZFS snapshot.
umount "$MOUNT_PATH"
if [ $? -ne 0 ]; then
echo "Failed to unmount $MOUNT_PATH" 1>&2
return 1
fi
# Remove the leading slash from the dataset path, if present.
DATASET_PATH="${DATASET_PATH#/}"
# Identify the first directory in the dataset path.
DATASET_FIRST_DIR=$(echo "$DATASET_PATH" | cut -d'/' -f1)
# Construct the path to the root mount directory.
MOUNT_PATH_ROOT="${SNAPSHOT_MOUNT_ROOT}/${DATASET_FIRST_DIR}"
# Remove the nested mount directory.
rm -rf "$MOUNT_PATH_ROOT"
if [ $? -ne 0 ]; then
echo "Failed to remove mount directory $MOUNT_PATH_ROOT" 1>&2
return 1
fi
# Check if there are any snapshots to delete.
SNAPSHOTS=$(zfs list -H -t snapshot -o name -r "$DATASET_PATH" | grep "@$SNAPSHOT_PREFIX")
if [ -n "$SNAPSHOTS" ]; then
# Destroy snapshots with the ZFS snapshot prefix for the specific dataset path.
echo "$SNAPSHOTS" | xargs -n 1 zfs destroy -r
if [ $? -ne 0 ]; then
echo "Failed to destroy ZFS snapshots with prefix $SNAPSHOT_PREFIX for dataset $DATASET_PATH" 1>&2
return 1
fi
else
echo "No snapshots found with prefix $SNAPSHOT_PREFIX for dataset $DATASET_PATH" 1>&2
fi
}
# Example usage of the above functions:
# ```
# DATASET_PATH="/path/to/dataset"
# DEST_PATH="my-service:/"
# SNAPSHOT_NAME=$(create_zfs_snapshot "$DATASET_PATH")
# if [ $? -eq 0 ]; then
# MOUNT_PATH=$(mount_zfs_snapshot "$SNAPSHOT_NAME")
# if [ $? -eq 0 ]; then
# rclone_sync "$MOUNT_PATH" "$DEST_PATH"
# zfs_cleanup "$MOUNT_PATH" "$DATASET_PATH"
# fi
# fi
# ```
# Where `my-service` is the name of the rclone remote as defined in the rclone.conf file.
# _
# _ __ ___ __ _(_)_ __
# | '_ ` _ \ / _` | | '_ \
# | | | | | | (_| | | | | |
# |_| |_| |_|\__,_|_|_| |_|
# Backup "/tank/b2_backup_test" to "test-remote" rclone remote.
DATASET_PATH="/tank/b2_backup_test"
DEST_PATH="test-remote:/"
SNAPSHOT_NAME=$(create_zfs_snapshot "$DATASET_PATH")
if [ $? -eq 0 ]; then
MOUNT_PATH=$(mount_zfs_snapshot "$SNAPSHOT_NAME")
if [ $? -eq 0 ]; then
rclone_sync "$MOUNT_PATH" "$DEST_PATH"
zfs_cleanup "$MOUNT_PATH" "$DATASET_PATH"
fi
fi
[Unit]
Description=Service for Cloud Backup using ZFS snapshots and rclone
After=network.target
[Service]
Type=oneshot
ExecStart=/opt/cloud_backup_service/backup.sh
User=root
Group=root
[Unit]
Description=Timer for Cloud Backup Service
[Timer]
OnCalendar=00:15
AccuracySec=15m
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.target
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment