diff --git a/README.md b/README.md index 3380a1f..652b811 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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: 1. Create LVM snapshot -2. Copy data with dd/pv +2. Copy data (dd/pv for raw, Borg for repositories) 3. Clean up snapshot No complex migration logic, no fancy features that can break. Just reliable backups. ## Features -### Three Backup Modes +### Five Backup Modes - **LV → LV**: Update existing logical volume backups - **LV → Raw**: Create fresh backups on raw devices - **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 -- **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 ## Quick Start @@ -53,6 +55,15 @@ sudo ./enhanced_simple_backup.sh vg-to-raw internal-vg /dev/sdb 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 ### Core System @@ -71,6 +82,7 @@ sudo python3 simple_backup_gui.py - Root access (`sudo`) - Python 3 + tkinter (for GUI) - `pv` command (optional, for progress display) +- **Borg Backup** (for Borg modes): `sudo apt install borgbackup` ## Safety Features diff --git a/enhanced_simple_backup.sh b/enhanced_simple_backup.sh index 5e98803..70cf6ca 100755 --- a/enhanced_simple_backup.sh +++ b/enhanced_simple_backup.sh @@ -16,22 +16,33 @@ BLUE='\033[0;34m' NC='\033[0m' usage() { - echo "Enhanced Simple LVM Backup Script" + 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 "" 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" + 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 "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 vg-to-borg internal-vg /path/to/borg/repo --encryption repokey --passphrase mypass" echo "" echo "List available sources/targets:" echo " ./list_drives.sh" @@ -70,14 +81,40 @@ cleanup_and_exit() { # Trap for cleanup trap 'cleanup_and_exit 130' INT TERM -# Check arguments -if [ $# -ne 3 ]; then +# 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="" + +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 if [ "$EUID" -ne 0 ]; then @@ -86,13 +123,25 @@ fi # Validate mode 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" ;; 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" @@ -236,6 +285,174 @@ case "$MODE" in 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" + 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 echo "" diff --git a/list_drives.sh b/list_drives.sh index dbd37dc..4f11410 100755 --- a/list_drives.sh +++ b/list_drives.sh @@ -3,7 +3,7 @@ # Enhanced script to list available backup sources and targets # 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 "=== SOURCES ===" @@ -74,6 +74,12 @@ echo "" echo "3. Entire VG to Raw Device (complete clone):" echo " sudo ./enhanced_simple_backup.sh vg-to-raw internal-vg /dev/sdb" 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 " sudo python3 simple_backup_gui.py" echo "" @@ -82,5 +88,7 @@ echo "- Always run as root (sudo)" echo "- lv-to-lv: Updates existing backup LV" echo "- lv-to-raw: Creates fresh backup, overwrites target device" 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 "" \ No newline at end of file diff --git a/simple_backup_gui.py b/simple_backup_gui.py index 2c17bfe..63f5654 100755 --- a/simple_backup_gui.py +++ b/simple_backup_gui.py @@ -45,6 +45,12 @@ class SimpleBackupGUI: ttk.Radiobutton(mode_frame, text="Entire VG → Device", variable=self.mode_var, value="vg_to_raw", 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 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.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 - 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 self.backup_btn = ttk.Button(main_frame, text="Start Backup", 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 self.stop_btn = ttk.Button(main_frame, text="Emergency Stop", 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 - 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.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 - 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.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) 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.rowconfigure(0, 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.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): """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): """Add message to log""" @@ -198,6 +254,10 @@ class SimpleBackupGUI: else: self.log("No target LVs found") 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: # 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) @@ -220,25 +280,61 @@ class SimpleBackupGUI: def start_backup(self): """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() - 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?" + # Validate inputs based on mode + if mode in ["lv_to_borg", "vg_to_borg"]: + if not self.source_var.get() or not self.repo_path_var.get(): + messagebox.showerror("Error", "Please select source and repository path") + return + + # Check if borg is installed + success, _ = self.run_command("which borg", show_output=False) + if not success: + messagebox.showerror("Error", "Borg Backup is not installed. Please install it first:\nsudo apt install borgbackup") + 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): return @@ -264,6 +360,10 @@ class SimpleBackupGUI: self.backup_lv_to_raw(source, target) elif mode == "vg_to_raw": 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.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") + 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): """Clean up on error""" if self.current_snapshot: