diff --git a/SIMPLE_BACKUP_README.md b/SIMPLE_BACKUP_README.md new file mode 100644 index 0000000..af2e4e8 --- /dev/null +++ b/SIMPLE_BACKUP_README.md @@ -0,0 +1,119 @@ +# Simple LVM Backup System + +**A much simpler, safer approach to LVM backups.** + +After issues with complex backup systems, this is a return to basics: +- Simple LVM snapshot creation +- Direct block-level copy with dd/pv +- Minimal logic, maximum reliability + +## What This Does + +1. **Creates an LVM snapshot** of your source volume +2. **Copies it block-for-block** to your target drive +3. **Cleans up the snapshot** when done + +That's it. No complex migration logic, no fancy features that can break. + +## Quick Start + +### 1. See what's available +```bash +sudo ./list_drives.sh +``` + +### 2. Run a simple backup +```bash +sudo ./simple_backup.sh /dev/your-vg/your-lv /dev/your-target-drive +``` + +### 3. Or use the GUI +```bash +sudo python3 simple_backup_gui.py +``` + +## Examples + +**Backup your root volume to an external SSD:** +```bash +sudo ./simple_backup.sh /dev/internal-vg/root /dev/sdb +``` + +**Backup your home volume:** +```bash +sudo ./simple_backup.sh /dev/internal-vg/home /dev/nvme1n1 +``` + +## Important Notes + +⚠️ **WARNING: The target drive will be completely overwritten!** + +- Always run as root (`sudo`) +- Target device will lose ALL existing data +- Make sure target device is unmounted before backup +- The backup is a complete block-level clone +- You need at least 1GB free space in your volume group for the snapshot + +## How It Works + +This is exactly what happens, no hidden complexity: + +1. `lvcreate -L1G -s -n backup_snap /dev/vg/lv` - Create snapshot +2. `pv /dev/vg/backup_snap | dd of=/dev/target bs=4M` - Copy data +3. `lvremove -f /dev/vg/backup_snap` - Remove snapshot + +## Files + +- `simple_backup.sh` - Command-line backup script +- `simple_backup_gui.py` - Simple GUI version +- `list_drives.sh` - Helper to show available drives + +## Why This Approach? + +The previous complex scripts had too much logic and caused system issues. This approach: + +- ✅ Uses standard LVM commands +- ✅ Minimal chance of errors +- ✅ Easy to understand and debug +- ✅ Does exactly what you expect +- ✅ No hidden "smart" features + +## Recovery + +To boot from your backup: +1. Connect the external drive +2. Boot from it directly, or +3. Use it as a recovery drive to restore your system + +## Requirements + +- LVM-enabled system +- Root access +- Python 3 + tkinter (for GUI) +- `pv` command (optional, for progress display) + +## If Something Goes Wrong + +The script will try to clean up snapshots automatically. If it fails: + +```bash +# List any remaining snapshots +sudo lvs | grep snap + +# Remove manually if needed +sudo lvremove /dev/vg/snapshot_name +``` + +## No More Complex Features + +This system intentionally does NOT include: +- Automatic drive detection with complex logic +- Migration between different LVM setups +- Boot repair or GRUB handling +- Multiple backup formats +- Configuration files +- Complex error handling + +If you need those features, use dedicated tools like CloneZilla or Borg Backup. + +This is for simple, reliable block-level LVM backups. Nothing more, nothing less. \ No newline at end of file diff --git a/enhanced_simple_backup.sh b/enhanced_simple_backup.sh new file mode 100755 index 0000000..5e98803 --- /dev/null +++ b/enhanced_simple_backup.sh @@ -0,0 +1,260 @@ +#!/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 -e + +# 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" + 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 "" + 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 "" + 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 "" + 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 + +# Check arguments +if [ $# -ne 3 ]; then + usage +fi + +MODE="$1" +SOURCE="$2" +TARGET="$3" + +# 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") + ;; + *) + error "Invalid mode: $MODE" + ;; +esac + +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" + 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" + 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" + ;; +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 "" \ No newline at end of file diff --git a/list_drives.sh b/list_drives.sh new file mode 100755 index 0000000..dbd37dc --- /dev/null +++ b/list_drives.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +# Enhanced script to list available backup sources and targets +# Shows options for all three backup modes + +echo "=== Enhanced Simple LVM Backup - Available Options ===" +echo "" + +echo "=== SOURCES ===" +echo "" +echo "Logical Volumes (for lv-to-lv and lv-to-raw modes):" +echo "---------------------------------------------------" +if command -v lvs >/dev/null 2>&1; then + echo "Path Size VG Name" + echo "----------------------------------------" + lvs --noheadings -o lv_path,lv_size,vg_name | while read lv_path lv_size vg_name; do + printf "%-24s %-8s %s\n" "$lv_path" "$lv_size" "$vg_name" + done +else + echo "LVM not available" +fi + +echo "" +echo "Volume Groups (for vg-to-raw mode):" +echo "-----------------------------------" +if command -v vgs >/dev/null 2>&1; then + echo "VG Name Size PV Count" + echo "----------------------------------" + vgs --noheadings -o vg_name,vg_size,pv_count | while read vg_name vg_size pv_count; do + printf "%-16s %-8s %s PVs\n" "$vg_name" "$vg_size" "$pv_count" + done +else + echo "LVM not available" +fi + +echo "" +echo "=== TARGETS ===" +echo "" +echo "Existing Logical Volumes (for lv-to-lv mode):" +echo "---------------------------------------------" +if command -v lvs >/dev/null 2>&1; then + echo "Path Size VG Name" + echo "----------------------------------------" + lvs --noheadings -o lv_path,lv_size,vg_name | while read lv_path lv_size vg_name; do + # Highlight external/backup VGs + if [[ "$vg_name" == *"migration"* ]] || [[ "$vg_name" == *"external"* ]] || [[ "$vg_name" == *"backup"* ]]; then + printf "%-24s %-8s %s (backup VG)\n" "$lv_path" "$lv_size" "$vg_name" + else + printf "%-24s %-8s %s\n" "$lv_path" "$lv_size" "$vg_name" + fi + done +else + echo "LVM not available" +fi + +echo "" +echo "Raw Block Devices (for lv-to-raw and vg-to-raw modes):" +echo "------------------------------------------------------" +echo "Device Size Model" +echo "-------------------------" +lsblk -dno NAME,SIZE,MODEL | grep -E '^sd|^nvme' | while read name size model; do + printf "/dev/%-6s %-8s %s\n" "$name" "$size" "$model" +done + +echo "" +echo "=== USAGE EXAMPLES ===" +echo "" +echo "1. LV to LV (update existing backup):" +echo " sudo ./enhanced_simple_backup.sh lv-to-lv /dev/internal-vg/root /dev/backup-vg/root" +echo "" +echo "2. LV to Raw Device (fresh backup):" +echo " sudo ./enhanced_simple_backup.sh lv-to-raw /dev/internal-vg/root /dev/sdb" +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 "=== GUI VERSION ===" +echo " sudo python3 simple_backup_gui.py" +echo "" +echo "=== IMPORTANT NOTES ===" +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 "- Make sure target devices are unmounted before backup" +echo "" \ No newline at end of file diff --git a/simple_backup.sh b/simple_backup.sh new file mode 100755 index 0000000..ff1b37f --- /dev/null +++ b/simple_backup.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +# Simple LVM Backup Script +# Does exactly what it says: snapshot -> copy -> cleanup +# NO complex logic, just the essentials + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +usage() { + echo "Usage: $0 SOURCE_LV TARGET_DEVICE" + echo "" + echo "Example: $0 /dev/internal-vg/root /dev/sdb" + echo "" + echo "This will:" + echo "1. Create a snapshot of SOURCE_LV" + echo "2. Copy it block-for-block to TARGET_DEVICE" + echo "3. Clean up the snapshot" + echo "" + echo "WARNING: TARGET_DEVICE will be completely overwritten!" + 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}" +} + +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 + +# Check arguments +if [ $# -ne 2 ]; then + usage +fi + +SOURCE_LV="$1" +TARGET_DEVICE="$2" + +# Check if running as root +if [ "$EUID" -ne 0 ]; then + error "This script must be run as root" +fi + +# Basic validation +if [ ! -e "$SOURCE_LV" ]; then + error "Source LV does not exist: $SOURCE_LV" +fi + +if [ ! -e "$TARGET_DEVICE" ]; then + error "Target device does not exist: $TARGET_DEVICE" +fi + +# Extract VG and LV names +VG_NAME=$(lvs --noheadings -o vg_name "$SOURCE_LV" | tr -d ' ') +LV_NAME=$(lvs --noheadings -o lv_name "$SOURCE_LV" | tr -d ' ') +SNAPSHOT_NAME="${LV_NAME}_simple_backup" +SNAPSHOT_PATH="/dev/$VG_NAME/$SNAPSHOT_NAME" + +log "Simple LVM Backup Starting" +log "Source: $SOURCE_LV" +log "Target: $TARGET_DEVICE" +log "Snapshot will be: $SNAPSHOT_PATH" + +# Final confirmation +echo "" +echo -e "${YELLOW}WARNING: This will completely overwrite $TARGET_DEVICE${NC}" +echo -e "${YELLOW}All data on $TARGET_DEVICE will be lost!${NC}" +echo "" +read -p "Are you sure you want to continue? (yes/no): " confirm + +if [ "$confirm" != "yes" ]; then + echo "Backup cancelled." + exit 0 +fi + +echo "" +log "Starting backup process..." + +# Step 1: Create snapshot +log "Creating snapshot..." +if ! lvcreate -L1G -s -n "$SNAPSHOT_NAME" "$SOURCE_LV"; then + error "Failed to create snapshot" +fi +log "Snapshot created: $SNAPSHOT_PATH" + +# Step 2: Copy data +log "Starting copy operation..." +log "This may take a long time depending on the size of your data" + +# Use pv if available for progress, otherwise dd with progress +if command -v pv >/dev/null 2>&1; then + log "Using pv for progress monitoring" + if ! pv "$SNAPSHOT_PATH" | dd of="$TARGET_DEVICE" bs=4M; then + error "Copy operation failed" + fi +else + log "Using dd (install 'pv' for progress monitoring)" + if ! dd if="$SNAPSHOT_PATH" of="$TARGET_DEVICE" bs=4M status=progress; then + error "Copy operation failed" + fi +fi + +log "Copy completed successfully" + +# Step 3: Clean up snapshot +log "Removing snapshot..." +if ! lvremove -f "$SNAPSHOT_PATH"; then + warn "Failed to remove snapshot, but backup completed" + warn "You may need to manually remove: $SNAPSHOT_PATH" +else + log "Snapshot cleaned up" +fi + +log "Backup completed successfully!" +log "Your data has been copied to: $TARGET_DEVICE" + +echo "" +echo -e "${GREEN}SUCCESS: Backup completed!${NC}" +echo "" +echo "To verify the backup, you can:" +echo "1. Mount $TARGET_DEVICE and check the files" +echo "2. Boot from $TARGET_DEVICE if it's bootable" +echo "" \ No newline at end of file diff --git a/simple_backup_gui.py b/simple_backup_gui.py new file mode 100755 index 0000000..2c17bfe --- /dev/null +++ b/simple_backup_gui.py @@ -0,0 +1,436 @@ +#!/usr/bin/env python3 +""" +Simple LVM Backup GUI +Just the basics: snapshot -> copy -> cleanup +No complex logic, just simple reliable backups +""" + +import tkinter as tk +from tkinter import ttk, messagebox +import subprocess +import threading +import os +import time + +class SimpleBackupGUI: + def __init__(self, root): + self.root = root + self.root.title("Simple LVM Backup") + self.root.geometry("600x400") + + # State tracking + self.backup_running = False + self.current_snapshot = None + + self.setup_ui() + self.refresh_drives() + + def setup_ui(self): + # Main frame + main_frame = ttk.Frame(self.root, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # Backup mode selection + ttk.Label(main_frame, text="Backup Mode:").grid(row=0, column=0, sticky=tk.W, pady=5) + self.mode_var = tk.StringVar(value="lv_to_lv") + mode_frame = ttk.Frame(main_frame) + mode_frame.grid(row=0, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5) + + ttk.Radiobutton(mode_frame, text="LV → LV (update existing)", + variable=self.mode_var, value="lv_to_lv", + command=self.on_mode_change).pack(side=tk.LEFT, padx=5) + ttk.Radiobutton(mode_frame, text="LV → Raw Device (fresh)", + variable=self.mode_var, value="lv_to_raw", + command=self.on_mode_change).pack(side=tk.LEFT, padx=5) + 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) + + # Source selection + ttk.Label(main_frame, text="Source:").grid(row=1, column=0, sticky=tk.W, pady=5) + self.source_var = tk.StringVar() + self.source_combo = ttk.Combobox(main_frame, textvariable=self.source_var, width=50) + self.source_combo.grid(row=1, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5) + + # Target selection + ttk.Label(main_frame, text="Target:").grid(row=2, column=0, sticky=tk.W, pady=5) + self.target_var = tk.StringVar() + 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) + + # Refresh button + ttk.Button(main_frame, text="Refresh", command=self.refresh_drives).grid(row=3, 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) + + # 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) + + # Progress area + ttk.Label(main_frame, text="Progress:").grid(row=4, 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) + + # Log area + ttk.Label(main_frame, text="Log:").grid(row=6, 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) + + self.log_text = tk.Text(log_frame, height=15, width=70) + scrollbar = ttk.Scrollbar(log_frame, orient="vertical", command=self.log_text.yview) + self.log_text.configure(yscrollcommand=scrollbar.set) + + self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S)) + + # Configure grid weights + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + main_frame.columnconfigure(1, weight=1) + main_frame.rowconfigure(7, weight=1) + log_frame.columnconfigure(0, weight=1) + log_frame.rowconfigure(0, weight=1) + + def on_mode_change(self): + """Handle backup mode change""" + self.refresh_drives() + + def log(self, message): + """Add message to log""" + timestamp = time.strftime("%H:%M:%S") + self.log_text.insert(tk.END, f"[{timestamp}] {message}\n") + self.log_text.see(tk.END) + self.root.update_idletasks() + + def run_command(self, cmd, show_output=True): + """Run a command and return result""" + try: + if show_output: + self.log(f"Running: {cmd}") + + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + + if result.returncode != 0: + if show_output: + self.log(f"ERROR: {result.stderr.strip()}") + return False, result.stderr.strip() + else: + if show_output and result.stdout.strip(): + self.log(f"Output: {result.stdout.strip()}") + return True, result.stdout.strip() + + except Exception as e: + if show_output: + self.log(f"ERROR: {str(e)}") + return False, str(e) + + def refresh_drives(self): + """Refresh available drives based on selected mode""" + mode = self.mode_var.get() + self.log(f"Refreshing drives for mode: {mode}") + + # Source options + if mode == "vg_to_raw": + # Show volume groups + success, output = self.run_command("vgs --noheadings -o vg_name,vg_size,pv_count", show_output=False) + if success: + source_list = [] + for line in output.strip().split('\n'): + if line.strip(): + parts = line.strip().split() + if len(parts) >= 3: + vg_name = parts[0] + vg_size = parts[1] + pv_count = parts[2] + source_list.append(f"{vg_name} ({vg_size}) [{pv_count} PVs]") + + self.source_combo['values'] = source_list + self.log(f"Found {len(source_list)} volume groups") + else: + self.log("No volume groups found") + self.source_combo['values'] = [] + else: + # Show logical volumes (lv_to_lv and lv_to_raw) + success, output = self.run_command("lvs --noheadings -o lv_path,lv_size,vg_name", show_output=False) + if success: + lv_list = [] + for line in output.strip().split('\n'): + if line.strip(): + parts = line.strip().split() + if len(parts) >= 3: + lv_path = parts[0] + lv_size = parts[1] + vg_name = parts[2] + lv_list.append(f"{lv_path} ({lv_size}) [VG: {vg_name}]") + + self.source_combo['values'] = lv_list + self.log(f"Found {len(lv_list)} logical volumes") + else: + self.log("No LVM volumes found") + self.source_combo['values'] = [] + + # Target options + if mode == "lv_to_lv": + # Show existing logical volumes on external drives + success, output = self.run_command("lvs --noheadings -o lv_path,lv_size,vg_name", show_output=False) + if success: + target_list = [] + for line in output.strip().split('\n'): + if line.strip(): + parts = line.strip().split() + if len(parts) >= 3: + lv_path = parts[0] + lv_size = parts[1] + vg_name = parts[2] + # Filter out internal VGs (you might want to customize this) + if "migration" in vg_name or "external" in vg_name or "backup" in vg_name: + target_list.append(f"{lv_path} ({lv_size}) [VG: {vg_name}]") + + self.target_combo['values'] = target_list + self.log(f"Found {len(target_list)} target logical volumes") + else: + self.log("No target LVs found") + self.target_combo['values'] = [] + 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) + if success: + drive_list = [] + for line in output.strip().split('\n'): + if line.strip(): + parts = line.strip().split(None, 2) + if len(parts) >= 2: + name = parts[0] + size = parts[1] + model = parts[2] if len(parts) > 2 else "Unknown" + drive_list.append(f"/dev/{name} ({size}) {model}") + + self.target_combo['values'] = drive_list + self.log(f"Found {len(drive_list)} target drives") + else: + self.log("No block devices found") + self.target_combo['values'] = [] + + 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?" + + if not messagebox.askyesno("Confirm Backup", msg): + return + + # Start backup in thread + self.backup_running = True + self.backup_btn.config(state="disabled") + self.stop_btn.config(state="normal") + self.progress.start() + + thread = threading.Thread(target=self.backup_worker, args=(mode, source, target)) + thread.daemon = True + thread.start() + + def backup_worker(self, mode, source, target): + """The actual backup work""" + try: + self.log(f"=== Starting {mode} backup ===") + + if mode == "lv_to_lv": + self.backup_lv_to_lv(source, target) + elif mode == "lv_to_raw": + self.backup_lv_to_raw(source, target) + elif mode == "vg_to_raw": + self.backup_vg_to_raw(source, target) + + self.log("=== Backup completed successfully! ===") + self.root.after(0, lambda: messagebox.showinfo("Success", "Backup completed successfully!")) + + except Exception as e: + self.log(f"ERROR: {str(e)}") + self.cleanup_on_error() + self.root.after(0, lambda: messagebox.showerror("Backup Failed", str(e))) + + finally: + # Reset UI state + self.root.after(0, self.reset_ui_state) + + def backup_lv_to_lv(self, source_lv, target_lv): + """Backup LV to existing LV""" + self.log("Mode: LV to LV (updating existing backup)") + + # Create snapshot of source + vg_name = source_lv.split('/')[2] + lv_name = source_lv.split('/')[3] + snapshot_name = f"{lv_name}_backup_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}") + + # Copy snapshot to target LV + self.log(f"Copying {self.current_snapshot} to {target_lv}") + + success, _ = self.run_command("which pv", show_output=False) + if success: + copy_cmd = f"pv {self.current_snapshot} | dd of={target_lv} bs=4M" + else: + copy_cmd = f"dd if={self.current_snapshot} of={target_lv} bs=4M status=progress" + + success, output = self.run_command(copy_cmd) + if not success: + raise Exception(f"Failed to copy data: {output}") + + # Cleanup + 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_lv_to_raw(self, source_lv, target_device): + """Backup LV to raw device (fresh backup)""" + self.log("Mode: LV to Raw Device (fresh backup)") + + # Create snapshot of source + vg_name = source_lv.split('/')[2] + lv_name = source_lv.split('/')[3] + snapshot_name = f"{lv_name}_backup_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}") + + # Copy snapshot to target device + self.log(f"Copying {self.current_snapshot} to {target_device}") + self.log("This will create a raw block-level copy") + + success, _ = self.run_command("which pv", show_output=False) + if success: + copy_cmd = f"pv {self.current_snapshot} | dd of={target_device} bs=4M" + else: + copy_cmd = f"dd if={self.current_snapshot} of={target_device} bs=4M status=progress" + + success, output = self.run_command(copy_cmd) + if not success: + raise Exception(f"Failed to copy data: {output}") + + # Cleanup + 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_raw(self, source_vg, target_device): + """Backup entire VG to raw device""" + self.log("Mode: Entire VG to Raw Device") + self.log("This will create a complete clone including LVM metadata") + + # Get the physical volume(s) that make up this VG + success, output = self.run_command(f"vgs --noheadings -o pv_name {source_vg}", show_output=False) + if not success: + raise Exception(f"Failed to get PV info for VG {source_vg}") + + # For simplicity, we'll use the first PV as the source + # In a real implementation, you might want to handle multiple PVs + pv_list = output.strip().split() + if not pv_list: + raise Exception(f"No physical volumes found for VG {source_vg}") + + source_pv = pv_list[0] + self.log(f"Source PV: {source_pv}") + + # Copy the entire physical volume + self.log(f"Copying entire PV {source_pv} to {target_device}") + self.log("This preserves LVM metadata and all logical volumes") + + success, _ = self.run_command("which pv", show_output=False) + if success: + copy_cmd = f"pv {source_pv} | dd of={target_device} bs=4M" + else: + copy_cmd = f"dd if={source_pv} of={target_device} bs=4M status=progress" + + success, output = self.run_command(copy_cmd) + if not success: + raise Exception(f"Failed to copy PV: {output}") + + self.log("VG copy completed - target device now contains complete LVM structure") + + def cleanup_on_error(self): + """Clean up on error""" + if self.current_snapshot: + self.log("Attempting to clean up snapshot after error") + success, output = self.run_command(f"lvremove -f {self.current_snapshot}") + if success: + self.log("Snapshot cleaned up") + else: + self.log(f"Failed to clean up snapshot: {output}") + self.current_snapshot = None + + def emergency_stop(self): + """Emergency stop - kill any running processes""" + self.log("EMERGENCY STOP requested") + + # Kill any dd or pv processes + self.run_command("pkill -f 'dd.*if=.*dev'", show_output=False) + self.run_command("pkill -f 'pv.*dev'", show_output=False) + + self.cleanup_on_error() + self.backup_running = False + self.reset_ui_state() + + messagebox.showwarning("Stopped", "Backup process stopped. Check log for any cleanup needed.") + + def reset_ui_state(self): + """Reset UI to normal state""" + self.backup_running = False + self.backup_btn.config(state="normal") + self.stop_btn.config(state="disabled") + self.progress.stop() + +def main(): + # Check if running as root + if os.geteuid() != 0: + print("This application needs to run as root for LVM operations.") + print("Please run: sudo python3 simple_backup_gui.py") + return + + root = tk.Tk() + app = SimpleBackupGUI(root) + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file