Last active
June 9, 2024 10:06
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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