Issues fixed: 1. Source widget now correctly switches between combobox and listbox 2. Mode changes properly refresh and show correct source options 3. Added better logging to track widget switches 4. Clear selections when switching modes 5. Force UI update after widget changes Now: - LV modes (lv_to_borg, files_to_borg) show multi-select listbox - VG modes (vg_to_borg) show single-select combobox - Widget switching works correctly when changing modes - Borg settings appear for all Borg modes
1042 lines
47 KiB
Python
Executable File
1042 lines
47 KiB
Python
Executable File
#!/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() # Initialize with correct widgets for default mode
|
|
|
|
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)
|
|
ttk.Radiobutton(mode_frame, text="LV → Borg (block)",
|
|
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 (block)",
|
|
variable=self.mode_var, value="vg_to_borg",
|
|
command=self.on_mode_change).pack(side=tk.LEFT, padx=5)
|
|
ttk.Radiobutton(mode_frame, text="Files → Borg",
|
|
variable=self.mode_var, value="files_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)
|
|
|
|
# Create a frame for source selection that can switch between combobox and listbox
|
|
self.source_frame = ttk.Frame(main_frame)
|
|
self.source_frame.grid(row=1, column=1, columnspan=2, sticky=(tk.W, tk.E), pady=5)
|
|
|
|
# Single selection (combobox) - for most modes
|
|
self.source_var = tk.StringVar()
|
|
self.source_combo = ttk.Combobox(self.source_frame, textvariable=self.source_var, width=50)
|
|
self.source_combo.grid(row=0, column=0, sticky=(tk.W, tk.E))
|
|
|
|
# Multi-selection (listbox) - for LV modes
|
|
self.source_listbox_frame = ttk.Frame(self.source_frame)
|
|
|
|
# Help text for multi-selection
|
|
help_text = ttk.Label(self.source_listbox_frame, text="Hold Ctrl/Cmd to select multiple LVs",
|
|
font=("TkDefaultFont", 8), foreground="gray")
|
|
help_text.grid(row=0, column=0, columnspan=2, sticky=tk.W, pady=(0, 2))
|
|
|
|
self.source_listbox = tk.Listbox(self.source_listbox_frame, height=6, selectmode=tk.EXTENDED)
|
|
source_scrollbar = ttk.Scrollbar(self.source_listbox_frame, orient="vertical", command=self.source_listbox.yview)
|
|
self.source_listbox.configure(yscrollcommand=source_scrollbar.set)
|
|
self.source_listbox.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
|
source_scrollbar.grid(row=1, column=1, sticky=(tk.N, tk.S))
|
|
self.source_listbox_frame.columnconfigure(0, weight=1)
|
|
self.source_listbox_frame.rowconfigure(1, weight=1)
|
|
|
|
# Configure source frame
|
|
self.source_frame.columnconfigure(0, weight=1)
|
|
|
|
# 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)
|
|
|
|
# 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)
|
|
|
|
# Snapshot size option
|
|
ttk.Label(self.borg_frame, text="Snapshot Size:").grid(row=3, column=0, sticky=tk.W, pady=2)
|
|
self.snapshot_size_var = tk.StringVar(value="auto")
|
|
snapshot_combo = ttk.Combobox(self.borg_frame, textvariable=self.snapshot_size_var,
|
|
values=["auto", "conservative", "generous"], state="readonly", width=15)
|
|
snapshot_combo.grid(row=3, column=1, sticky=tk.W, pady=2)
|
|
|
|
# Help text for snapshot sizes
|
|
help_label = ttk.Label(self.borg_frame, text="auto: smart sizing, conservative: 5%, generous: 25%",
|
|
font=("TkDefaultFont", 8), foreground="gray")
|
|
help_label.grid(row=3, column=2, sticky=tk.W, padx=(5, 0), pady=2)
|
|
|
|
# Read-only mode for full filesystems
|
|
self.readonly_mode = tk.BooleanVar()
|
|
ttk.Checkbutton(self.borg_frame, text="Read-only mode (for very full filesystems)",
|
|
variable=self.readonly_mode).grid(row=5, column=0, columnspan=3, sticky=tk.W, pady=2)
|
|
|
|
# Passphrase
|
|
ttk.Label(self.borg_frame, text="Passphrase:").grid(row=4, 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=4, column=1, sticky=(tk.W, tk.E), pady=2)
|
|
|
|
# Configure borg frame grid
|
|
self.borg_frame.columnconfigure(1, weight=1)
|
|
self.borg_frame.columnconfigure(2, weight=0)
|
|
|
|
# Refresh button
|
|
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=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=4, column=2, pady=10)
|
|
|
|
# Progress area
|
|
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=6, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
|
|
|
|
# Log area
|
|
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=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)
|
|
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(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"""
|
|
mode = self.mode_var.get()
|
|
|
|
# Show/hide Borg settings
|
|
if mode in ["lv_to_borg", "vg_to_borg", "files_to_borg"]:
|
|
self.borg_frame.grid()
|
|
else:
|
|
self.borg_frame.grid_remove()
|
|
|
|
# Always refresh drives when mode changes
|
|
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 calculate_snapshot_size(self, lv_size_bytes):
|
|
"""Calculate appropriate snapshot size based on LV size and user preference"""
|
|
mode = self.snapshot_size_var.get()
|
|
|
|
if mode == "conservative":
|
|
# 5% of LV size, minimum 1GB
|
|
snapshot_size_bytes = max(lv_size_bytes // 20, 1024**3)
|
|
elif mode == "generous":
|
|
# 25% of LV size, minimum 2GB
|
|
snapshot_size_bytes = max(lv_size_bytes // 4, 2 * 1024**3)
|
|
else: # auto
|
|
# 10% of LV size, minimum 1GB, but increase for very large LVs
|
|
base_percentage = 10
|
|
if lv_size_bytes > 50 * 1024**3: # >50GB
|
|
base_percentage = 15 # Use 15% for large, potentially active LVs
|
|
snapshot_size_bytes = max(lv_size_bytes * base_percentage // 100, 1024**3)
|
|
|
|
snapshot_size_gb = snapshot_size_bytes // (1024**3)
|
|
return f"{snapshot_size_gb}G"
|
|
|
|
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}")
|
|
|
|
# Clear target combo for Borg modes
|
|
if mode in ["lv_to_borg", "vg_to_borg", "files_to_borg"]:
|
|
self.target_combo['values'] = []
|
|
self.target_var.set("")
|
|
|
|
# Show appropriate source selection widget
|
|
if mode in ["lv_to_borg", "files_to_borg"]:
|
|
# Multi-selection for individual LV modes
|
|
self.source_combo.grid_remove()
|
|
self.source_listbox_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
|
self.log("Switched to multi-selection listbox for LV selection")
|
|
else:
|
|
# Single selection for VG modes and other modes
|
|
self.source_listbox_frame.grid_remove()
|
|
self.source_combo.grid(row=0, column=0, sticky=(tk.W, tk.E))
|
|
self.log("Switched to single-selection combobox")
|
|
|
|
# Update the window to force widget refresh
|
|
self.root.update_idletasks()
|
|
|
|
# Source options
|
|
if mode == "vg_to_raw" or mode == "vg_to_borg":
|
|
# 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.source_var.set("") # Clear selection
|
|
self.log(f"Found {len(source_list)} volume groups")
|
|
else:
|
|
self.log("No volume groups found")
|
|
self.source_combo['values'] = []
|
|
else:
|
|
# Show logical volumes (all LV modes: lv_to_lv, lv_to_raw, lv_to_borg, files_to_borg)
|
|
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}]")
|
|
|
|
# Update the appropriate widget
|
|
if mode in ["lv_to_borg", "files_to_borg"]:
|
|
# Update listbox for multi-selection
|
|
self.source_listbox.delete(0, tk.END)
|
|
for lv in lv_list:
|
|
self.source_listbox.insert(tk.END, lv)
|
|
self.log(f"Loaded {len(lv_list)} logical volumes into listbox")
|
|
else:
|
|
# Update combobox for single selection
|
|
self.source_combo['values'] = lv_list
|
|
self.source_var.set("") # Clear selection
|
|
self.log(f"Loaded {len(lv_list)} logical volumes into combobox")
|
|
|
|
self.log(f"Found {len(lv_list)} logical volumes")
|
|
else:
|
|
self.log("No LVM volumes found")
|
|
if mode in ["lv_to_borg", "files_to_borg"]:
|
|
self.source_listbox.delete(0, tk.END)
|
|
else:
|
|
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'] = []
|
|
elif mode in ["lv_to_borg", "vg_to_borg", "files_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)
|
|
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"""
|
|
mode = self.mode_var.get()
|
|
|
|
# Get selected sources based on mode
|
|
if mode in ["lv_to_borg", "files_to_borg"]:
|
|
# Multi-selection mode
|
|
selected_indices = self.source_listbox.curselection()
|
|
if not selected_indices:
|
|
messagebox.showerror("Error", "Please select at least one logical volume")
|
|
return
|
|
|
|
selected_sources = []
|
|
for index in selected_indices:
|
|
selected_sources.append(self.source_listbox.get(index).split()[0])
|
|
|
|
if not self.repo_path_var.get():
|
|
messagebox.showerror("Error", "Please select 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
|
|
|
|
repo_path = self.repo_path_var.get()
|
|
|
|
# Build confirmation message for multiple LVs
|
|
if mode == "lv_to_borg":
|
|
msg = f"Borg backup of {len(selected_sources)} LVs (block-level):\n\n"
|
|
else: # files_to_borg
|
|
msg = f"Borg backup of {len(selected_sources)} LVs (file-level):\n\n"
|
|
|
|
for source in selected_sources:
|
|
msg += f"• {source}\n"
|
|
|
|
msg += f"\nRepository: {repo_path}\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?"
|
|
|
|
if not messagebox.askyesno("Confirm Backup", msg):
|
|
return
|
|
|
|
# Start multi-LV backup
|
|
self.backup_running = True
|
|
self.backup_btn.config(state="disabled")
|
|
self.stop_btn.config(state="normal")
|
|
self.progress.start()
|
|
|
|
thread = threading.Thread(target=self.multi_lv_backup_worker, args=(mode, selected_sources, repo_path))
|
|
thread.daemon = True
|
|
thread.start()
|
|
return
|
|
|
|
# Original single-selection logic for other modes
|
|
# Validate inputs based on mode
|
|
if mode in ["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 VG Borg
|
|
msg = f"Borg backup of entire VG (block-level):\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
|
|
|
|
# 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)
|
|
elif mode == "lv_to_borg":
|
|
self.backup_lv_to_borg(source, target)
|
|
elif mode == "files_to_borg":
|
|
self.backup_files_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!"))
|
|
|
|
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 multi_lv_backup_worker(self, mode, selected_sources, repo_path):
|
|
"""Backup multiple LVs sequentially"""
|
|
try:
|
|
self.log(f"=== Starting multi-LV {mode} backup ===")
|
|
self.log(f"Selected {len(selected_sources)} logical volumes")
|
|
|
|
# Initialize repository once if needed
|
|
if self.create_new_repo.get():
|
|
borg_env = os.environ.copy()
|
|
if self.passphrase_var.get():
|
|
borg_env['BORG_PASSPHRASE'] = self.passphrase_var.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")
|
|
|
|
# Backup each LV
|
|
successful_backups = 0
|
|
failed_backups = []
|
|
|
|
for i, source_lv in enumerate(selected_sources, 1):
|
|
self.log(f"--- Processing LV {i}/{len(selected_sources)}: {source_lv} ---")
|
|
|
|
try:
|
|
if mode == "lv_to_borg":
|
|
self.backup_lv_to_borg(source_lv, repo_path)
|
|
elif mode == "files_to_borg":
|
|
self.backup_files_to_borg(source_lv, repo_path)
|
|
|
|
successful_backups += 1
|
|
self.log(f"Successfully backed up {source_lv}")
|
|
|
|
except Exception as e:
|
|
self.log(f"Failed to backup {source_lv}: {str(e)}")
|
|
failed_backups.append(source_lv)
|
|
# Continue with next LV instead of stopping
|
|
|
|
# Summary
|
|
self.log(f"=== Multi-LV backup completed ===")
|
|
self.log(f"Successful: {successful_backups}/{len(selected_sources)}")
|
|
if failed_backups:
|
|
self.log(f"Failed: {', '.join(failed_backups)}")
|
|
|
|
# Show completion dialog
|
|
if failed_backups:
|
|
msg = f"Backup completed with some failures:\n\nSuccessful: {successful_backups}\nFailed: {len(failed_backups)}\n\nFailed LVs:\n" + '\n'.join(failed_backups)
|
|
self.root.after(0, lambda: messagebox.showwarning("Backup Completed with Warnings", msg))
|
|
else:
|
|
self.root.after(0, lambda: messagebox.showinfo("Success", f"All {successful_backups} LVs backed up 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 backup_lv_to_borg(self, source_lv, repo_path):
|
|
"""Backup LV to Borg repository (block-level)"""
|
|
self.log("Mode: LV to Borg Repository (block-level backup)")
|
|
|
|
# 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}"
|
|
|
|
# Get LV size to determine appropriate snapshot size
|
|
success, lv_size_output = self.run_command(f"lvs --noheadings -o lv_size --units b {source_lv}", show_output=False)
|
|
if success:
|
|
lv_size_bytes = int(lv_size_output.strip().replace('B', ''))
|
|
snapshot_size = self.calculate_snapshot_size(lv_size_bytes)
|
|
else:
|
|
snapshot_size = "2G" # Default fallback
|
|
|
|
self.log(f"Creating snapshot: {snapshot_name} (size: {snapshot_size})")
|
|
success, output = self.run_command(f"lvcreate -L{snapshot_size} -s -n {snapshot_name} {source_lv}")
|
|
if not success:
|
|
raise Exception(f"Failed to create snapshot: {output}")
|
|
|
|
# Create Borg backup of the raw block device
|
|
archive_name = f"lv_{lv_name}_{time.strftime('%Y%m%d_%H%M%S')}"
|
|
self.log(f"Creating Borg archive (block-level): {archive_name}")
|
|
self.log("Backing up raw snapshot block device to Borg...")
|
|
self.log("This preserves exact block-level state including filesystem metadata")
|
|
|
|
# Use stdin mode to pipe the block device into borg
|
|
borg_cmd = f"dd if={self.current_snapshot} bs=4M | borg create --stdin-name '{lv_name}.img' --progress --stats {repo_path}::{archive_name} -"
|
|
|
|
# Run command with Borg 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:
|
|
if line.strip():
|
|
self.log(line.strip())
|
|
|
|
process.wait()
|
|
if process.returncode != 0:
|
|
raise Exception("Borg block-level backup failed")
|
|
|
|
self.log("Block-level Borg backup completed successfully")
|
|
|
|
# Cleanup 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 (block-level)"""
|
|
self.log("Mode: Entire VG to Borg Repository (block-level backup)")
|
|
self.log("This will store all LV snapshots as raw block devices in Borg")
|
|
|
|
# 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)}")
|
|
|
|
# Create one archive with all LVs
|
|
archive_name = f"vg_{source_vg}_{time.strftime('%Y%m%d_%H%M%S')}"
|
|
self.log(f"Creating Borg archive (block-level): {archive_name}")
|
|
|
|
# Create temporary directory for organizing the LV images
|
|
import tempfile
|
|
temp_dir = tempfile.mkdtemp(prefix="borg_vg_backup_")
|
|
|
|
snapshots_created = []
|
|
|
|
try:
|
|
# Process each LV one by one to avoid space issues
|
|
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}"
|
|
|
|
# Get LV size to determine appropriate snapshot size
|
|
success, lv_size_output = self.run_command(f"lvs --noheadings -o lv_size --units b {lv_path}", show_output=False)
|
|
if success:
|
|
lv_size_bytes = int(lv_size_output.strip().replace('B', ''))
|
|
snapshot_size = self.calculate_snapshot_size(lv_size_bytes)
|
|
else:
|
|
snapshot_size = "2G" # Default fallback
|
|
|
|
self.log(f"Processing LV: {lv_name}")
|
|
self.log(f"Creating snapshot: {snapshot_name} (size: {snapshot_size})")
|
|
success, output = self.run_command(f"lvcreate -L{snapshot_size} -s -n {snapshot_name} {lv_path}")
|
|
if not success:
|
|
self.log(f"Warning: Failed to create snapshot for {lv_name}: {output}")
|
|
continue
|
|
|
|
snapshots_created.append(snapshot_path)
|
|
|
|
# Stream this LV directly to Borg using append mode (if supported)
|
|
# or create individual archives
|
|
archive_name_lv = f"vg_{source_vg}_lv_{lv_name}_{time.strftime('%Y%m%d_%H%M%S')}"
|
|
self.log(f"Backing up {lv_name} to archive: {archive_name_lv}")
|
|
|
|
# Use stdin mode to stream the block device
|
|
borg_cmd = f"dd if={snapshot_path} bs=4M | borg create --stdin-name '{lv_name}.img' --progress --stats {repo_path}::{archive_name_lv} -"
|
|
|
|
# Run command with Borg 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:
|
|
if line.strip():
|
|
self.log(line.strip())
|
|
|
|
process.wait()
|
|
if process.returncode != 0:
|
|
self.log(f"Warning: Backup of {lv_name} failed")
|
|
else:
|
|
self.log(f"Successfully backed up {lv_name}")
|
|
|
|
# Clean up this snapshot immediately to save space
|
|
self.log(f"Removing snapshot {snapshot_path}")
|
|
success, output = self.run_command(f"lvremove -f {snapshot_path}")
|
|
if success:
|
|
snapshots_created.remove(snapshot_path)
|
|
else:
|
|
self.log(f"Warning: Failed to remove snapshot {snapshot_path}: {output}")
|
|
|
|
self.log("Block-level VG Borg backup completed successfully")
|
|
self.log(f"Created individual archives for each LV in VG {source_vg}")
|
|
|
|
finally:
|
|
# Remove temporary directory
|
|
import shutil
|
|
try:
|
|
shutil.rmtree(temp_dir)
|
|
self.log("Temporary directory cleaned up")
|
|
except:
|
|
self.log(f"Warning: Could not remove temp directory {temp_dir}")
|
|
|
|
# Remove any remaining snapshots
|
|
for snapshot_path in snapshots_created:
|
|
self.log(f"Removing remaining 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 backup_files_to_borg(self, source_lv, repo_path):
|
|
"""Backup LV files to Borg repository (file-level via snapshot mount)"""
|
|
self.log("Mode: LV to Borg Repository (file-level backup)")
|
|
self.log("This will mount an LV snapshot and backup files (space-efficient)")
|
|
|
|
# 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}_files_snap"
|
|
self.current_snapshot = f"/dev/{vg_name}/{snapshot_name}"
|
|
|
|
# Get LV size to determine appropriate snapshot size
|
|
success, lv_size_output = self.run_command(f"lvs --noheadings -o lv_size --units b {source_lv}", show_output=False)
|
|
if success:
|
|
lv_size_bytes = int(lv_size_output.strip().replace('B', ''))
|
|
snapshot_size = self.calculate_snapshot_size(lv_size_bytes)
|
|
else:
|
|
snapshot_size = "2G" # Default fallback
|
|
|
|
self.log(f"Creating snapshot: {snapshot_name} (size: {snapshot_size})")
|
|
success, output = self.run_command(f"lvcreate -L{snapshot_size} -s -n {snapshot_name} {source_lv}")
|
|
if not success:
|
|
raise Exception(f"Failed to create snapshot: {output}")
|
|
|
|
# Create temporary mount point
|
|
import tempfile
|
|
temp_mount = tempfile.mkdtemp(prefix="borg_files_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 of files
|
|
archive_name = f"files_{lv_name}_{time.strftime('%Y%m%d_%H%M%S')}"
|
|
self.log(f"Creating Borg archive (file-level): {archive_name}")
|
|
self.log("Backing up files from mounted snapshot...")
|
|
self.log("This is space-efficient and skips empty blocks")
|
|
|
|
borg_cmd = f"borg create --progress --stats --compression auto,zstd {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 file-level backup failed")
|
|
|
|
self.log("File-level 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 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() |