ProxmoxBackupsServer-to-SMB/backups2smb.sh
2025-05-23 22:37:55 +02:00

466 lines
No EOL
15 KiB
Bash
Executable file

#!/bin/bash
# === Config ===
SERVER_IP="192.168.2.194"
SHARE_NAME="PBS-back"
MOUNT_POINT="/mnt/smb-backup"
CREDENTIALS_FILE="/root/.smb-pbs-cred"
DATE=$(date +%F-%H%M)
BACKUP_NAME="pbs-config-$DATE"
TMP_BACKUP="/tmp/$BACKUP_NAME"
CHUNK_SIZE="10G"
MIN_SPACE_REQUIRED="5G" # Minimum space required in /tmp
LOG="/var/log/pbs-smb-backup.log"
SRC="/etc /root /mypool"
# Exclusion patterns
EXCLUDE_PATTERNS=(
"node_modules"
".cursor-server"
".git"
".vscode"
"*.log"
"*.tmp"
"*.temp"
"*.swp"
"*.swo"
"*.bak"
"*.cache"
"*.pid"
"*.lock"
"*.socket"
"*.sock"
"*.pid"
"*.log.*"
"*.gz"
"*.zip"
"*.tar"
"*.7z"
"*.rar"
"*.iso"
"*.img"
"*.vmdk"
"*.vhd"
"*.qcow2"
"*.raw"
"*.vdi"
"*.vbox"
"*.ova"
"*.ovf"
"*.vmx"
"*.vmsd"
"*.vmsn"
"*.vmss"
"*.vmtm"
"*.vmxf"
"*.nvram"
"*.vmem"
"*.vswp"
)
MAX_BACKUPS=3 # Keep exactly 3 backups
# === Cleanup function ===
cleanup_old_backups() {
echo "[$(date)] 🧹 Starting cleanup of old backups..." | tee -a "$LOG"
# Cleanup old backups on SMB share
if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] Keeping only the $MAX_BACKUPS most recent backups..." | tee -a "$LOG"
# List all backup directories, sort by date (newest first), and remove older ones
cd "$MOUNT_POINT" || exit 1
ls -td pbs-config-* 2>/dev/null | tail -n +$((MAX_BACKUPS + 1)) | xargs -r rm -rf
echo "[$(date)] Current backups:" | tee -a "$LOG"
ls -lhd pbs-config-* 2>/dev/null | tee -a "$LOG"
fi
# Cleanup old logs
echo "[$(date)] Cleaning up old log files..." | tee -a "$LOG"
find /var/log -name "pbs-smb-backup.*.log" -mtime +7 -delete
# Cleanup /tmp
echo "[$(date)] Cleaning up old temporary files..." | tee -a "$LOG"
rm -rf /tmp/pbs-config-*
rm -rf /tmp/restore-*
}
# === Create mount point if needed ===
mkdir -p "$MOUNT_POINT"
# === Ensure credentials file exists ===
if [ ! -f "$CREDENTIALS_FILE" ]; then
echo "username=pbs" > "$CREDENTIALS_FILE"
echo "password=2104" >> "$CREDENTIALS_FILE"
chmod 600 "$CREDENTIALS_FILE"
fi
# === Helper functions ===
get_available_space() {
df -B1 /tmp | awk 'NR==2 {print $4}'
}
check_space() {
local required_space=$(numfmt --from=iec $MIN_SPACE_REQUIRED)
local available_space=$(get_available_space)
if [ "$available_space" -lt "$required_space" ]; then
echo "[$(date)] ⚠️ Warning: Only $(numfmt --to=iec $available_space) available in /tmp" | tee -a "$LOG"
return 1
fi
return 0
}
wait_for_space() {
local required_space=$(numfmt --from=iec $MIN_SPACE_REQUIRED)
local available_space
while true; do
available_space=$(get_available_space)
if [ "$available_space" -ge "$required_space" ]; then
return 0
fi
echo "[$(date)] ⚠️ Waiting for more space in /tmp (need $MIN_SPACE_REQUIRED, have $(numfmt --to=iec $available_space))..." | tee -a "$LOG"
sleep 10
done
}
cleanup_processes() {
local mount_point="$1"
local max_attempts=3
local attempt=1
echo "[$(date)] Cleaning up processes using $mount_point..." | tee -a "$LOG"
while [ $attempt -le $max_attempts ]; do
# Get list of processes using the mount point, excluding our own script
local pids=$(lsof -t "$mount_point" 2>/dev/null | grep -v "$$")
if [ -z "$pids" ]; then
echo "[$(date)] No processes found using $mount_point" | tee -a "$LOG"
return 0
fi
echo "[$(date)] Attempt $attempt: Found processes: $pids" | tee -a "$LOG"
# First try SIGTERM
kill -15 $pids 2>/dev/null
sleep 5
# Check if processes are still running
pids=$(lsof -t "$mount_point" 2>/dev/null | grep -v "$$")
if [ -n "$pids" ]; then
echo "[$(date)] Processes still running, sending SIGKILL..." | tee -a "$LOG"
kill -9 $pids 2>/dev/null
sleep 5
fi
# Final check
if [ -z "$(lsof -t "$mount_point" 2>/dev/null | grep -v "$$")" ]; then
echo "[$(date)] Successfully cleaned up all processes" | tee -a "$LOG"
return 0
fi
((attempt++))
done
echo "[$(date)] ⚠️ Warning: Could not clean up all processes after $max_attempts attempts" | tee -a "$LOG"
return 1
}
mount_share() {
echo "[$(date)] Mounting SMB share..." | tee -a "$LOG"
# Check if already mounted
if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] Share already mounted, attempting to unmount..." | tee -a "$LOG"
# Clean up any processes first
cleanup_processes "$MOUNT_POINT"
# Try to unmount gracefully first
umount "$MOUNT_POINT" 2>/dev/null
sleep 2
# If still mounted, try force unmount
if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] Force unmounting..." | tee -a "$LOG"
umount -f "$MOUNT_POINT" 2>/dev/null
sleep 2
# If still mounted, try lazy unmount
if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] Lazy unmounting..." | tee -a "$LOG"
umount -l "$MOUNT_POINT" 2>/dev/null
sleep 2
# If still mounted, try one more time with process cleanup
if mountpoint -q "$MOUNT_POINT"; then
cleanup_processes "$MOUNT_POINT"
sleep 2
umount -f "$MOUNT_POINT" 2>/dev/null
sleep 2
fi
fi
fi
# Final check - if still mounted, exit
if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] ❌ Failed to unmount existing share. Please check for processes using $MOUNT_POINT" | tee -a "$LOG"
echo "[$(date)] Running processes:" | tee -a "$LOG"
lsof "$MOUNT_POINT" | grep -v "$$" | tee -a "$LOG"
exit 1
fi
fi
# Ensure mount point is clean
rm -rf "$MOUNT_POINT"/*
# Try mounting with different SMB versions if needed
for smb_ver in "3.0" "2.1" "2.0" "1.0"; do
echo "[$(date)] Attempting mount with SMB version $smb_ver..." | tee -a "$LOG"
mount -t cifs "//$SERVER_IP/$SHARE_NAME" "$MOUNT_POINT" -o "credentials=$CREDENTIALS_FILE,iocharset=utf8,vers=$smb_ver,sec=ntlmssp,uid=0,gid=0,file_mode=0644,dir_mode=0755"
if [ $? -eq 0 ]; then
echo "[$(date)] Successfully mounted with SMB version $smb_ver" | tee -a "$LOG"
return 0
fi
# Show dmesg output for debugging
echo "[$(date)] Mount failed, checking dmesg for details..." | tee -a "$LOG"
dmesg | tail -n 20 | tee -a "$LOG"
sleep 2
done
echo "[$(date)] ❌ Failed to mount SMB share. Please check:" | tee -a "$LOG"
echo "1. SMB server is running and accessible" | tee -a "$LOG"
echo "2. Credentials are correct" | tee -a "$LOG"
echo "3. Network connectivity to $SERVER_IP" | tee -a "$LOG"
echo "4. SMB share '$SHARE_NAME' exists and is accessible" | tee -a "$LOG"
exit 1
}
unmount_share() {
echo "[$(date)] Unmounting SMB share..." | tee -a "$LOG"
if mountpoint -q "$MOUNT_POINT"; then
# Clean up any processes first
cleanup_processes "$MOUNT_POINT"
# Try to unmount gracefully first
umount "$MOUNT_POINT" 2>/dev/null
sleep 2
# If still mounted, try force unmount
if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] Force unmounting..." | tee -a "$LOG"
umount -f "$MOUNT_POINT" 2>/dev/null
sleep 2
# If still mounted, try lazy unmount
if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] Lazy unmounting..." | tee -a "$LOG"
umount -l "$MOUNT_POINT" 2>/dev/null
sleep 2
# If still mounted, try one more time with process cleanup
if mountpoint -q "$MOUNT_POINT"; then
cleanup_processes "$MOUNT_POINT"
sleep 2
umount -f "$MOUNT_POINT" 2>/dev/null
sleep 2
fi
fi
fi
# Final check - if still mounted, warn but continue
if mountpoint -q "$MOUNT_POINT"; then
echo "[$(date)] ⚠️ Warning: Could not unmount $MOUNT_POINT completely" | tee -a "$LOG"
echo "[$(date)] Running processes:" | tee -a "$LOG"
lsof "$MOUNT_POINT" | grep -v "$$" | tee -a "$LOG"
fi
fi
}
# Add trap to ensure cleanup on script exit
trap 'handle_exit' EXIT
handle_exit() {
local exit_code=$?
echo "[$(date)] Cleaning up on exit..." | tee -a "$LOG"
# Only attempt cleanup if we're not already in a cleanup process
if [ -z "$CLEANUP_IN_PROGRESS" ]; then
export CLEANUP_IN_PROGRESS=1
cleanup_processes "$MOUNT_POINT"
unmount_share
rm -rf "$TMP_BACKUP"
fi
exit $exit_code
}
# === Handle -u option for restore ===
if [ "$1" == "-u" ]; then
echo "[$(date)] 🔁 Restore mode selected. Starting cleanup..." | tee -a "$LOG"
cleanup_old_backups
mount_share
# Get latest backup directory
LATEST_BACKUP_DIR=$(ls -td "$MOUNT_POINT"/pbs-config-* 2>/dev/null | head -n1)
if [ -z "$LATEST_BACKUP_DIR" ]; then
echo "[$(date)] ❌ No backup found on SMB share." | tee -a "$LOG"
unmount_share
exit 1
fi
echo "[$(date)] ⬇️ Starting restore process from $LATEST_BACKUP_DIR..." | tee -a "$LOG"
# Create a temporary directory for processing
TMP_RESTORE_DIR="/tmp/restore-$(basename "$LATEST_BACKUP_DIR")"
mkdir -p "$TMP_RESTORE_DIR"
# Process chunks in order
for chunk in $(ls -v "$LATEST_BACKUP_DIR"/chunk_* 2>/dev/null); do
if [ ! -f "$chunk" ]; then
continue
fi
echo "[$(date)] Processing chunk: $(basename "$chunk")" | tee -a "$LOG"
# Copy chunk to temp
rsync -ah --progress "$chunk" "$TMP_RESTORE_DIR/"
# Extract the chunk
echo "[$(date)] Extracting chunk..." | tee -a "$LOG"
7z x -y "$TMP_RESTORE_DIR/$(basename "$chunk")" -o/ | tee -a "$LOG"
# Remove the processed chunk
rm -f "$TMP_RESTORE_DIR/$(basename "$chunk")"
done
# Cleanup
rm -rf "$TMP_RESTORE_DIR"
unmount_share
echo "[$(date)] ✅ Restore completed." | tee -a "$LOG"
exit 0
fi
# === Regular backup mode ===
# Step 1: Start cleanup
echo "[$(date)] 🧹 Starting initial cleanup..." | tee -a "$LOG"
cleanup_old_backups
if ! check_space; then
echo "[$(date)] ⚠️ Proceeding with caution due to limited space" | tee -a "$LOG"
fi
# Step 2: Create temporary directory for chunks
mkdir -p "$TMP_BACKUP"
# Step 3: Mount share
mount_share
# Step 4: Create backup directory on SMB
BACKUP_DIR="$MOUNT_POINT/$BACKUP_NAME"
mkdir -p "$BACKUP_DIR"
# Step 5: Create backup in chunks with space management
echo "[$(date)] 🗜️ Starting backup with space management..." | tee -a "$LOG"
# Build find command exclusions
build_find_exclusions() {
local exclusions=""
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
exclusions+=" -not -path '*/$pattern/*' -not -name '$pattern'"
done
echo "$exclusions"
}
# Create a temporary file list with exclusions
echo "[$(date)] 📋 Creating file list (excluding unnecessary files)..." | tee -a "$LOG"
eval "find $SRC -type f $(build_find_exclusions) > \"$TMP_BACKUP/filelist.txt\""
# Initialize variables for chunk creation
chunk_num=1
current_chunk_size=0
current_chunk_files=()
# Process files and create chunks
while IFS= read -r file; do
# Skip if file doesn't exist
[ -f "$file" ] || continue
# Get file size
file_size=$(stat -c %s "$file")
# If adding this file would exceed chunk size, create the chunk
if [ $((current_chunk_size + file_size)) -gt $(numfmt --from=iec $CHUNK_SIZE) ]; then
if [ ${#current_chunk_files[@]} -gt 0 ]; then
echo "[$(date)] 📦 Creating chunk $chunk_num with ${#current_chunk_files[@]} files..." | tee -a "$LOG"
# Create the chunk and wait for it to complete
chunk_file="$TMP_BACKUP/chunk_$(printf "%03d" $chunk_num).7z"
7z a -y -spf -t7z -m0=lzma2 -mx=5 "$chunk_file" "${current_chunk_files[@]}" | tee -a "$LOG"
# Wait for 7z to complete and verify the chunk exists
if [ -f "$chunk_file" ]; then
echo "[$(date)] ⬆️ Transferring chunk $chunk_num to SMB..." | tee -a "$LOG"
rsync -ah --progress "$chunk_file" "$BACKUP_DIR/"
# Verify the transfer was successful
if [ $? -eq 0 ]; then
echo "[$(date)] ✅ Chunk $chunk_num transferred successfully" | tee -a "$LOG"
rm -f "$chunk_file"
else
echo "[$(date)] ❌ Failed to transfer chunk $chunk_num" | tee -a "$LOG"
exit 1
fi
else
echo "[$(date)] ❌ Failed to create chunk $chunk_num" | tee -a "$LOG"
exit 1
fi
# Reset for next chunk
current_chunk_files=()
current_chunk_size=0
((chunk_num++))
# Check space before continuing
if ! check_space; then
wait_for_space
fi
fi
fi
# Add file to current chunk
current_chunk_files+=("$file")
current_chunk_size=$((current_chunk_size + file_size))
done < "$TMP_BACKUP/filelist.txt"
# Create final chunk if there are remaining files
if [ ${#current_chunk_files[@]} -gt 0 ]; then
echo "[$(date)] 📦 Creating final chunk $chunk_num with ${#current_chunk_files[@]} files..." | tee -a "$LOG"
chunk_file="$TMP_BACKUP/chunk_$(printf "%03d" $chunk_num).7z"
7z a -y -spf -t7z -m0=lzma2 -mx=5 "$chunk_file" "${current_chunk_files[@]}" | tee -a "$LOG"
# Wait for 7z to complete and verify the chunk exists
if [ -f "$chunk_file" ]; then
echo "[$(date)] ⬆️ Transferring final chunk to SMB..." | tee -a "$LOG"
rsync -ah --progress "$chunk_file" "$BACKUP_DIR/"
# Verify the transfer was successful
if [ $? -eq 0 ]; then
echo "[$(date)] ✅ Final chunk transferred successfully" | tee -a "$LOG"
rm -f "$chunk_file"
else
echo "[$(date)] ❌ Failed to transfer final chunk" | tee -a "$LOG"
exit 1
fi
else
echo "[$(date)] ❌ Failed to create final chunk" | tee -a "$LOG"
exit 1
fi
fi
# Step 6: Final cleanup and finish
rm -rf "$TMP_BACKUP"
unmount_share
echo "[$(date)] ✅ Backup completed successfully: $BACKUP_NAME" | tee -a "$LOG"