Files
backup_to_external_m.2/old_scripts/lvm_backup_gui_fixed.py
root 72f9838f55 cleanup: Archive old complex scripts and documentation
- Move all old complex backup scripts to old_scripts/
- Archive previous documentation versions
- Clean up temporary files and debian packages
- Update README to focus on new simple system
- Keep only the enhanced simple backup system in main directory

Main directory now contains only:
- simple_backup_gui.py (GUI interface)
- enhanced_simple_backup.sh (CLI interface)
- list_drives.sh (helper)
- simple_backup.sh (basic CLI)
- SIMPLE_BACKUP_README.md (detailed docs)
- README.md (project overview)
2025-10-09 00:30:03 +02:00

459 lines
20 KiB
Python

#!/usr/bin/env python3
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
import subprocess
import threading
import re
import os
import time
class LVMBackupGUI:
def __init__(self, root):
self.root = root
self.root.title("LVM Backup Manager")
self.root.geometry("900x700")
# Configure style
style = ttk.Style()
style.theme_use('clam')
# State variables
self.backup_process = None
self.backup_running = False
self.backup_thread = None
# StringVars for UI
self.source_var = tk.StringVar()
self.target_var = tk.StringVar()
self.status_var = tk.StringVar(value="Ready")
self.progress_var = tk.DoubleVar()
self.stage_var = tk.StringVar(value="Waiting to start...")
self.speed_var = tk.StringVar(value="0 MB/s")
self.time_var = tk.StringVar(value="Not calculated")
self.size_var = tk.StringVar(value="Not calculated")
self.setup_ui()
self.refresh_drives()
def setup_ui(self):
"""Setup the user interface"""
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Title
title_frame = ttk.Frame(main_frame)
title_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 20))
title_label = ttk.Label(title_frame, text="🛡️ LVM Backup Manager",
font=('Arial', 16, 'bold'))
title_label.pack()
# Source Drive Selection
source_frame = ttk.LabelFrame(main_frame, text="Source Drive", padding="10")
source_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
ttk.Label(source_frame, text="Volume Group:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10))
self.source_combo = ttk.Combobox(source_frame, textvariable=self.source_var,
state="readonly", width=50)
self.source_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
self.source_combo.bind('<<ComboboxSelected>>', self.on_source_selected)
ttk.Button(source_frame, text="🔄 Refresh",
command=self.refresh_drives).grid(row=0, column=2)
# Arrow
arrow_frame = ttk.Frame(main_frame)
arrow_frame.grid(row=2, column=0, columnspan=2, pady=10)
ttk.Label(arrow_frame, text="", font=('Arial', 24)).pack()
# Target Drive Selection
target_frame = ttk.LabelFrame(main_frame, text="Target Drive", padding="10")
target_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
ttk.Label(target_frame, text="Volume Group:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10))
self.target_combo = ttk.Combobox(target_frame, textvariable=self.target_var,
state="readonly", width=50)
self.target_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
self.target_combo.bind('<<ComboboxSelected>>', self.on_target_selected)
ttk.Button(target_frame, text="🔄 Refresh",
command=self.refresh_drives).grid(row=0, column=2)
# Backup Information
info_frame = ttk.LabelFrame(main_frame, text="📊 Backup Information", padding="10")
info_frame.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
# Info grid
ttk.Label(info_frame, text="Total Size:").grid(row=0, column=0, sticky=tk.W, padx=(0, 20))
ttk.Label(info_frame, textvariable=self.size_var).grid(row=0, column=1, sticky=tk.W, padx=(0, 40))
ttk.Label(info_frame, text="Estimated Time:").grid(row=0, column=2, sticky=tk.W, padx=(0, 20))
ttk.Label(info_frame, textvariable=self.time_var).grid(row=0, column=3, sticky=tk.W)
ttk.Label(info_frame, text="Transfer Speed:").grid(row=1, column=0, sticky=tk.W, padx=(0, 20))
ttk.Label(info_frame, textvariable=self.speed_var).grid(row=1, column=1, sticky=tk.W, padx=(0, 40))
ttk.Label(info_frame, text="Status:").grid(row=1, column=2, sticky=tk.W, padx=(0, 20))
ttk.Label(info_frame, textvariable=self.status_var).grid(row=1, column=3, sticky=tk.W)
# Control Buttons
button_frame = ttk.Frame(main_frame)
button_frame.grid(row=5, column=0, columnspan=2, pady=20)
self.start_button = ttk.Button(button_frame, text="🚀 Start Backup",
command=self.start_backup, style='Accent.TButton')
self.start_button.pack(side=tk.LEFT, padx=(0, 10))
self.stop_button = ttk.Button(button_frame, text="⏹ Stop Backup",
command=self.stop_backup, state='disabled')
self.stop_button.pack(side=tk.LEFT, padx=(0, 10))
self.verify_button = ttk.Button(button_frame, text="✅ Verify Backup",
command=self.verify_backup)
self.verify_button.pack(side=tk.LEFT)
# Progress Section
progress_frame = ttk.LabelFrame(main_frame, text="📈 Progress", padding="10")
progress_frame.grid(row=6, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
ttk.Label(progress_frame, text="Overall Progress:").grid(row=0, column=0, sticky=tk.W, pady=(0, 5))
self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var,
length=400, mode='determinate')
self.progress_bar.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
# Current stage
stage_frame = ttk.Frame(progress_frame)
stage_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E))
ttk.Label(stage_frame, text="⚙️").pack(side=tk.LEFT, padx=(0, 5))
ttk.Label(stage_frame, textvariable=self.stage_var).pack(side=tk.LEFT)
# Log Output
log_frame = ttk.LabelFrame(main_frame, text="📝 Log Output", padding="10")
log_frame.grid(row=7, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
self.log_text = scrolledtext.ScrolledText(log_frame, height=10, width=80)
self.log_text.pack(fill=tk.BOTH, expand=True)
# Configure grid weights
main_frame.columnconfigure(0, weight=1)
main_frame.rowconfigure(7, weight=1)
source_frame.columnconfigure(1, weight=1)
target_frame.columnconfigure(1, weight=1)
info_frame.columnconfigure(1, weight=1)
info_frame.columnconfigure(3, weight=1)
progress_frame.columnconfigure(0, weight=1)
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
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 refresh_drives(self):
"""Refresh the list of available LVM volume groups"""
try:
# Get volume groups
result = subprocess.run(['sudo', 'vgs', '--noheadings', '-o', 'vg_name,vg_size,vg_free'],
capture_output=True, text=True)
if result.returncode == 0:
vgs = []
for line in result.stdout.strip().split('\\n'):
if line.strip():
parts = line.strip().split()
if len(parts) >= 3:
vg_name = parts[0]
vg_size = parts[1]
vg_free = parts[2]
vgs.append(f"{vg_name} ({vg_size} total, {vg_free} free)")
self.source_combo['values'] = vgs
self.target_combo['values'] = vgs
# Auto-select if only specific VGs found
if len(vgs) >= 2:
for vg in vgs:
if 'internal-vg' in vg:
self.source_var.set(vg)
elif 'migration-vg' in vg:
self.target_var.set(vg)
self.log(f"Found {len(vgs)} volume groups")
else:
self.log("Failed to get volume groups - are you running as root?")
except Exception as e:
self.log(f"Error refreshing drives: {e}")
def on_source_selected(self, event=None):
"""Handle source drive selection"""
self.calculate_backup_info()
def on_target_selected(self, event=None):
"""Handle target drive selection"""
self.calculate_backup_info()
def calculate_backup_info(self):
"""Calculate backup size and time estimates"""
if not self.source_var.get():
return
try:
source_vg = self.source_var.get().split()[0]
# Get logical volume sizes
result = subprocess.run(['sudo', 'lvs', source_vg, '--noheadings', '-o', 'lv_size', '--units', 'b'],
capture_output=True, text=True)
if result.returncode == 0:
total_bytes = 0
for line in result.stdout.strip().split('\\n'):
if line.strip():
size_str = line.strip().rstrip('B')
try:
total_bytes += int(size_str)
except ValueError:
continue
# Convert to GB
total_gb = total_bytes / (1024**3)
# Estimate time (assuming 250 MB/s average)
est_seconds = total_bytes / (250 * 1024 * 1024)
est_hours = int(est_seconds // 3600)
est_mins = int((est_seconds % 3600) // 60)
time_str = f"{est_hours}h {est_mins}m" if est_hours > 0 else f"{est_mins}m"
self.size_var.set(f"{total_gb:.1f} GB")
self.time_var.set(time_str)
self.speed_var.set("~250 MB/s")
except Exception as e:
self.log(f"Error calculating backup info: {e}")
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 drives")
return
source_vg = self.source_var.get().split()[0]
target_vg = self.target_var.get().split()[0]
if source_vg == target_vg:
messagebox.showerror("Error", "Source and target cannot be the same")
return
# Confirm
if not messagebox.askyesno("Confirm Backup",
f"This will OVERWRITE all data on {target_vg}!\\n\\nContinue?"):
return
# Update UI
self.backup_running = True
self.start_button.config(state='disabled')
self.stop_button.config(state='normal')
self.verify_button.config(state='disabled')
self.status_var.set("Running...")
self.progress_var.set(0)
self.stage_var.set("Starting backup...")
# Clear log
self.log_text.delete(1.0, tk.END)
self.log("🚀 Starting LVM block-level backup...")
# Start backup thread
self.backup_thread = threading.Thread(target=self.run_backup,
args=(source_vg, target_vg))
self.backup_thread.daemon = True
self.backup_thread.start()
def run_backup(self, source_vg, target_vg):
"""Execute the backup process"""
try:
# Create the backup script
script_content = f'''#!/bin/bash
set -e
SOURCE_VG="{source_vg}"
TARGET_VG="{target_vg}"
SNAPSHOT_SIZE="2G"
echo "[$(date '+%H:%M:%S')] Checking system requirements..."
echo "[$(date '+%H:%M:%S')] Cleaning up any existing snapshots..."
lvremove -f "$SOURCE_VG/root-backup-snap" 2>/dev/null || true
lvremove -f "$SOURCE_VG/home-backup-snap" 2>/dev/null || true
lvremove -f "$SOURCE_VG/boot-backup-snap" 2>/dev/null || true
echo "[$(date '+%H:%M:%S')] Creating LVM snapshots..."
lvcreate -L "$SNAPSHOT_SIZE" -s -n root-backup-snap "$SOURCE_VG/root"
lvcreate -L "$SNAPSHOT_SIZE" -s -n home-backup-snap "$SOURCE_VG/home"
lvcreate -L 1G -s -n boot-backup-snap "$SOURCE_VG/boot"
echo "STAGE:Snapshots created"
echo "[$(date '+%H:%M:%S')] Unmounting target volumes..."
umount "/dev/$TARGET_VG/home" 2>/dev/null || true
umount "/dev/$TARGET_VG/root" 2>/dev/null || true
umount "/dev/$TARGET_VG/boot" 2>/dev/null || true
echo "[$(date '+%H:%M:%S')] Cloning root volume..."
echo "STAGE:Cloning root volume"
dd if="/dev/$SOURCE_VG/root-backup-snap" of="/dev/$TARGET_VG/root" bs=64M status=progress
echo "STAGE:Root volume completed"
echo "[$(date '+%H:%M:%S')] Cloning home volume..."
echo "STAGE:Cloning home volume"
dd if="/dev/$SOURCE_VG/home-backup-snap" of="/dev/$TARGET_VG/home" bs=64M status=progress
echo "STAGE:Home volume completed"
echo "[$(date '+%H:%M:%S')] Cloning boot volume..."
echo "STAGE:Cloning boot volume"
dd if="/dev/$SOURCE_VG/boot-backup-snap" of="/dev/$TARGET_VG/boot" bs=64M status=progress
echo "STAGE:Boot volume completed"
echo "[$(date '+%H:%M:%S')] Cleaning up snapshots..."
lvremove -f "$SOURCE_VG/root-backup-snap"
lvremove -f "$SOURCE_VG/home-backup-snap"
lvremove -f "$SOURCE_VG/boot-backup-snap"
echo "[$(date '+%H:%M:%S')] Verifying backup..."
echo "STAGE:Verifying backup"
fsck -n "/dev/$TARGET_VG/root" 2>/dev/null || echo "Root filesystem verified"
fsck -n "/dev/$TARGET_VG/boot" 2>/dev/null || echo "Boot filesystem verified"
echo "STAGE:Backup completed successfully!"
echo "[$(date '+%H:%M:%S')] Backup completed successfully!"
'''
# Write temp script
with open('/tmp/gui_backup.sh', 'w') as f:
f.write(script_content)
os.chmod('/tmp/gui_backup.sh', 0o755)
# Execute backup
self.backup_process = subprocess.Popen(
['sudo', '/tmp/gui_backup.sh'],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1
)
# Monitor output
for line in iter(self.backup_process.stdout.readline, ''):
if not self.backup_running:
break
line = line.strip()
if line:
self.root.after(0, lambda l=line: self.log(l))
# Parse progress
if line.startswith('STAGE:'):
stage = line[6:]
self.root.after(0, lambda s=stage: self.stage_var.set(s))
if 'Snapshots created' in stage:
self.root.after(0, lambda: self.progress_var.set(10))
elif 'Cloning root' in stage:
self.root.after(0, lambda: self.progress_var.set(15))
elif 'Root volume completed' in stage:
self.root.after(0, lambda: self.progress_var.set(35))
elif 'Cloning home' in stage:
self.root.after(0, lambda: self.progress_var.set(40))
elif 'Home volume completed' in stage:
self.root.after(0, lambda: self.progress_var.set(80))
elif 'Cloning boot' in stage:
self.root.after(0, lambda: self.progress_var.set(85))
elif 'Boot volume completed' in stage:
self.root.after(0, lambda: self.progress_var.set(90))
elif 'Verifying' in stage:
self.root.after(0, lambda: self.progress_var.set(95))
elif 'completed successfully' in stage:
self.root.after(0, lambda: self.progress_var.set(100))
# Parse dd speed
if 'MB/s' in line or 'GB/s' in line:
speed_match = re.search(r'(\\d+(?:\\.\\d+)?)\\s*(MB|GB)/s', line)
if speed_match:
speed = f"{speed_match.group(1)} {speed_match.group(2)}/s"
self.root.after(0, lambda s=speed: self.speed_var.set(s))
# Check result
return_code = self.backup_process.wait()
success = return_code == 0
# Cleanup temp script
try:
os.remove('/tmp/gui_backup.sh')
except:
pass
# Update UI
self.root.after(0, lambda: self.backup_finished(success))
except Exception as e:
self.root.after(0, lambda: self.log(f"❌ Error: {e}"))
self.root.after(0, lambda: self.backup_finished(False))
def backup_finished(self, success):
"""Handle backup completion"""
self.backup_running = False
self.start_button.config(state='normal')
self.stop_button.config(state='disabled')
self.verify_button.config(state='normal')
if success:
self.status_var.set("Completed")
self.log("✅ Backup completed successfully!")
messagebox.showinfo("Success", "Backup completed successfully!\\n\\nYour external drive now contains a bootable copy.")
else:
self.status_var.set("Failed")
self.log("❌ Backup failed!")
messagebox.showerror("Error", "Backup failed. Check the log for details.")
def stop_backup(self):
"""Stop the backup process"""
if self.backup_process:
self.backup_running = False
self.backup_process.terminate()
self.log("⏹ Backup stopped by user")
self.backup_finished(False)
def verify_backup(self):
"""Verify the backup"""
if not self.target_var.get():
messagebox.showerror("Error", "Please select a target drive first")
return
target_vg = self.target_var.get().split()[0]
self.log(f"🔍 Verifying backup on {target_vg}...")
# Simple verification
try:
result = subprocess.run(['sudo', 'lvs', target_vg], capture_output=True, text=True)
if result.returncode == 0:
self.log("✅ Target volumes exist and are accessible")
messagebox.showinfo("Verification", "Basic verification passed!\\nTarget volumes are accessible.")
else:
self.log("❌ Verification failed - target volumes not accessible")
messagebox.showerror("Verification", "Verification failed!")
except Exception as e:
self.log(f"❌ Verification error: {e}")
messagebox.showerror("Error", f"Verification error: {e}")
def main():
root = tk.Tk()
app = LVMBackupGUI(root)
root.mainloop()
if __name__ == "__main__":
main()