From 4f5c090534f5f9fc0b9930a136efdc2e29dc567f Mon Sep 17 00:00:00 2001 From: Joseph Hanson Date: Sat, 4 Jan 2025 19:17:53 -0600 Subject: [PATCH] snap and mount separation --- .../services/zfs-nightly-snap/default.nix | 43 +++++- .../services/zfs-nightly-snap/functions.sh | 68 ++++++++++ .../snap-and-mount-standalone.sh | 15 --- .../zfs-nightly-snap/snap-and-mount.sh | 124 ++---------------- 4 files changed, 120 insertions(+), 130 deletions(-) create mode 100644 nixos/modules/nixos/services/zfs-nightly-snap/functions.sh delete mode 100755 nixos/modules/nixos/services/zfs-nightly-snap/snap-and-mount-standalone.sh mode change 100644 => 100755 nixos/modules/nixos/services/zfs-nightly-snap/snap-and-mount.sh diff --git a/nixos/modules/nixos/services/zfs-nightly-snap/default.nix b/nixos/modules/nixos/services/zfs-nightly-snap/default.nix index 96a034e..85c2417 100644 --- a/nixos/modules/nixos/services/zfs-nightly-snap/default.nix +++ b/nixos/modules/nixos/services/zfs-nightly-snap/default.nix @@ -18,7 +18,48 @@ let ]; text = '' - ${builtins.readFile ./snap-and-mount.sh} "${cfg.mountPath}" "${cfg.zfsDataset}" "${cfg.snapshotName}" + # Import our functions + ${builtins.readFile ./functions.sh} + + BACKUP_DIRECTORY="${cfg.mountPath}" + ZFS_DATASET="${cfg.zfsDataset}" + SNAPSHOT_NAME="${cfg.snapshotName}" + + if [ "$(id -u)" -ne 0 ]; then + echo "Error: This script must be run as root." + exit 1 + fi + + # Main logic + zfs_backup_cleanup "$BACKUP_DIRECTORY" + + echo "Previous snapshot:" + zfs list -t snapshot | grep "$ZFS_DATASET@$SNAPSHOT_NAME" || true + + echo "Attempting to destroy existing snapshot..." + if zfs destroy -r "$ZFS_DATASET@$SNAPSHOT_NAME"; then + echo "Successfully destroyed old snapshot" + else + echo "Failed to destroy existing snapshot" + exit 1 + fi + + # Create new snapshot + if ! zfs snapshot -r "$ZFS_DATASET@$SNAPSHOT_NAME"; then + echo "Failed to create snapshot" + exit 1 + fi + + echo "New snapshot created:" + zfs list -t snapshot | grep "$ZFS_DATASET@$SNAPSHOT_NAME" + + if ! mount_dataset; then + echo "Failed to mount snapshot" + exit 1 + fi + + echo "Successfully created and mounted snapshot at $BACKUP_DIRECTORY" + mount | grep "$BACKUP_DIRECTORY" ''; }; in diff --git a/nixos/modules/nixos/services/zfs-nightly-snap/functions.sh b/nixos/modules/nixos/services/zfs-nightly-snap/functions.sh new file mode 100644 index 0000000..54f7cfd --- /dev/null +++ b/nixos/modules/nixos/services/zfs-nightly-snap/functions.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +# functions sourced from: https://github.com/Jip-Hop/zfs-backup-snapshots +function mount_dataset() { + mkdir -p "$BACKUP_DIRECTORY" + mapfile -t fs < <(zfs list "$ZFS_DATASET" -r -H -o name,mountpoint | grep -E "(legacy)$|(none)$" -v | awk '{print gsub("/","/", $2), $1}' | sort -n | cut -d' ' -f2-) + + for fs in "${fs[@]}"; do + mount_latest_snap "${fs}" "${BACKUP_DIRECTORY}" + done + return 0 +} + +function zfs_backup_cleanup() { + mapfile -t fs < <(tac /etc/mtab | cut -d " " -f 2 | grep "${1}") + + for i in "${fs[@]}"; do + echo "Unmounting $i" + umount "$i" + done + + find "${1}" -type d -empty -delete 2>/dev/null || true +} + +function zfs_latest_snap() { + snapshot=$(zfs list -H -t snapshot -o name -S creation -d1 "${1}" | head -1 | cut -d '@' -f 2) + if [[ -z $snapshot ]]; then + echo "No snapshot exists for ${1}, it will not be backed up." + return 1 + fi + echo "$snapshot" +} + +function zfs_snapshot_mountpoint() { + mountpoint=$(zfs list -H -o mountpoint "${1}") + if [[ $? == 1 ]]; then + return 1 + fi + path="${mountpoint}/.zfs/snapshot/${2}" + if stat "${path}" &>/dev/null; then + echo "${path}" + return 0 + else + return 1 + fi +} + +function mount_latest_snap() { + BACKUP_DIRECTORY="${2}" + filesystem="${1}" + + snapshot=$(zfs_latest_snap "${filesystem}") + if [[ $? == 1 ]]; then + return 1 + fi + + sourcepath=$(zfs_snapshot_mountpoint "${filesystem}" "${snapshot}") + if [[ $? == 1 ]]; then + echo "Cannot find snapshot ${snapshot} for ${filesystem}, perhaps it's not mounted?" + return 1 + fi + + mountpath=${BACKUP_DIRECTORY}/${filesystem} + mkdir -p "${mountpath}" + echo "mount ${sourcepath} => ${mountpath}" + mount --bind --read-only "${sourcepath}" "${mountpath}" + return 0 +} diff --git a/nixos/modules/nixos/services/zfs-nightly-snap/snap-and-mount-standalone.sh b/nixos/modules/nixos/services/zfs-nightly-snap/snap-and-mount-standalone.sh deleted file mode 100755 index 002775b..0000000 --- a/nixos/modules/nixos/services/zfs-nightly-snap/snap-and-mount-standalone.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env nix-shell -#!nix-shell -I nixpkgs=/etc/nix/inputs/nixpkgs -i bash -p busybox zfs -# shellcheck disable=SC1008 - -set -e # Exit on error - -BACKUP_DIRECTORY="/mnt/restic_nightly_backup" -ZFS_DATASET="nahar/containers/volumes" -SNAPSHOT_NAME="restic_nightly_snap" - -# Execute the main script with our parameters -./snap-and-mount.sh \ - "$BACKUP_DIRECTORY" \ - "$ZFS_DATASET" \ - "$SNAPSHOT_NAME" diff --git a/nixos/modules/nixos/services/zfs-nightly-snap/snap-and-mount.sh b/nixos/modules/nixos/services/zfs-nightly-snap/snap-and-mount.sh old mode 100644 new mode 100755 index 030f769..850a751 --- a/nixos/modules/nixos/services/zfs-nightly-snap/snap-and-mount.sh +++ b/nixos/modules/nixos/services/zfs-nightly-snap/snap-and-mount.sh @@ -1,129 +1,27 @@ +#!/usr/bin/env nix-shell +#!nix-shell -I nixpkgs=/etc/nix/inputs/nixpkgs -i bash -p busybox zfs # shellcheck disable=SC1008 -# Command line arguments -BACKUP_DIRECTORY="$1" -ZFS_DATASET="$2" -SNAPSHOT_NAME="$3" +set -e # Exit on error -if [ -z "$BACKUP_DIRECTORY" ] || [ -z "$ZFS_DATASET" ] || [ -z "$SNAPSHOT_NAME" ]; then - echo "Usage: $0 " - exit 1 -fi -# Check if running as root +# Source the functions +. ./functions.sh + +BACKUP_DIRECTORY="${1:-/mnt/restic_nightly_backup}" +ZFS_DATASET="${2:-nahar/containers/volumes}" +SNAPSHOT_NAME="${3:-restic_nightly_snap}" if [ "$(id -u)" -ne 0 ]; then echo "Error: This script must be run as root." exit 1 fi -# functions sourced from: https://github.com/Jip-Hop/zfs-backup-snapshots -# some enhancements made to the original code to adhere to best practices -# mounts all zfs filesystems under $ZFS_DATASET -function mount_dataset() { - # ensure BACKUP_DIRECTORY exists - mkdir -p "$BACKUP_DIRECTORY" - # get list of all zfs filesystems under $ZFS_DATASET - # exclude if mountpoint "legacy" and "none" mountpoint - # order by shallowest mountpoint first (determined by number of slashes) - mapfile -t fs < <(zfs list "$ZFS_DATASET" -r -H -o name,mountpoint | grep -E "(legacy)$|(none)$" -v | awk '{print gsub("/","/", $2), $1}' | sort -n | cut -d' ' -f2-) - - for fs in "${fs[@]}"; do - mount_latest_snap "${fs}" "${BACKUP_DIRECTORY}" - done - return 0 -} - -# umounts and cleans up the backup directory -# usage: zfs_backup_cleanup BACKUP_DIRECTORY -function zfs_backup_cleanup() { - # get all filesystems mounted within the backup directory - mapfile -t fs < <(tac /etc/mtab | cut -d " " -f 2 | grep "${1}") - - # umount said filesystems - for i in "${fs[@]}"; do - echo "Unmounting $i" - umount "$i" - done - - # delete empty directories from within the backup directory - find "${1}" -type d -empty -delete 2>/dev/null || true -} - -# gets the name of the newest snapshot given a zfs filesystem -# usage: get_latest_snap filesystem -function zfs_latest_snap() { - snapshot=$(zfs list -H -t snapshot -o name -S creation -d1 "${1}" | head -1 | cut -d '@' -f 2) - if [[ -z $snapshot ]]; then - # if there's no snapshot then let's ignore it - echo "No snapshot exists for ${1}, it will not be backed up." - return 1 - fi - echo "$snapshot" -} - -# gets the path of a snapshot given a zfs filesystem and a snapshot name -# usage zfs_snapshot_mountpoint filesystem snapshot -function zfs_snapshot_mountpoint() { - # get mountpoint for filesystem - mountpoint=$(zfs list -H -o mountpoint "${1}") - - # exit if filesystem doesn't exist - if [[ $? == 1 ]]; then - return 1 - fi - - # build out path - path="${mountpoint}/.zfs/snapshot/${2}" - - # check to make sure path exists - if stat "${path}" &>/dev/null; then - echo "${path}" - return 0 - else - return 1 - fi -} - -# mounts latest snapshot in directory -# usage: mount_latest_snap filesystem BACKUP_DIRECTORY -function mount_latest_snap() { - BACKUP_DIRECTORY="${2}" - filesystem="${1}" - - # get name of latest snapshot - snapshot=$(zfs_latest_snap "${filesystem}") - - # if there's no snapshot then let's ignore it - if [[ $? == 1 ]]; then - echo "No snapshot exists for ${filesystem}, it will not be backed up." - return 1 - fi - - sourcepath=$(zfs_snapshot_mountpoint "${filesystem}" "${snapshot}") - # if the filesystem is not mounted/path doesn't exist then let's ignore as well - if [[ $? == 1 ]]; then - echo "Cannot find snapshot ${snapshot} for ${filesystem}, perhaps it's not mounted? Anyways, it will not be backed up." - return 1 - fi - - # mountpath may be inside a previously mounted snapshot - mountpath=${BACKUP_DIRECTORY}/${filesystem} - - # mount to backup directory using a bind filesystem - mkdir -p "${mountpath}" - echo "mount ${sourcepath} => ${mountpath}" - mount --bind --read-only "${sourcepath}" "${mountpath}" - return 0 -} - -# Unmount and cleanup if necessary +# Main logic zfs_backup_cleanup "$BACKUP_DIRECTORY" -# Check if snapshot exists echo "Previous snapshot:" zfs list -t snapshot | grep "$ZFS_DATASET@$SNAPSHOT_NAME" || true -# Attempt to destroy existing snapshot echo "Attempting to destroy existing snapshot..." if zfs destroy -r "$ZFS_DATASET@$SNAPSHOT_NAME"; then echo "Successfully destroyed old snapshot" @@ -132,7 +30,6 @@ else exit 1 fi -# Create new snapshot if ! zfs snapshot -r "$ZFS_DATASET@$SNAPSHOT_NAME"; then echo "Failed to create snapshot" exit 1 @@ -141,7 +38,6 @@ fi echo "New snapshot created:" zfs list -t snapshot | grep "$ZFS_DATASET@$SNAPSHOT_NAME" -# Mount the snapshot if ! mount_dataset; then echo "Failed to mount snapshot" exit 1