feat: Add Borg Backup support to simple LVM backup system

- Add LV → Borg and VG → Borg backup modes
- GUI: Borg settings panel with repo path, encryption, passphrase
- CLI: Enhanced script with Borg options (--new-repo, --encryption, --passphrase)
- Automatic repository initialization for new repos
- Support for all Borg encryption modes (none, repokey, keyfile)
- Mount snapshots temporarily for file-level Borg backups
- Comprehensive cleanup of snapshots and mount points
- Updated documentation and examples

Benefits:
- Deduplication and compression
- Strong encryption support
- Incremental backups capability
- Space-efficient storage
- Still maintains simple snapshot → backup → cleanup workflow
This commit is contained in:
root
2025-10-09 00:37:17 +02:00
parent 72f9838f55
commit 179a84e442
4 changed files with 566 additions and 35 deletions

View File

@@ -1,4 +1,4 @@
# Enhanced Simple LVM Backup System # Enhanced Simple LVM Backup System with Borg Support
A reliable, straightforward backup system for LVM-enabled Linux systems. Born from the need for simple, dependable backups without complex logic that can cause system issues. A reliable, straightforward backup system for LVM-enabled Linux systems. Born from the need for simple, dependable backups without complex logic that can cause system issues.
@@ -6,22 +6,24 @@ A reliable, straightforward backup system for LVM-enabled Linux systems. Born fr
**Simple is better.** This system does exactly three things: **Simple is better.** This system does exactly three things:
1. Create LVM snapshot 1. Create LVM snapshot
2. Copy data with dd/pv 2. Copy data (dd/pv for raw, Borg for repositories)
3. Clean up snapshot 3. Clean up snapshot
No complex migration logic, no fancy features that can break. Just reliable backups. No complex migration logic, no fancy features that can break. Just reliable backups.
## Features ## Features
### Three Backup Modes ### Five Backup Modes
- **LV → LV**: Update existing logical volume backups - **LV → LV**: Update existing logical volume backups
- **LV → Raw**: Create fresh backups on raw devices - **LV → Raw**: Create fresh backups on raw devices
- **VG → Raw**: Clone entire volume groups with LVM metadata - **VG → Raw**: Clone entire volume groups with LVM metadata
- **LV → Borg**: Backup logical volume to Borg repository (deduplicated, compressed, encrypted)
- **VG → Borg**: Backup entire volume group to Borg repository
### Two Interfaces ### Two Interfaces
- **GUI**: `simple_backup_gui.py` - User-friendly interface - **GUI**: `simple_backup_gui.py` - User-friendly interface with Borg configuration
- **CLI**: `enhanced_simple_backup.sh` - Command-line power user interface - **CLI**: `enhanced_simple_backup.sh` - Command-line power user interface
## Quick Start ## Quick Start
@@ -53,6 +55,15 @@ sudo ./enhanced_simple_backup.sh vg-to-raw internal-vg /dev/sdb
sudo python3 simple_backup_gui.py sudo python3 simple_backup_gui.py
``` ```
**Borg repository backups:**
```bash
# Create new Borg repo and backup LV
sudo ./enhanced_simple_backup.sh lv-to-borg /dev/internal-vg/root /path/to/borg/repo --new-repo --encryption repokey --passphrase mypass
# Backup entire VG to existing repo
sudo ./enhanced_simple_backup.sh vg-to-borg internal-vg /path/to/borg/repo --passphrase mypass
```
## Files ## Files
### Core System ### Core System
@@ -71,6 +82,7 @@ sudo python3 simple_backup_gui.py
- Root access (`sudo`) - Root access (`sudo`)
- Python 3 + tkinter (for GUI) - Python 3 + tkinter (for GUI)
- `pv` command (optional, for progress display) - `pv` command (optional, for progress display)
- **Borg Backup** (for Borg modes): `sudo apt install borgbackup`
## Safety Features ## Safety Features

View File

