#!/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) 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) 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) # 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=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"]: 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""" 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'] = [] 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) 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() # 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 # 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 == "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 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}" 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 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)}") snapshots_created = [] 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, lv_name)) # Create Borg archive with all block devices archive_name = f"vg_{source_vg}_{time.strftime('%Y%m%d_%H%M%S')}" self.log(f"Creating Borg archive (block-level): {archive_name}") self.log("Backing up all LV snapshots as raw block devices...") # Create temporary directory structure for the archive import tempfile temp_dir = tempfile.mkdtemp(prefix="borg_vg_backup_") try: # Copy all snapshots to temporary files that Borg can backup for snapshot_path, lv_name in snapshots_created: temp_file = os.path.join(temp_dir, f"{lv_name}.img") self.log(f"Creating temporary image file for {lv_name}") # Use dd to copy snapshot to temporary file copy_cmd = f"dd if={snapshot_path} of={temp_file} bs=4M" success, output = self.run_command(copy_cmd) if not success: raise Exception(f"Failed to create temp file for {lv_name}: {output}") # Create Borg backup of all the block device images borg_cmd = f"borg create --progress --stats {repo_path}::{archive_name} {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: if line.strip(): self.log(line.strip()) process.wait() if process.returncode != 0: raise Exception("Borg VG backup failed") self.log("Block-level VG Borg backup completed successfully") finally: # Remove temporary directory import shutil try: shutil.rmtree(temp_dir) self.log("Temporary files cleaned up") except: self.log(f"Warning: Could not remove temp directory {temp_dir}") finally: # Remove all snapshots for snapshot_path, lv_name 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: 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()