#!/bin/bash # Enhanced Simple LVM Backup Script # Supports three backup modes: # 1. LV → LV: Update existing logical volume backup # 2. LV → Raw: Create fresh backup on raw device # 3. VG → Raw: Clone entire volume group to raw device set -euo pipefail IFS=$'\n\t' # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' usage() { echo "Enhanced Simple LVM Backup Script with Borg Support" echo "" echo "Usage:" echo " $0 lv-to-lv SOURCE_LV TARGET_LV" echo " $0 lv-to-raw SOURCE_LV TARGET_DEVICE" echo " $0 vg-to-raw SOURCE_VG TARGET_DEVICE" echo " $0 lv-to-borg SOURCE_LV REPO_PATH [--new-repo] [--encryption MODE] [--passphrase PASS]" echo " $0 vg-to-borg SOURCE_VG REPO_PATH [--new-repo] [--encryption MODE] [--passphrase PASS]" echo " $0 files-to-borg SOURCE_LV REPO_PATH [--new-repo] [--encryption MODE] [--passphrase PASS]" echo "" echo "Modes:" echo " lv-to-lv - Update existing LV backup (SOURCE_LV → TARGET_LV)" echo " lv-to-raw - Create fresh backup (SOURCE_LV → raw device)" echo " vg-to-raw - Clone entire VG (SOURCE_VG → raw device)" echo " lv-to-borg - Backup LV to Borg repository (block-level)" echo " vg-to-borg - Backup entire VG to Borg repository (block-level)" echo " files-to-borg - Backup LV files to Borg repository (file-level, space-efficient)" echo "" echo "Borg Options:" echo " --new-repo Create new repository" echo " --encryption MODE Encryption mode: none, repokey, keyfile (default: repokey)" echo " --passphrase PASS Repository passphrase" echo " --generous-snapshots Use 25% of LV size for snapshots (for very active systems)" echo "" echo "Examples:" echo " $0 lv-to-lv /dev/internal-vg/root /dev/backup-vg/root" echo " $0 lv-to-raw /dev/internal-vg/root /dev/sdb" echo " $0 vg-to-raw internal-vg /dev/sdb" echo " $0 lv-to-borg /dev/internal-vg/root /path/to/borg/repo --new-repo" echo " $0 files-to-borg /dev/internal-vg/home /path/to/borg/repo --new-repo" echo "" echo "List available sources/targets:" echo " ./list_drives.sh" echo "" exit 1 } log() { echo -e "${GREEN}[$(date '+%H:%M:%S')] $1${NC}" } error() { echo -e "${RED}[ERROR] $1${NC}" >&2 cleanup_and_exit 1 } warn() { echo -e "${YELLOW}[WARNING] $1${NC}" } info() { echo -e "${BLUE}[INFO] $1${NC}" } cleanup_and_exit() { local exit_code=${1:-0} if [ -n "$SNAPSHOT_PATH" ] && lvs "$SNAPSHOT_PATH" >/dev/null 2>&1; then warn "Cleaning up snapshot: $SNAPSHOT_PATH" lvremove -f "$SNAPSHOT_PATH" || warn "Failed to remove snapshot" fi exit $exit_code } # Trap for cleanup trap 'cleanup_and_exit 130' INT TERM # Parse arguments and options if [ $# -lt 3 ]; then usage fi MODE="$1" SOURCE="$2" TARGET="$3" shift 3 # Parse Borg-specific options NEW_REPO=false ENCRYPTION="repokey" PASSPHRASE="" GENEROUS_SNAPSHOTS=false while [ $# -gt 0 ]; do case "$1" in --new-repo) NEW_REPO=true shift ;; --encryption) ENCRYPTION="$2" shift 2 ;; --passphrase) PASSPHRASE="$2" shift 2 ;; --generous-snapshots) GENEROUS_SNAPSHOTS=true shift ;; *) error "Unknown option: $1" ;; esac done # Check if running as root if [ "$EUID" -ne 0 ]; then error "This script must be run as root" fi # Validate mode case "$MODE" in "lv-to-lv"|"lv-to-raw"|"vg-to-raw"|"lv-to-borg"|"vg-to-borg"|"files-to-borg") ;; *) error "Invalid mode: $MODE" ;; esac # Check Borg requirements for Borg modes if [[ "$MODE" == *"-to-borg" ]]; then if ! command -v borg >/dev/null 2>&1; then error "Borg Backup is not installed. Please install it: sudo apt install borgbackup" fi # Set up Borg environment if [ -n "$PASSPHRASE" ]; then export BORG_PASSPHRASE="$PASSPHRASE" fi fi log "Enhanced Simple LVM Backup" log "Mode: $MODE" log "Source: $SOURCE" log "Target: $TARGET" # Mode-specific validation and execution case "$MODE" in "lv-to-lv") # LV to LV backup if [ ! -e "$SOURCE" ]; then error "Source LV does not exist: $SOURCE" fi if [ ! -e "$TARGET" ]; then error "Target LV does not exist: $TARGET" fi # Extract VG and LV names VG_NAME=$(lvs --noheadings -o vg_name "$SOURCE" | tr -d ' ') LV_NAME=$(lvs --noheadings -o lv_name "$SOURCE" | tr -d ' ') SNAPSHOT_NAME="${LV_NAME}_backup_snap_$(date +%s)" SNAPSHOT_PATH="/dev/$VG_NAME/$SNAPSHOT_NAME" info "This will update the existing backup LV" echo "" echo -e "${YELLOW}Source LV: $SOURCE${NC}" echo -e "${YELLOW}Target LV: $TARGET${NC}" echo -e "${YELLOW}The target LV will be overwritten with current source data${NC}" echo "" read -p "Continue? (yes/no): " confirm if [ "$confirm" != "yes" ]; then echo "Backup cancelled." exit 0 fi log "Creating snapshot of source LV" lvcreate -L1G -s -n "$SNAPSHOT_NAME" "$SOURCE" || error "Failed to create snapshot" log "Copying snapshot to target LV" if command -v pv >/dev/null 2>&1; then pv "$SNAPSHOT_PATH" | dd of="$TARGET" bs=4M || error "Copy failed" else dd if="$SNAPSHOT_PATH" of="$TARGET" bs=4M status=progress || error "Copy failed" fi log "Cleaning up snapshot" lvremove -f "$SNAPSHOT_PATH" || warn "Failed to remove snapshot" SNAPSHOT_PATH="" log "LV to LV backup completed successfully" ;; "lv-to-raw") # LV to raw device backup if [ ! -e "$SOURCE" ]; then error "Source LV does not exist: $SOURCE" fi if [ ! -e "$TARGET" ]; then error "Target device does not exist: $TARGET" fi # Extract VG and LV names VG_NAME=$(lvs --noheadings -o vg_name "$SOURCE" | tr -d ' ') LV_NAME=$(lvs --noheadings -o lv_name "$SOURCE" | tr -d ' ') SNAPSHOT_NAME="${LV_NAME}_backup_snap_$(date +%s)" SNAPSHOT_PATH="/dev/$VG_NAME/$SNAPSHOT_NAME" info "This will create a fresh backup on raw device" echo "" echo -e "${YELLOW}Source LV: $SOURCE${NC}" echo -e "${YELLOW}Target Device: $TARGET${NC}" echo -e "${RED}WARNING: All data on $TARGET will be lost!${NC}" echo "" read -p "Continue? (yes/no): " confirm if [ "$confirm" != "yes" ]; then echo "Backup cancelled." exit 0 fi log "Creating snapshot of source LV" lvcreate -L1G -s -n "$SNAPSHOT_NAME" "$SOURCE" || error "Failed to create snapshot" log "Copying snapshot to target device" if command -v pv >/dev/null 2>&1; then pv "$SNAPSHOT_PATH" | dd of="$TARGET" bs=4M || error "Copy failed" else dd if="$SNAPSHOT_PATH" of="$TARGET" bs=4M status=progress || error "Copy failed" fi log "Cleaning up snapshot" lvremove -f "$SNAPSHOT_PATH" || warn "Failed to remove snapshot" SNAPSHOT_PATH="" log "LV to raw device backup completed successfully" ;; "vg-to-raw") # VG to raw device backup if ! vgs "$SOURCE" >/dev/null 2>&1; then error "Source VG does not exist: $SOURCE" fi if [ ! -e "$TARGET" ]; then error "Target device does not exist: $TARGET" fi # Get the first PV of the source VG SOURCE_PV=$(vgs --noheadings -o pv_name "$SOURCE" | head -n1 | tr -d ' ') if [ -z "$SOURCE_PV" ]; then error "No physical volumes found for VG: $SOURCE" fi info "This will clone the entire volume group" echo "" echo -e "${YELLOW}Source VG: $SOURCE${NC}" echo -e "${YELLOW}Source PV: $SOURCE_PV${NC}" echo -e "${YELLOW}Target Device: $TARGET${NC}" echo -e "${RED}WARNING: All data on $TARGET will be lost!${NC}" echo -e "${BLUE}This preserves LVM metadata and all logical volumes${NC}" echo "" read -p "Continue? (yes/no): " confirm if [ "$confirm" != "yes" ]; then echo "Backup cancelled." exit 0 fi log "Copying entire PV to target device" log "This preserves LVM structure and all LVs" if command -v pv >/dev/null 2>&1; then pv "$SOURCE_PV" | dd of="$TARGET" bs=4M || error "Copy failed" else dd if="$SOURCE_PV" of="$TARGET" bs=4M status=progress || error "Copy failed" fi log "VG to raw device backup completed successfully" log "Target device now contains complete LVM structure" ;; "lv-to-borg") # LV to Borg repository backup if [ ! -e "$SOURCE" ]; then error "Source LV does not exist: $SOURCE" fi # Extract VG and LV names VG_NAME=$(lvs --noheadings -o vg_name "$SOURCE" | tr -d ' ') LV_NAME=$(lvs --noheadings -o lv_name "$SOURCE" | tr -d ' ') SNAPSHOT_NAME="${LV_NAME}_borg_snap_$(date +%s)" SNAPSHOT_PATH="/dev/$VG_NAME/$SNAPSHOT_NAME" # Get LV size to determine appropriate snapshot size LV_SIZE_BYTES=$(lvs --noheadings -o lv_size --units b "$SOURCE" | tr -d ' ' | sed 's/B$//') # Use different percentages based on options if [ "$GENEROUS_SNAPSHOTS" = true ]; then SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES / 4)) # 25% MIN_SIZE=$((2 * 1024 * 1024 * 1024)) # 2GB minimum else # Auto mode: 10% normally, 15% for large LVs if [ $LV_SIZE_BYTES -gt $((50 * 1024 * 1024 * 1024)) ]; then SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES * 15 / 100)) # 15% for >50GB else SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES / 10)) # 10% normally fi MIN_SIZE=$((1024 * 1024 * 1024)) # 1GB minimum fi if [ $SNAPSHOT_SIZE_BYTES -lt $MIN_SIZE ]; then SNAPSHOT_SIZE_BYTES=$MIN_SIZE fi SNAPSHOT_SIZE_GB=$((SNAPSHOT_SIZE_BYTES / 1073741824)) SNAPSHOT_SIZE="${SNAPSHOT_SIZE_GB}G" info "This will backup LV to Borg repository" echo "" echo -e "${YELLOW}Source LV: $SOURCE${NC}" echo -e "${YELLOW}Repository: $TARGET${NC}" echo -e "${BLUE}Snapshot size: $SNAPSHOT_SIZE${NC}" if [ "$NEW_REPO" = true ]; then echo -e "${BLUE}Will create new repository${NC}" else echo -e "${BLUE}Will add to existing repository${NC}" fi echo -e "${BLUE}Encryption: $ENCRYPTION${NC}" echo "" read -p "Continue? (yes/no): " confirm if [ "$confirm" != "yes" ]; then echo "Backup cancelled." exit 0 fi # Initialize repository if needed if [ "$NEW_REPO" = true ]; then log "Creating new Borg repository: $TARGET" if [ "$ENCRYPTION" = "none" ]; then borg init --encryption=none "$TARGET" || error "Failed to initialize repository" else borg init --encryption="$ENCRYPTION" "$TARGET" || error "Failed to initialize repository" fi log "Repository initialized successfully" fi log "Creating snapshot of source LV" lvcreate -L"$SNAPSHOT_SIZE" -s -n "$SNAPSHOT_NAME" "$SOURCE" || error "Failed to create snapshot" # Create Borg archive ARCHIVE_NAME="lv_${LV_NAME}_$(date +%Y%m%d_%H%M%S)" log "Creating Borg archive (block-level): $ARCHIVE_NAME" log "Backing up raw snapshot block device to Borg..." log "This preserves exact block-level state including filesystem metadata" dd if="$SNAPSHOT_PATH" bs=4M | borg create --stdin-name "${LV_NAME}.img" --progress --stats "$TARGET::$ARCHIVE_NAME" - || error "Borg backup failed" log "Block-level Borg backup completed successfully" # Cleanup log "Cleaning up snapshot" lvremove -f "$SNAPSHOT_PATH" || warn "Failed to remove snapshot" SNAPSHOT_PATH="" log "LV to Borg backup completed successfully" ;; "vg-to-borg") # VG to Borg repository backup if ! vgs "$SOURCE" >/dev/null 2>&1; then error "Source VG does not exist: $SOURCE" fi info "This will backup entire VG to Borg repository" echo "" echo -e "${YELLOW}Source VG: $SOURCE${NC}" echo -e "${YELLOW}Repository: $TARGET${NC}" if [ "$NEW_REPO" = true ]; then echo -e "${BLUE}Will create new repository${NC}" else echo -e "${BLUE}Will add to existing repository${NC}" fi echo -e "${BLUE}Encryption: $ENCRYPTION${NC}" echo -e "${BLUE}This will backup all logical volumes in the VG${NC}" echo "" read -p "Continue? (yes/no): " confirm if [ "$confirm" != "yes" ]; then echo "Backup cancelled." exit 0 fi # Initialize repository if needed if [ "$NEW_REPO" = true ]; then log "Creating new Borg repository: $TARGET" if [ "$ENCRYPTION" = "none" ]; then borg init --encryption=none "$TARGET" || error "Failed to initialize repository" else borg init --encryption="$ENCRYPTION" "$TARGET" || error "Failed to initialize repository" fi log "Repository initialized successfully" fi # Get all LVs in VG LV_LIST=$(lvs --noheadings -o lv_name "$SOURCE" | tr -d ' ') if [ -z "$LV_LIST" ]; then error "No logical volumes found in VG: $SOURCE" fi log "Found logical volumes: $(echo $LV_LIST | tr '\n' ' ')" # Process each LV one by one to avoid space issues for LV_NAME in $LV_LIST; do SNAPSHOT_NAME="${LV_NAME}_borg_snap_$(date +%s)" SNAPSHOT_PATH="/dev/$SOURCE/$SNAPSHOT_NAME" LV_PATH="/dev/$SOURCE/$LV_NAME" # Get LV size to determine appropriate snapshot size LV_SIZE_BYTES=$(lvs --noheadings -o lv_size --units b "$LV_PATH" | tr -d ' ' | sed 's/B$//') # Use different percentages based on options if [ "$GENEROUS_SNAPSHOTS" = true ]; then SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES / 4)) # 25% MIN_SIZE=$((2 * 1024 * 1024 * 1024)) # 2GB minimum else # Auto mode: 10% normally, 15% for large LVs if [ $LV_SIZE_BYTES -gt $((50 * 1024 * 1024 * 1024)) ]; then SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES * 15 / 100)) # 15% for >50GB else SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES / 10)) # 10% normally fi MIN_SIZE=$((1024 * 1024 * 1024)) # 1GB minimum fi if [ $SNAPSHOT_SIZE_BYTES -lt $MIN_SIZE ]; then SNAPSHOT_SIZE_BYTES=$MIN_SIZE fi SNAPSHOT_SIZE_GB=$((SNAPSHOT_SIZE_BYTES / 1073741824)) SNAPSHOT_SIZE="${SNAPSHOT_SIZE_GB}G" log "Processing LV: $LV_NAME" log "Creating snapshot: $SNAPSHOT_NAME (size: $SNAPSHOT_SIZE)" lvcreate -L"$SNAPSHOT_SIZE" -s -n "$SNAPSHOT_NAME" "$LV_PATH" || { warn "Failed to create snapshot for $LV_NAME" continue } # Create individual archive for this LV ARCHIVE_NAME="vg_${SOURCE}_lv_${LV_NAME}_$(date +%Y%m%d_%H%M%S)" log "Backing up $LV_NAME to archive: $ARCHIVE_NAME" log "Streaming raw block device directly to Borg..." dd if="$SNAPSHOT_PATH" bs=4M | borg create --stdin-name "${LV_NAME}.img" --progress --stats "$TARGET::$ARCHIVE_NAME" - || { warn "Backup of $LV_NAME failed" } # Clean up this snapshot immediately to save space log "Removing snapshot $SNAPSHOT_PATH" lvremove -f "$SNAPSHOT_PATH" || warn "Failed to remove snapshot $SNAPSHOT_PATH" done log "Block-level VG Borg backup completed successfully" log "Created individual archives for each LV in VG $SOURCE" log "VG to Borg backup completed successfully" ;; "files-to-borg") # Files to Borg repository backup if [ ! -e "$SOURCE" ]; then error "Source LV does not exist: $SOURCE" fi # Extract VG and LV names VG_NAME=$(lvs --noheadings -o vg_name "$SOURCE" | tr -d ' ') LV_NAME=$(lvs --noheadings -o lv_name "$SOURCE" | tr -d ' ') SNAPSHOT_NAME="${LV_NAME}_files_snap_$(date +%s)" SNAPSHOT_PATH="/dev/$VG_NAME/$SNAPSHOT_NAME" # Get LV size to determine appropriate snapshot size (file-level needs much smaller) LV_SIZE_BYTES=$(lvs --noheadings -o lv_size --units b "$SOURCE" | tr -d ' ' | sed 's/B$//') if [ "$GENEROUS_SNAPSHOTS" = true ]; then # File-level generous: 5% with 1G min, 20G max SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES * 5 / 100)) [ $SNAPSHOT_SIZE_BYTES -lt $((1 * 1024 * 1024 * 1024)) ] && SNAPSHOT_SIZE_BYTES=$((1 * 1024 * 1024 * 1024)) [ $SNAPSHOT_SIZE_BYTES -gt $((20 * 1024 * 1024 * 1024)) ] && SNAPSHOT_SIZE_BYTES=$((20 * 1024 * 1024 * 1024)) else # Auto: 3% with 1G min, 15G max SNAPSHOT_SIZE_BYTES=$((LV_SIZE_BYTES * 3 / 100)) [ $SNAPSHOT_SIZE_BYTES -lt $((1 * 1024 * 1024 * 1024)) ] && SNAPSHOT_SIZE_BYTES=$((1 * 1024 * 1024 * 1024)) [ $SNAPSHOT_SIZE_BYTES -gt $((15 * 1024 * 1024 * 1024)) ] && SNAPSHOT_SIZE_BYTES=$((15 * 1024 * 1024 * 1024)) fi SNAPSHOT_SIZE_GB=$((SNAPSHOT_SIZE_BYTES / 1073741824)) SNAPSHOT_SIZE="${SNAPSHOT_SIZE_GB}G" info "This will backup LV files to Borg repository (space-efficient)" echo "" echo -e "${YELLOW}Source LV: $SOURCE${NC}" echo -e "${YELLOW}Repository: $TARGET${NC}" echo -e "${BLUE}Snapshot size: $SNAPSHOT_SIZE${NC}" echo -e "${BLUE}Mode: File-level backup (skips empty blocks)${NC}" if [ "$NEW_REPO" = true ]; then echo -e "${BLUE}Will create new repository${NC}" else echo -e "${BLUE}Will add to existing repository${NC}" fi echo -e "${BLUE}Encryption: $ENCRYPTION${NC}" echo "" read -p "Continue? (yes/no): " confirm if [ "$confirm" != "yes" ]; then echo "Backup cancelled." exit 0 fi # Initialize repository if needed if [ "$NEW_REPO" = true ]; then log "Creating new Borg repository: $TARGET" if [ "$ENCRYPTION" = "none" ]; then borg init --encryption=none "$TARGET" || error "Failed to initialize repository" else borg init --encryption="$ENCRYPTION" "$TARGET" || error "Failed to initialize repository" fi log "Repository initialized successfully" fi log "Creating snapshot of source LV" lvcreate -L"$SNAPSHOT_SIZE" -s -n "$SNAPSHOT_NAME" "$SOURCE" || error "Failed to create snapshot" # Create temporary mount point TEMP_MOUNT=$(mktemp -d -t borg_files_backup_XXXXXX) # Mount snapshot read-only with safe FS options FS_TYPE=$(blkid -o value -s TYPE "$SNAPSHOT_PATH" 2>/dev/null || echo "") if [ "$FS_TYPE" = "ext4" ] || [ "$FS_TYPE" = "ext3" ]; then MNT_OPTS="ro,noload" elif [ "$FS_TYPE" = "xfs" ]; then MNT_OPTS="ro,norecovery" else MNT_OPTS="ro" fi log "Mounting snapshot to $TEMP_MOUNT (opts: $MNT_OPTS)" mount -o "$MNT_OPTS" "$SNAPSHOT_PATH" "$TEMP_MOUNT" || error "Failed to mount snapshot" # Create Borg archive ARCHIVE_NAME="files_${LV_NAME}_$(date +%Y%m%d_%H%M%S)" log "Creating Borg archive (file-level): $ARCHIVE_NAME" log "Backing up files from mounted snapshot..." log "This is space-efficient and skips empty blocks" borg create --progress --stats --compression auto,zstd "$TARGET::$ARCHIVE_NAME" "$TEMP_MOUNT" || error "Borg file-level backup failed" log "File-level Borg backup completed successfully" # Cleanup log "Cleaning up mount point and snapshot" umount "$TEMP_MOUNT" || warn "Failed to unmount" rmdir "$TEMP_MOUNT" lvremove -f "$SNAPSHOT_PATH" || warn "Failed to remove snapshot" SNAPSHOT_PATH="" log "Files to Borg backup completed successfully" ;; esac echo "" echo -e "${GREEN}SUCCESS: Backup completed!${NC}" echo "" echo "Next steps:" case "$MODE" in "lv-to-lv") echo "- Your backup LV has been updated" echo "- You can mount $TARGET to verify the backup" ;; "lv-to-raw") echo "- Your backup device contains a raw copy of the LV" echo "- You can mount $TARGET directly or restore from it" ;; "vg-to-raw") echo "- Your backup device contains the complete LVM structure" echo "- You can boot from $TARGET or import the VG for recovery" echo "- To access: vgimport or boot directly from the device" ;; esac echo ""