@@ -16,22 +16,33 @@ BLUE='\033[0;34m'
NC='\033[0m' NC='\033[0m'
usage() { usage() {
echo "Enhanced Simple LVM Backup Script" echo "Enhanced Simple LVM Backup Script with Borg Support"
echo "" echo ""
echo "Usage:" echo "Usage:"
echo " $0 lv-to-lv SOURCE_LV TARGET_LV" echo " $0 lv-to-lv SOURCE_LV TARGET_LV"
echo " $0 lv-to-raw SOURCE_LV TARGET_DEVICE" echo " $0 lv-to-raw SOURCE_LV TARGET_DEVICE"
echo " $0 vg-to-raw SOURCE_VG 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 "" echo ""
echo "Modes:" echo "Modes:"
echo " lv-to-lv - Update existing LV backup (SOURCE_LV → TARGET_LV)" echo " lv-to-lv - Update existing LV backup (SOURCE_LV → TARGET_LV)"
echo " lv-to-raw - Create fresh backup (SOURCE_LV → raw device)" echo " lv-to-raw - Create fresh backup (SOURCE_LV → raw device)"
echo " vg-to-raw - Clone entire VG (SOURCE_VG → raw device)" echo " vg-to-raw - Clone entire VG (SOURCE_VG → raw device)"
echo " lv-to-borg - Backup LV to Borg repository"
echo " vg-to-borg - Backup entire VG to Borg repository"
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 "" echo ""
echo "Examples:" echo "Examples:"
echo " $0 lv-to-lv /dev/internal-vg/root /dev/backup-vg/root" 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 lv-to-raw /dev/internal-vg/root /dev/sdb"
echo " $0 vg-to-raw internal-vg /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 vg-to-borg internal-vg /path/to/borg/repo --encryption repokey --passphrase mypass"
echo "" echo ""
echo "List available sources/targets:" echo "List available sources/targets:"
echo " ./list_drives.sh" echo " ./list_drives.sh"
@@ -70,14 +81,40 @@ cleanup_and_exit() {
# Trap for cleanup # Trap for cleanup
trap 'cleanup_and_exit 130' INT TERM trap 'cleanup_and_exit 130' INT TERM
# Check arguments # Parse arguments and options
if [ $# -ne 3 ]; then if [ $# -lt 3 ]; then
usage usage
fi fi
MODE="$1" MODE="$1"
SOURCE="$2" SOURCE="$2"
TARGET="$3" TARGET="$3"
shift 3
# Parse Borg-specific options
NEW_REPO=false
ENCRYPTION="repokey"
PASSPHRASE=""
while [ $# -gt 0 ]; do
case "$1" in
--new-repo)
NEW_REPO=true
shift
;;
--encryption)
ENCRYPTION="$2"
shift 2
;;
--passphrase)
PASSPHRASE="$2"
shift 2
;;
*)
error "Unknown option: $1"
;;
esac
done
# Check if running as root # Check if running as root
if [ "$EUID" -ne 0 ]; then if [ "$EUID" -ne 0 ]; then
@@ -86,13 +123,25 @@ fi
# Validate mode # Validate mode
case "$MODE" in case "$MODE" in
"lv-to-lv"|"lv-to-raw"|"vg-to-raw") "lv-to-lv"|"lv-to-raw"|"vg-to-raw"|"lv-to-borg"|"vg-to-borg")
;; ;;
*) *)
error "Invalid mode: $MODE" error "Invalid mode: $MODE"
;; ;;
esac 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 "Enhanced Simple LVM Backup"
log "Mode: $MODE" log "Mode: $MODE"
log "Source: $SOURCE" log "Source: $SOURCE"
@@ -236,6 +285,174 @@ case "$MODE" in
log "VG to raw device backup completed successfully" log "VG to raw device backup completed successfully"
log "Target device now contains complete LVM structure" 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"
SNAPSHOT_PATH="/dev/$VG_NAME/$SNAPSHOT_NAME"
info "This will backup LV to Borg repository"
echo ""
echo -e "${YELLOW}Source LV: $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 ""
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 -L1G -s -n "$SNAPSHOT_NAME" "$SOURCE" || error "Failed to create snapshot"
# Create temporary mount point
TEMP_MOUNT=$(mktemp -d -t borg_backup_XXXXXX)
# Mount snapshot
log "Mounting snapshot to $TEMP_MOUNT"
mount "$SNAPSHOT_PATH" "$TEMP_MOUNT" || error "Failed to mount snapshot"
# Create Borg archive
ARCHIVE_NAME="lv_${LV_NAME}_$(date +%Y%m%d_%H%M%S)"
log "Creating Borg archive: $ARCHIVE_NAME"
log "This may take a long time depending on data size..."
borg create --progress --stats "$TARGET::$ARCHIVE_NAME" "$TEMP_MOUNT" || error "Borg backup failed"
log "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 "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' ' ')"
# Create base temp directory
BASE_TEMP_DIR=$(mktemp -d -t borg_vg_backup_XXXXXX)
SNAPSHOTS_CREATED=""
# Create snapshots and mount them
for LV_NAME in $LV_LIST; do
SNAPSHOT_NAME="${LV_NAME}_borg_snap"
SNAPSHOT_PATH="/dev/$SOURCE/$SNAPSHOT_NAME"
LV_PATH="/dev/$SOURCE/$LV_NAME"
MOUNT_POINT="$BASE_TEMP_DIR/$LV_NAME"
log "Creating snapshot: $SNAPSHOT_NAME"
lvcreate -L500M -s -n "$SNAPSHOT_NAME" "$LV_PATH" || {
warn "Failed to create snapshot for $LV_NAME"
continue
}
SNAPSHOTS_CREATED="$SNAPSHOTS_CREATED $SNAPSHOT_PATH"
# Create mount point and mount
mkdir -p "$MOUNT_POINT"
log "Mounting $SNAPSHOT_PATH to $MOUNT_POINT"
mount "$SNAPSHOT_PATH" "$MOUNT_POINT" || {
warn "Failed to mount $SNAPSHOT_PATH"
continue
}
done
# Create Borg archive
ARCHIVE_NAME="vg_${SOURCE}_$(date +%Y%m%d_%H%M%S)"
log "Creating Borg archive: $ARCHIVE_NAME"
log "This may take a long time depending on data size..."
borg create --progress --stats "$TARGET::$ARCHIVE_NAME" "$BASE_TEMP_DIR" || error "Borg backup failed"
log "Borg backup of entire VG completed successfully"
# Cleanup
log "Cleaning up mount points and snapshots"
for MOUNT_POINT in "$BASE_TEMP_DIR"/*; do
if mountpoint -q "$MOUNT_POINT" 2>/dev/null; then
umount "$MOUNT_POINT" || warn "Failed to unmount $MOUNT_POINT"
fi
done
rm -rf "$BASE_TEMP_DIR"
for SNAPSHOT_PATH in $SNAPSHOTS_CREATED; do
log "Removing snapshot $SNAPSHOT_PATH"
lvremove -f "$SNAPSHOT_PATH" || warn "Failed to remove snapshot $SNAPSHOT_PATH"
done
log "VG to Borg backup completed successfully"
;;
esac esac
echo "" echo ""

View File

@@ -3,7 +3,7 @@
# Enhanced script to list available backup sources and targets # Enhanced script to list available backup sources and targets
# Shows options for all three backup modes # Shows options for all three backup modes
echo "=== Enhanced Simple LVM Backup - Available Options ===" echo "=== Enhanced Simple LVM Backup with Borg Support - Available Options ==="
echo "" echo ""
echo "=== SOURCES ===" echo "=== SOURCES ==="
@@ -74,6 +74,12 @@ echo ""
echo "3. Entire VG to Raw Device (complete clone):" echo "3. Entire VG to Raw Device (complete clone):"
echo " sudo ./enhanced_simple_backup.sh vg-to-raw internal-vg /dev/sdb" echo " sudo ./enhanced_simple_backup.sh vg-to-raw internal-vg /dev/sdb"
echo "" echo ""
echo "4. LV to Borg Repository (encrypted backup):"
echo " sudo ./enhanced_simple_backup.sh lv-to-borg /dev/internal-vg/root /path/to/borg/repo --new-repo"
echo ""
echo "5. Entire VG to Borg Repository (all LVs):"
echo " sudo ./enhanced_simple_backup.sh vg-to-borg internal-vg /path/to/borg/repo --encryption repokey"
echo ""
echo "=== GUI VERSION ===" echo "=== GUI VERSION ==="
echo " sudo python3 simple_backup_gui.py" echo " sudo python3 simple_backup_gui.py"
echo "" echo ""
@@ -82,5 +88,7 @@ echo "- Always run as root (sudo)"
echo "- lv-to-lv: Updates existing backup LV" echo "- lv-to-lv: Updates existing backup LV"
echo "- lv-to-raw: Creates fresh backup, overwrites target device" echo "- lv-to-raw: Creates fresh backup, overwrites target device"
echo "- vg-to-raw: Clones entire VG including LVM metadata" echo "- vg-to-raw: Clones entire VG including LVM metadata"
echo "- lv-to-borg/vg-to-borg: Creates deduplicated, compressed, encrypted backups"
echo "- Borg backups require: sudo apt install borgbackup"
echo "- Make sure target devices are unmounted before backup" echo "- Make sure target devices are unmounted before backup"
echo "" echo ""

View File

@@ -45,6 +45,12 @@ class SimpleBackupGUI:
ttk.Radiobutton(mode_frame, text="Entire VG → Device", ttk.Radiobutton(mode_frame, text="Entire VG → Device",
variable=self.mode_var, value="vg_to_raw", variable=self.mode_var, value="vg_to_raw",
command=self.on_mode_change).pack(side=tk.LEFT, padx=5) command=self.on_mode_change).pack(side=tk.LEFT, padx=5)
ttk.Radiobutton(mode_frame, text="LV → Borg Repo",
variable=self.mode_var, value="lv_to_borg",
command=self.on_mode_change).pack(side=tk.LEFT, padx=5)
ttk.Radiobutton(mode_frame, text="VG → Borg Repo",
variable=self.mode_var, value="vg_to_borg",
command=self.on_mode_change).pack(side=tk.LEFT, padx=5)
# Source selection # Source selection
ttk.Label(main_frame, text="Source:").grid(row=1, column=0, sticky=tk.W, pady=5) ttk.Label(main_frame, text="Source:").grid(row=1, column=0, sticky=tk.W, pady=5)
@@ -58,30 +64,63 @@ class SimpleBackupGUI:
self.target_combo = ttk.Combobox(main_frame, textvariable=self.target_var, width=50) self.target_combo = ttk.Combobox(main_frame, textvariable=self.target_var, width=50)
self.target_combo.grid(row=2, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5) self.target_combo.grid(row=2, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5)
# Borg-specific settings (hidden initially)
self.borg_frame = ttk.LabelFrame(main_frame, text="Borg Backup Settings", padding="5")
self.borg_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
self.borg_frame.grid_remove() # Hide initially
# Repo path
ttk.Label(self.borg_frame, text="Repository Path:").grid(row=0, column=0, sticky=tk.W, pady=2)
self.repo_path_var = tk.StringVar()
repo_entry = ttk.Entry(self.borg_frame, textvariable=self.repo_path_var, width=40)
repo_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), pady=2)
ttk.Button(self.borg_frame, text="Browse", command=self.browse_repo).grid(row=0, column=2, padx=5, pady=2)
# Create new repo checkbox
self.create_new_repo = tk.BooleanVar()
ttk.Checkbutton(self.borg_frame, text="Create new repository",
variable=self.create_new_repo).grid(row=1, column=0, columnspan=2, sticky=tk.W, pady=2)
# Encryption settings
ttk.Label(self.borg_frame, text="Encryption Mode:").grid(row=2, column=0, sticky=tk.W, pady=2)
self.encryption_var = tk.StringVar(value="repokey")
encryption_combo = ttk.Combobox(self.borg_frame, textvariable=self.encryption_var,
values=["none", "repokey", "keyfile"], state="readonly", width=15)
encryption_combo.grid(row=2, column=1, sticky=tk.W, pady=2)
# Passphrase
ttk.Label(self.borg_frame, text="Passphrase:").grid(row=3, column=0, sticky=tk.W, pady=2)
self.passphrase_var = tk.StringVar()
passphrase_entry = ttk.Entry(self.borg_frame, textvariable=self.passphrase_var, show="*", width=40)
passphrase_entry.grid(row=3, column=1, sticky=(tk.W, tk.E), pady=2)
# Configure borg frame grid
self.borg_frame.columnconfigure(1, weight=1)
# Refresh button # Refresh button
ttk.Button(main_frame, text="Refresh", command=self.refresh_drives).grid(row=3, column=0, pady=10) ttk.Button(main_frame, text="Refresh", command=self.refresh_drives).grid(row=4, column=0, pady=10)
# Backup button # Backup button
self.backup_btn = ttk.Button(main_frame, text="Start Backup", self.backup_btn = ttk.Button(main_frame, text="Start Backup",
command=self.start_backup, style="Accent.TButton") command=self.start_backup, style="Accent.TButton")
self.backup_btn.grid(row=3, column=1, pady=10) self.backup_btn.grid(row=4, column=1, pady=10)
# Emergency stop # Emergency stop
self.stop_btn = ttk.Button(main_frame, text="Emergency Stop", self.stop_btn = ttk.Button(main_frame, text="Emergency Stop",
command=self.emergency_stop, state="disabled") command=self.emergency_stop, state="disabled")
self.stop_btn.grid(row=3, column=2, pady=10) self.stop_btn.grid(row=4, column=2, pady=10)
# Progress area # Progress area
ttk.Label(main_frame, text="Progress:").grid(row=4, column=0, sticky=tk.W, pady=(20, 5)) ttk.Label(main_frame, text="Progress:").grid(row=5, column=0, sticky=tk.W, pady=(20, 5))
self.progress = ttk.Progressbar(main_frame, mode='indeterminate') self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
self.progress.grid(row=5, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5) self.progress.grid(row=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
# Log area # Log area
ttk.Label(main_frame, text="Log:").grid(row=6, column=0, sticky=tk.W, pady=(10, 5)) ttk.Label(main_frame, text="Log:").grid(row=7, column=0, sticky=tk.W, pady=(10, 5))
log_frame = ttk.Frame(main_frame) log_frame = ttk.Frame(main_frame)
log_frame.grid(row=7, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5) log_frame.grid(row=8, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
self.log_text = tk.Text(log_frame, height=15, width=70) self.log_text = tk.Text(log_frame, height=15, width=70)
scrollbar = ttk.Scrollbar(log_frame, orient="vertical", command=self.log_text.yview) scrollbar = ttk.Scrollbar(log_frame, orient="vertical", command=self.log_text.yview)
@@ -94,13 +133,30 @@ class SimpleBackupGUI:
self.root.columnconfigure(0, weight=1) self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1) self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1) main_frame.columnconfigure(1, weight=1)
main_frame.rowconfigure(7, weight=1) main_frame.rowconfigure(8, weight=1)
log_frame.columnconfigure(0, weight=1) log_frame.columnconfigure(0, weight=1)
log_frame.rowconfigure(0, weight=1) log_frame.rowconfigure(0, weight=1)
def browse_repo(self):
"""Browse for Borg repository path"""
from tkinter import filedialog
path = filedialog.askdirectory(title="Select Borg Repository Directory")
if path:
self.repo_path_var.set(path)
def on_mode_change(self): def on_mode_change(self):
"""Handle backup mode change""" """Handle backup mode change"""
self.refresh_drives() mode = self.mode_var.get()
# Show/hide Borg settings
if mode in ["lv_to_borg", "vg_to_borg"]:
self.borg_frame.grid()
# Clear target combo for Borg modes (repo path is used instead)
self.target_combo['values'] = []
self.target_var.set("")
else:
self.borg_frame.grid_remove()
self.refresh_drives()
def log(self, message): def log(self, message):
"""Add message to log""" """Add message to log"""
@@ -198,6 +254,10 @@ class SimpleBackupGUI:
else: else:
self.log("No target LVs found") self.log("No target LVs found")
self.target_combo['values'] = [] self.target_combo['values'] = []
elif mode in ["lv_to_borg", "vg_to_borg"]:
# No target combo for Borg modes - repository path is used
self.target_combo['values'] = []
self.log("Using Borg repository path instead of target device")
else: else:
# Show raw block devices (lv_to_raw and vg_to_raw) # Show raw block devices (lv_to_raw and vg_to_raw)
success, output = self.run_command("lsblk -dno NAME,SIZE,MODEL | grep -E '^sd|^nvme'", show_output=False) success, output = self.run_command("lsblk -dno NAME,SIZE,MODEL | grep -E '^sd|^nvme'", show_output=False)
@@ -220,25 +280,61 @@ class SimpleBackupGUI:
def start_backup(self): def start_backup(self):
"""Start the backup process""" """Start the backup process"""
if not self.source_var.get() or not self.target_var.get():
messagebox.showerror("Error", "Please select both source and target")
return
mode = self.mode_var.get() mode = self.mode_var.get()
source = self.source_var.get().split()[0]
target = self.target_var.get().split()[0]
# Build confirmation message based on mode # Validate inputs based on mode
if mode == "lv_to_lv": if mode in ["lv_to_borg", "vg_to_borg"]:
msg = f"Update existing LV backup:\n\nSource LV: {source}\nTarget LV: {target}\n\n" if not self.source_var.get() or not self.repo_path_var.get():
msg += "This will overwrite the target LV with current source data.\n\nContinue?" messagebox.showerror("Error", "Please select source and repository path")
elif mode == "lv_to_raw": return
msg = f"Create fresh backup:\n\nSource LV: {source}\nTarget Device: {target}\n\n"
msg += "WARNING: Target device will be completely overwritten!\n\nContinue?" # Check if borg is installed
elif mode == "vg_to_raw": success, _ = self.run_command("which borg", show_output=False)
msg = f"Clone entire Volume Group:\n\nSource VG: {source}\nTarget Device: {target}\n\n" if not success:
msg += "WARNING: Target device will be completely overwritten!\n" messagebox.showerror("Error", "Borg Backup is not installed. Please install it first:\nsudo apt install borgbackup")
msg += "This will clone ALL logical volumes in the VG.\n\nContinue?" return
source = self.source_var.get().split()[0]
repo_path = self.repo_path_var.get()
# Build confirmation message for Borg
if mode == "lv_to_borg":
msg = f"Borg backup of LV:\n\nSource LV: {source}\nRepository: {repo_path}\n\n"
if self.create_new_repo.get():
msg += "This will create a new Borg repository.\n"
else:
msg += "This will add to existing Borg repository.\n"
msg += f"Encryption: {self.encryption_var.get()}\n\nContinue?"
else: # vg_to_borg
msg = f"Borg backup of entire VG:\n\nSource VG: {source}\nRepository: {repo_path}\n\n"
if self.create_new_repo.get():
msg += "This will create a new Borg repository.\n"
else:
msg += "This will add to existing Borg repository.\n"
msg += f"Encryption: {self.encryption_var.get()}\n"
msg += "This will backup ALL logical volumes in the VG.\n\nContinue?"
target = repo_path
else:
# Original validation for non-Borg modes
if not self.source_var.get() or not self.target_var.get():
messagebox.showerror("Error", "Please select both source and target")
return
source = self.source_var.get().split()[0]
target = self.target_var.get().split()[0]
# Build confirmation message based on mode
if mode == "lv_to_lv":
msg = f"Update existing LV backup:\n\nSource LV: {source}\nTarget LV: {target}\n\n"
msg += "This will overwrite the target LV with current source data.\n\nContinue?"
elif mode == "lv_to_raw":
msg = f"Create fresh backup:\n\nSource LV: {source}\nTarget Device: {target}\n\n"
msg += "WARNING: Target device will be completely overwritten!\n\nContinue?"
elif mode == "vg_to_raw":
msg = f"Clone entire Volume Group:\n\nSource VG: {source}\nTarget Device: {target}\n\n"
msg += "WARNING: Target device will be completely overwritten!\n"
msg += "This will clone ALL logical volumes in the VG.\n\nContinue?"
if not messagebox.askyesno("Confirm Backup", msg): if not messagebox.askyesno("Confirm Backup", msg):
return return
@@ -264,6 +360,10 @@ class SimpleBackupGUI:
self.backup_lv_to_raw(source, target) self.backup_lv_to_raw(source, target)
elif mode == "vg_to_raw": elif mode == "vg_to_raw":
self.backup_vg_to_raw(source, target) self.backup_vg_to_raw(source, target)
elif mode == "lv_to_borg":
self.backup_lv_to_borg(source, target)
elif mode == "vg_to_borg":
self.backup_vg_to_borg(source, target)
self.log("=== Backup completed successfully! ===") self.log("=== Backup completed successfully! ===")
self.root.after(0, lambda: messagebox.showinfo("Success", "Backup completed successfully!")) self.root.after(0, lambda: messagebox.showinfo("Success", "Backup completed successfully!"))
@@ -389,6 +489,200 @@ class SimpleBackupGUI:
self.log("VG copy completed - target device now contains complete LVM structure") self.log("VG copy completed - target device now contains complete LVM structure")
def backup_lv_to_borg(self, source_lv, repo_path):
"""Backup LV to Borg repository"""
self.log("Mode: LV to Borg Repository")
# Set up environment for Borg
borg_env = os.environ.copy()
if self.passphrase_var.get():
borg_env['BORG_PASSPHRASE'] = self.passphrase_var.get()
# Initialize repository if needed
if self.create_new_repo.get():
self.log(f"Creating new Borg repository: {repo_path}")
encryption = self.encryption_var.get()
if encryption == "none":
init_cmd = f"borg init --encryption=none {repo_path}"
else:
init_cmd = f"borg init --encryption={encryption} {repo_path}"
result = subprocess.run(init_cmd, shell=True, capture_output=True, text=True, env=borg_env)
if result.returncode != 0:
raise Exception(f"Failed to initialize Borg repository: {result.stderr}")
self.log("Repository initialized successfully")
# Create snapshot
vg_name = source_lv.split('/')[2]
lv_name = source_lv.split('/')[3]
snapshot_name = f"{lv_name}_borg_snap"
self.current_snapshot = f"/dev/{vg_name}/{snapshot_name}"
self.log(f"Creating snapshot: {snapshot_name}")
success, output = self.run_command(f"lvcreate -L1G -s -n {snapshot_name} {source_lv}")
if not success:
raise Exception(f"Failed to create snapshot: {output}")
# Create a temporary mount point
import tempfile
temp_mount = tempfile.mkdtemp(prefix="borg_backup_")
try:
# Mount the snapshot
self.log(f"Mounting snapshot to {temp_mount}")
success, output = self.run_command(f"mount {self.current_snapshot} {temp_mount}")
if not success:
raise Exception(f"Failed to mount snapshot: {output}")
# Create Borg backup
archive_name = f"lv_{lv_name}_{time.strftime('%Y%m%d_%H%M%S')}"
self.log(f"Creating Borg archive: {archive_name}")
self.log("This may take a long time depending on data size...")
borg_cmd = f"borg create --progress --stats {repo_path}::{archive_name} {temp_mount}"
# Run borg with environment
process = subprocess.Popen(borg_cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, env=borg_env)
# Stream output
for line in process.stdout:
self.log(line.strip())
process.wait()
if process.returncode != 0:
raise Exception("Borg backup failed")
self.log("Borg backup completed successfully")
finally:
# Cleanup: unmount and remove temp directory
self.log("Cleaning up mount point")
self.run_command(f"umount {temp_mount}", show_output=False)
os.rmdir(temp_mount)
# Remove snapshot
self.log("Cleaning up snapshot")
success, output = self.run_command(f"lvremove -f {self.current_snapshot}")
if not success:
self.log(f"Warning: Failed to remove snapshot: {output}")
else:
self.log("Snapshot cleaned up")
self.current_snapshot = None
def backup_vg_to_borg(self, source_vg, repo_path):
"""Backup entire VG to Borg repository"""
self.log("Mode: Entire VG to Borg Repository")
# Set up environment for Borg
borg_env = os.environ.copy()
if self.passphrase_var.get():
borg_env['BORG_PASSPHRASE'] = self.passphrase_var.get()
# Initialize repository if needed
if self.create_new_repo.get():
self.log(f"Creating new Borg repository: {repo_path}")
encryption = self.encryption_var.get()
if encryption == "none":
init_cmd = f"borg init --encryption=none {repo_path}"
else:
init_cmd = f"borg init --encryption={encryption} {repo_path}"
result = subprocess.run(init_cmd, shell=True, capture_output=True, text=True, env=borg_env)
if result.returncode != 0:
raise Exception(f"Failed to initialize Borg repository: {result.stderr}")
self.log("Repository initialized successfully")
# Get all LVs in the VG
success, output = self.run_command(f"lvs --noheadings -o lv_name {source_vg}", show_output=False)
if not success:
raise Exception(f"Failed to get LVs for VG {source_vg}")
lv_names = [lv.strip() for lv in output.strip().split('\n') if lv.strip()]
if not lv_names:
raise Exception(f"No logical volumes found in VG {source_vg}")
self.log(f"Found {len(lv_names)} logical volumes: {', '.join(lv_names)}")
snapshots_created = []
mount_points = []
try:
# Create snapshots for all LVs
for lv_name in lv_names:
snapshot_name = f"{lv_name}_borg_snap"
snapshot_path = f"/dev/{source_vg}/{snapshot_name}"
lv_path = f"/dev/{source_vg}/{lv_name}"
self.log(f"Creating snapshot: {snapshot_name}")
success, output = self.run_command(f"lvcreate -L500M -s -n {snapshot_name} {lv_path}")
if not success:
raise Exception(f"Failed to create snapshot for {lv_name}: {output}")
snapshots_created.append(snapshot_path)
# Mount all snapshots
import tempfile
base_temp_dir = tempfile.mkdtemp(prefix="borg_vg_backup_")
for i, snapshot_path in enumerate(snapshots_created):
lv_name = lv_names[i]
mount_point = os.path.join(base_temp_dir, lv_name)
os.makedirs(mount_point)
mount_points.append(mount_point)
self.log(f"Mounting {snapshot_path} to {mount_point}")
success, output = self.run_command(f"mount {snapshot_path} {mount_point}")
if not success:
self.log(f"Warning: Failed to mount {snapshot_path}: {output}")
# Continue with other LVs
# Create Borg backup of the entire VG structure
archive_name = f"vg_{source_vg}_{time.strftime('%Y%m%d_%H%M%S')}"
self.log(f"Creating Borg archive: {archive_name}")
self.log("This may take a long time depending on data size...")
borg_cmd = f"borg create --progress --stats {repo_path}::{archive_name} {base_temp_dir}"
# Run borg with environment
process = subprocess.Popen(borg_cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, env=borg_env)
# Stream output
for line in process.stdout:
self.log(line.strip())
process.wait()
if process.returncode != 0:
raise Exception("Borg backup failed")
self.log("Borg backup of entire VG completed successfully")
finally:
# Cleanup: unmount all mount points
for mount_point in mount_points:
if os.path.ismount(mount_point):
self.log(f"Unmounting {mount_point}")
self.run_command(f"umount {mount_point}", show_output=False)
# Remove temp directory
if 'base_temp_dir' in locals():
import shutil
try:
shutil.rmtree(base_temp_dir)
except:
self.log(f"Warning: Could not remove temp directory {base_temp_dir}")
# Remove all snapshots
for snapshot_path in snapshots_created:
self.log(f"Removing snapshot {snapshot_path}")
success, output = self.run_command(f"lvremove -f {snapshot_path}")
if not success:
self.log(f"Warning: Failed to remove snapshot {snapshot_path}: {output}")
self.current_snapshot = None
def cleanup_on_error(self): def cleanup_on_error(self):
"""Clean up on error""" """Clean up on error"""
if self.current_snapshot: if self.current_snapshot: