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)
This commit is contained in:
23
old_scripts/deb-package/usr/bin/lvm-backup-manager
Normal file
23
old_scripts/deb-package/usr/bin/lvm-backup-manager
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# LVM Backup Manager GUI Wrapper
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "LVM Backup Manager requires root privileges."
|
||||
echo "Please run with: sudo lvm-backup-manager"
|
||||
|
||||
# Try to launch with pkexec if available
|
||||
if command -v pkexec >/dev/null 2>&1; then
|
||||
exec pkexec "$0" "$@"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set proper path
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
export PATH="$SCRIPT_DIR:$PATH"
|
||||
|
||||
# Launch the GUI
|
||||
cd "$SCRIPT_DIR"
|
||||
exec python3 "$SCRIPT_DIR/lvm_backup_gui.py" "$@"
|
||||
193
old_scripts/deb-package/usr/bin/lvm-block-backup
Normal file
193
old_scripts/deb-package/usr/bin/lvm-block-backup
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/bin/bash
|
||||
|
||||
# LVM Block-Level Backup Script
|
||||
# Creates consistent snapshots and clones entire volumes block-for-block
|
||||
# This is the ONLY backup script you need!
|
||||
|
||||
set -e
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Configuration
|
||||
SOURCE_VG="internal-vg"
|
||||
TARGET_VG="migration-vg"
|
||||
SNAPSHOT_SIZE="2G"
|
||||
LOG_FILE="/var/log/lvm-block-backup.log"
|
||||
|
||||
log() {
|
||||
local message="[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
echo -e "${BLUE}$message${NC}"
|
||||
echo "$message" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
error() {
|
||||
local message="[ERROR] $1"
|
||||
echo -e "${RED}$message${NC}" >&2
|
||||
echo "$message" >> "$LOG_FILE"
|
||||
exit 1
|
||||
}
|
||||
|
||||
success() {
|
||||
local message="[SUCCESS] $1"
|
||||
echo -e "${GREEN}$message${NC}"
|
||||
echo "$message" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
warning() {
|
||||
local message="[WARNING] $1"
|
||||
echo -e "${YELLOW}$message${NC}"
|
||||
echo "$message" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
check_requirements() {
|
||||
log "Checking system requirements..."
|
||||
|
||||
# Check if running as root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
error "This script must be run as root. Use: sudo $0"
|
||||
fi
|
||||
|
||||
# Check if source VG exists
|
||||
if ! vgs "$SOURCE_VG" >/dev/null 2>&1; then
|
||||
error "Source volume group '$SOURCE_VG' not found"
|
||||
fi
|
||||
|
||||
# Check if target VG exists
|
||||
if ! vgs "$TARGET_VG" >/dev/null 2>&1; then
|
||||
error "Target volume group '$TARGET_VG' not found"
|
||||
fi
|
||||
|
||||
# Check if volumes exist
|
||||
local volumes=("root" "home" "boot")
|
||||
for vol in "${volumes[@]}"; do
|
||||
if ! lvs "$SOURCE_VG/$vol" >/dev/null 2>&1; then
|
||||
error "Source logical volume '$SOURCE_VG/$vol' not found"
|
||||
fi
|
||||
if ! lvs "$TARGET_VG/$vol" >/dev/null 2>&1; then
|
||||
error "Target logical volume '$TARGET_VG/$vol' not found"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check available space for snapshots
|
||||
local vg_free=$(vgs --noheadings -o vg_free --units g "$SOURCE_VG" | tr -d ' G')
|
||||
local vg_free_int=${vg_free%.*}
|
||||
|
||||
if [ "$vg_free_int" -lt 6 ]; then
|
||||
error "Insufficient free space for snapshots. Need at least 6GB, have ${vg_free}GB"
|
||||
fi
|
||||
|
||||
success "System requirements check passed"
|
||||
}
|
||||
|
||||
cleanup_snapshots() {
|
||||
log "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
|
||||
}
|
||||
|
||||
create_snapshots() {
|
||||
log "Creating LVM snapshots for consistent backup..."
|
||||
|
||||
cleanup_snapshots
|
||||
|
||||
# Create snapshots
|
||||
lvcreate -L "$SNAPSHOT_SIZE" -s -n root-backup-snap "$SOURCE_VG/root" || error "Failed to create root snapshot"
|
||||
lvcreate -L "$SNAPSHOT_SIZE" -s -n home-backup-snap "$SOURCE_VG/home" || error "Failed to create home snapshot"
|
||||
lvcreate -L 1G -s -n boot-backup-snap "$SOURCE_VG/boot" || error "Failed to create boot snapshot"
|
||||
|
||||
success "Snapshots created successfully"
|
||||
}
|
||||
|
||||
clone_volumes() {
|
||||
log "Starting block-level volume cloning..."
|
||||
|
||||
# Unmount target volumes if mounted
|
||||
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
|
||||
|
||||
# Clone root volume
|
||||
log "Cloning root volume (this may take a while)..."
|
||||
dd if="/dev/$SOURCE_VG/root-backup-snap" of="/dev/$TARGET_VG/root" bs=64M status=progress || error "Failed to clone root volume"
|
||||
success "Root volume cloned"
|
||||
|
||||
# Clone home volume
|
||||
log "Cloning home volume (this will take the longest)..."
|
||||
dd if="/dev/$SOURCE_VG/home-backup-snap" of="/dev/$TARGET_VG/home" bs=64M status=progress || error "Failed to clone home volume"
|
||||
success "Home volume cloned"
|
||||
|
||||
# Clone boot volume
|
||||
log "Cloning boot volume..."
|
||||
dd if="/dev/$SOURCE_VG/boot-backup-snap" of="/dev/$TARGET_VG/boot" bs=64M status=progress || error "Failed to clone boot volume"
|
||||
success "Boot volume cloned"
|
||||
}
|
||||
|
||||
verify_backup() {
|
||||
log "Verifying backup integrity..."
|
||||
|
||||
# Check filesystem integrity
|
||||
fsck -n "/dev/$TARGET_VG/root" || warning "Root filesystem check showed issues"
|
||||
fsck -n "/dev/$TARGET_VG/boot" || warning "Boot filesystem check showed issues"
|
||||
|
||||
# Note: Can't check encrypted home volume without decryption
|
||||
|
||||
success "Backup verification completed"
|
||||
}
|
||||
|
||||
show_backup_info() {
|
||||
log "Creating backup information..."
|
||||
|
||||
cat << EOF
|
||||
|
||||
=====================================
|
||||
LVM Block-Level Backup Complete
|
||||
=====================================
|
||||
Date: $(date)
|
||||
Source: $SOURCE_VG
|
||||
Target: $TARGET_VG
|
||||
|
||||
Volume Information:
|
||||
$(lvs $SOURCE_VG $TARGET_VG)
|
||||
|
||||
The external drive now contains an exact block-level copy of your internal drive.
|
||||
You can boot from the external drive by selecting it in your BIOS/UEFI boot menu.
|
||||
|
||||
To mount the backup volumes:
|
||||
sudo mount /dev/$TARGET_VG/root /mnt/backup-root
|
||||
sudo mount /dev/$TARGET_VG/boot /mnt/backup-boot
|
||||
# Home volume needs LUKS decryption first
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
echo -e "${GREEN}=== LVM Block-Level Backup Tool ===${NC}"
|
||||
echo "This will create an exact copy of your internal LVM volumes to the external drive."
|
||||
echo
|
||||
|
||||
read -p "Are you sure you want to proceed? This will OVERWRITE data on $TARGET_VG! [y/N]: " confirm
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
echo "Backup cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_requirements
|
||||
create_snapshots
|
||||
clone_volumes
|
||||
cleanup_snapshots
|
||||
verify_backup
|
||||
show_backup_info
|
||||
|
||||
success "Backup completed successfully!"
|
||||
}
|
||||
|
||||
# Trap to cleanup on exit
|
||||
trap cleanup_snapshots EXIT
|
||||
|
||||
main "$@"
|
||||
541
old_scripts/deb-package/usr/bin/lvm_backup_gui.py
Normal file
541
old_scripts/deb-package/usr/bin/lvm_backup_gui.py
Normal file
@@ -0,0 +1,541 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
LVM Backup GUI - Professional interface for LVM snapshot backups
|
||||
Creates block-level clones of LVM volumes with progress monitoring
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, scrolledtext
|
||||
import subprocess
|
||||
import threading
|
||||
import re
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class LVMBackupGUI:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("LVM Backup Manager")
|
||||
self.root.geometry("900x700")
|
||||
self.root.resizable(True, True)
|
||||
|
||||
# Configure style
|
||||
self.setup_styles()
|
||||
|
||||
# Variables
|
||||
self.source_vg = tk.StringVar()
|
||||
self.target_vg = tk.StringVar()
|
||||
self.backup_running = False
|
||||
self.backup_process = None
|
||||
|
||||
# Create GUI
|
||||
self.create_widgets()
|
||||
self.refresh_drives()
|
||||
|
||||
def setup_styles(self):
|
||||
"""Configure modern styling"""
|
||||
style = ttk.Style()
|
||||
|
||||
# Configure colors and fonts
|
||||
self.colors = {
|
||||
'primary': '#2196F3',
|
||||
'secondary': '#FFC107',
|
||||
'success': '#4CAF50',
|
||||
'danger': '#F44336',
|
||||
'warning': '#FF9800',
|
||||
'light': '#F5F5F5',
|
||||
'dark': '#333333'
|
||||
}
|
||||
|
||||
style.configure('Title.TLabel', font=('Arial', 16, 'bold'))
|
||||
style.configure('Heading.TLabel', font=('Arial', 12, 'bold'))
|
||||
style.configure('Info.TLabel', font=('Arial', 10))
|
||||
|
||||
def create_widgets(self):
|
||||
"""Create the main GUI interface"""
|
||||
|
||||
# Main container with padding
|
||||
main_frame = ttk.Frame(self.root, padding="20")
|
||||
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, 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)
|
||||
|
||||
# Title
|
||||
title_label = ttk.Label(main_frame, text="🛡️ LVM Backup Manager", style='Title.TLabel')
|
||||
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
|
||||
|
||||
# Source drive selection
|
||||
self.create_drive_selection_frame(main_frame, "Source Drive", 1, self.source_vg, True)
|
||||
|
||||
# Arrow
|
||||
arrow_label = ttk.Label(main_frame, text="⬇️", font=('Arial', 20))
|
||||
arrow_label.grid(row=2, column=1, pady=10)
|
||||
|
||||
# Target drive selection
|
||||
self.create_drive_selection_frame(main_frame, "Target Drive", 3, self.target_vg, False)
|
||||
|
||||
# Backup info frame
|
||||
self.create_backup_info_frame(main_frame, 4)
|
||||
|
||||
# Control buttons
|
||||
self.create_control_frame(main_frame, 5)
|
||||
|
||||
# Progress frame
|
||||
self.create_progress_frame(main_frame, 6)
|
||||
|
||||
# Log frame
|
||||
self.create_log_frame(main_frame, 7)
|
||||
|
||||
def create_drive_selection_frame(self, parent, title, row, var, is_source):
|
||||
"""Create drive selection section"""
|
||||
frame = ttk.LabelFrame(parent, text=title, padding="10")
|
||||
frame.grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Drive dropdown
|
||||
ttk.Label(frame, text="Volume Group:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10))
|
||||
|
||||
combo = ttk.Combobox(frame, textvariable=var, state='readonly', width=30)
|
||||
combo.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
|
||||
combo.bind('<<ComboboxSelected>>', lambda e: self.update_drive_info())
|
||||
|
||||
refresh_btn = ttk.Button(frame, text="🔄 Refresh", command=self.refresh_drives)
|
||||
refresh_btn.grid(row=0, column=2)
|
||||
|
||||
# Drive info labels
|
||||
info_frame = ttk.Frame(frame)
|
||||
info_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(10, 0))
|
||||
info_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Store references for updating
|
||||
setattr(self, f"{title.lower().replace(' ', '_')}_info", info_frame)
|
||||
|
||||
if is_source:
|
||||
self.source_combo = combo
|
||||
else:
|
||||
self.target_combo = combo
|
||||
|
||||
def create_backup_info_frame(self, parent, row):
|
||||
"""Create backup information display"""
|
||||
frame = ttk.LabelFrame(parent, text="📊 Backup Information", padding="10")
|
||||
frame.grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
|
||||
frame.columnconfigure(1, weight=1)
|
||||
|
||||
self.backup_info_labels = {}
|
||||
|
||||
info_items = [
|
||||
("Total Size:", "total_size"),
|
||||
("Estimated Time:", "est_time"),
|
||||
("Transfer Speed:", "speed"),
|
||||
("Status:", "status")
|
||||
]
|
||||
|
||||
for i, (label, key) in enumerate(info_items):
|
||||
ttk.Label(frame, text=label).grid(row=i//2, column=(i%2)*2, sticky=tk.W, padx=(0, 10), pady=2)
|
||||
value_label = ttk.Label(frame, text="Not calculated", style='Info.TLabel')
|
||||
value_label.grid(row=i//2, column=(i%2)*2+1, sticky=tk.W, padx=(0, 20), pady=2)
|
||||
self.backup_info_labels[key] = value_label
|
||||
|
||||
def create_control_frame(self, parent, row):
|
||||
"""Create control buttons"""
|
||||
frame = ttk.Frame(parent)
|
||||
frame.grid(row=row, column=0, columnspan=3, pady=20)
|
||||
|
||||
self.start_btn = ttk.Button(frame, text="▶️ Start Backup", command=self.start_backup, style='Accent.TButton')
|
||||
self.start_btn.pack(side=tk.LEFT, padx=(0, 10))
|
||||
|
||||
self.stop_btn = ttk.Button(frame, text="⏹️ Stop Backup", command=self.stop_backup, state='disabled')
|
||||
self.stop_btn.pack(side=tk.LEFT, padx=(0, 10))
|
||||
|
||||
self.verify_btn = ttk.Button(frame, text="✅ Verify Backup", command=self.verify_backup)
|
||||
self.verify_btn.pack(side=tk.LEFT)
|
||||
|
||||
def create_progress_frame(self, parent, row):
|
||||
"""Create progress monitoring"""
|
||||
frame = ttk.LabelFrame(parent, text="📈 Progress", padding="10")
|
||||
frame.grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
|
||||
# Overall progress
|
||||
ttk.Label(frame, text="Overall Progress:").grid(row=0, column=0, sticky=tk.W)
|
||||
self.overall_progress = ttk.Progressbar(frame, mode='determinate', length=400)
|
||||
self.overall_progress.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(5, 10))
|
||||
|
||||
# Current operation
|
||||
self.current_operation = ttk.Label(frame, text="Ready to start backup", style='Info.TLabel')
|
||||
self.current_operation.grid(row=2, column=0, sticky=tk.W)
|
||||
|
||||
# Time remaining
|
||||
self.time_remaining = ttk.Label(frame, text="", style='Info.TLabel')
|
||||
self.time_remaining.grid(row=3, column=0, sticky=tk.W)
|
||||
|
||||
def create_log_frame(self, parent, row):
|
||||
"""Create log output"""
|
||||
frame = ttk.LabelFrame(parent, text="📝 Log Output", padding="10")
|
||||
frame.grid(row=row, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
|
||||
frame.columnconfigure(0, weight=1)
|
||||
frame.rowconfigure(0, weight=1)
|
||||
parent.rowconfigure(row, weight=1)
|
||||
|
||||
self.log_text = scrolledtext.ScrolledText(frame, height=12, width=80, font=('Consolas', 9))
|
||||
self.log_text.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||
|
||||
# Add initial message
|
||||
self.log("LVM Backup Manager initialized")
|
||||
self.log("Select source and target drives to begin")
|
||||
|
||||
def log(self, message):
|
||||
"""Add message to log with timestamp"""
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
log_message = f"[{timestamp}] {message}\n"
|
||||
|
||||
self.log_text.insert(tk.END, log_message)
|
||||
self.log_text.see(tk.END)
|
||||
self.root.update_idletasks()
|
||||
|
||||
def refresh_drives(self):
|
||||
"""Scan for available LVM volume groups"""
|
||||
try:
|
||||
self.log("Scanning for LVM volume groups...")
|
||||
|
||||
# Get volume groups
|
||||
result = subprocess.run(['sudo', 'vgs', '--noheadings', '-o', 'vg_name,vg_size,vg_free'],
|
||||
capture_output=True, text=True, check=True)
|
||||
|
||||
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)")
|
||||
|
||||
# Update comboboxes
|
||||
self.source_combo['values'] = vgs
|
||||
self.target_combo['values'] = vgs
|
||||
|
||||
if vgs:
|
||||
self.log(f"Found {len(vgs)} volume groups")
|
||||
else:
|
||||
self.log("No LVM volume groups found")
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.log(f"Error scanning drives: {e}")
|
||||
messagebox.showerror("Error", "Failed to scan for LVM volume groups. Make sure you have LVM installed and proper permissions.")
|
||||
|
||||
def update_drive_info(self):
|
||||
"""Update drive information when selection changes"""
|
||||
if not self.source_vg.get() or not self.target_vg.get():
|
||||
return
|
||||
|
||||
try:
|
||||
source_vg = self.source_vg.get().split()[0]
|
||||
target_vg = self.target_vg.get().split()[0]
|
||||
|
||||
# Get detailed volume information
|
||||
source_info = self.get_vg_details(source_vg)
|
||||
target_info = self.get_vg_details(target_vg)
|
||||
|
||||
# Calculate backup information
|
||||
self.calculate_backup_info(source_info, target_info)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error updating drive info: {e}")
|
||||
|
||||
def get_vg_details(self, vg_name):
|
||||
"""Get detailed information about a volume group"""
|
||||
try:
|
||||
# Get VG info
|
||||
vg_result = subprocess.run(['sudo', 'vgs', vg_name, '--noheadings', '-o', 'vg_size,vg_free,vg_uuid'],
|
||||
capture_output=True, text=True, check=True)
|
||||
vg_parts = vg_result.stdout.strip().split()
|
||||
|
||||
# Get LV info
|
||||
lv_result = subprocess.run(['sudo', 'lvs', vg_name, '--noheadings', '-o', 'lv_name,lv_size'],
|
||||
capture_output=True, text=True, check=True)
|
||||
|
||||
volumes = []
|
||||
total_lv_size = 0
|
||||
for line in lv_result.stdout.strip().split('\n'):
|
||||
if line.strip():
|
||||
parts = line.strip().split()
|
||||
if len(parts) >= 2:
|
||||
lv_name = parts[0]
|
||||
lv_size = parts[1]
|
||||
volumes.append((lv_name, lv_size))
|
||||
# Convert size to bytes for calculation
|
||||
size_bytes = self.parse_size_to_bytes(lv_size)
|
||||
total_lv_size += size_bytes
|
||||
|
||||
return {
|
||||
'name': vg_name,
|
||||
'total_size': vg_parts[0],
|
||||
'free_size': vg_parts[1],
|
||||
'uuid': vg_parts[2],
|
||||
'volumes': volumes,
|
||||
'total_lv_size_bytes': total_lv_size
|
||||
}
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
def parse_size_to_bytes(self, size_str):
|
||||
"""Parse LVM size string to bytes"""
|
||||
size_str = size_str.strip()
|
||||
multipliers = {'B': 1, 'K': 1024, 'M': 1024**2, 'G': 1024**3, 'T': 1024**4}
|
||||
|
||||
# Extract number and unit
|
||||
if size_str[-1].upper() in multipliers:
|
||||
number = float(size_str[:-1])
|
||||
unit = size_str[-1].upper()
|
||||
else:
|
||||
number = float(size_str)
|
||||
unit = 'B'
|
||||
|
||||
return int(number * multipliers.get(unit, 1))
|
||||
|
||||
def calculate_backup_info(self, source_info, target_info):
|
||||
"""Calculate and display backup information"""
|
||||
if not source_info or not target_info:
|
||||
return
|
||||
|
||||
# Calculate total size to backup
|
||||
total_bytes = source_info['total_lv_size_bytes']
|
||||
total_gb = total_bytes / (1024**3)
|
||||
|
||||
# Estimate time (based on typical speeds: 200-400 MB/s)
|
||||
avg_speed_mbs = 250 # MB/s
|
||||
est_seconds = total_bytes / (avg_speed_mbs * 1024 * 1024)
|
||||
est_time = str(timedelta(seconds=int(est_seconds)))
|
||||
|
||||
# Update labels
|
||||
self.backup_info_labels['total_size'].config(text=f"{total_gb:.1f} GB")
|
||||
self.backup_info_labels['est_time'].config(text=est_time)
|
||||
self.backup_info_labels['speed'].config(text=f"~{avg_speed_mbs} MB/s")
|
||||
self.backup_info_labels['status'].config(text="Ready")
|
||||
|
||||
self.log(f"Backup calculation: {total_gb:.1f} GB, estimated {est_time}")
|
||||
|
||||
def start_backup(self):
|
||||
"""Start the backup process"""
|
||||
if not self.source_vg.get() or not self.target_vg.get():
|
||||
messagebox.showerror("Error", "Please select both source and target drives")
|
||||
return
|
||||
|
||||
source_vg = self.source_vg.get().split()[0]
|
||||
target_vg = self.target_vg.get().split()[0]
|
||||
|
||||
if source_vg == target_vg:
|
||||
messagebox.showerror("Error", "Source and target cannot be the same drive")
|
||||
return
|
||||
|
||||
# Confirm backup
|
||||
if not messagebox.askyesno("Confirm Backup",
|
||||
f"This will overwrite all data on {target_vg}.\n\nAre you sure you want to continue?"):
|
||||
return
|
||||
|
||||
# Update UI state
|
||||
self.backup_running = True
|
||||
self.start_btn.config(state='disabled')
|
||||
self.stop_btn.config(state='normal')
|
||||
self.overall_progress.config(value=0)
|
||||
self.backup_info_labels['status'].config(text="Running...")
|
||||
|
||||
# Start backup in 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):
|
||||
"""Run the actual backup process"""
|
||||
try:
|
||||
self.log(f"Starting backup: {source_vg} → {target_vg}")
|
||||
|
||||
# Modify the backup script to work with our selected VGs
|
||||
script_path = os.path.join(os.path.dirname(__file__), 'lvm_block_backup.sh')
|
||||
|
||||
# Create a temporary script with modified VG names
|
||||
temp_script = self.create_temp_script(script_path, source_vg, target_vg)
|
||||
|
||||
# Run the backup script
|
||||
self.backup_process = subprocess.Popen(
|
||||
['sudo', temp_script],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
# Monitor progress
|
||||
self.monitor_backup_progress()
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Backup failed: {e}")
|
||||
self.backup_finished(False)
|
||||
|
||||
def create_temp_script(self, original_script, source_vg, target_vg):
|
||||
"""Create a temporary script with modified VG names"""
|
||||
temp_script = '/tmp/lvm_backup_gui_temp.sh'
|
||||
|
||||
with open(original_script, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace VG names
|
||||
content = content.replace('SOURCE_VG="internal-vg"', f'SOURCE_VG="{source_vg}"')
|
||||
content = content.replace('TARGET_VG="migration-vg"', f'TARGET_VG="{target_vg}"')
|
||||
|
||||
# Make it auto-answer 'y' to confirmation
|
||||
content = content.replace('read -p "Are you sure you want to proceed?', 'echo "Auto-confirmed by GUI"; confirm="y"; #read -p "Are you sure you want to proceed?')
|
||||
|
||||
with open(temp_script, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
os.chmod(temp_script, 0o755)
|
||||
return temp_script
|
||||
|
||||
def monitor_backup_progress(self):
|
||||
"""Monitor backup progress and update UI"""
|
||||
if not self.backup_running or not self.backup_process:
|
||||
return
|
||||
|
||||
try:
|
||||
# Read output
|
||||
line = self.backup_process.stdout.readline()
|
||||
if line:
|
||||
line = line.strip()
|
||||
self.log(line)
|
||||
|
||||
# Parse progress from dd output
|
||||
if 'kopiert' in line or 'copied' in line:
|
||||
self.parse_dd_progress(line)
|
||||
elif 'SUCCESS' in line:
|
||||
if 'Root volume cloned' in line:
|
||||
self.overall_progress.config(value=33)
|
||||
self.current_operation.config(text="Root volume completed ✅")
|
||||
elif 'Home volume cloned' in line:
|
||||
self.overall_progress.config(value=90)
|
||||
self.current_operation.config(text="Home volume completed ✅")
|
||||
elif 'Boot volume cloned' in line:
|
||||
self.overall_progress.config(value=95)
|
||||
self.current_operation.config(text="Boot volume completed ✅")
|
||||
elif 'Cloning' in line:
|
||||
if 'root' in line.lower():
|
||||
self.current_operation.config(text="📁 Cloning root volume...")
|
||||
elif 'home' in line.lower():
|
||||
self.current_operation.config(text="🏠 Cloning home volume...")
|
||||
elif 'boot' in line.lower():
|
||||
self.current_operation.config(text="⚡ Cloning boot volume...")
|
||||
|
||||
# Check if process is still running
|
||||
if self.backup_process.poll() is None:
|
||||
# Schedule next check
|
||||
self.root.after(100, self.monitor_backup_progress)
|
||||
else:
|
||||
# Process finished
|
||||
success = self.backup_process.returncode == 0
|
||||
self.backup_finished(success)
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error monitoring progress: {e}")
|
||||
self.backup_finished(False)
|
||||
|
||||
def parse_dd_progress(self, line):
|
||||
"""Parse dd progress output"""
|
||||
try:
|
||||
# Look for speed information
|
||||
if 'MB/s' in line:
|
||||
speed_match = re.search(r'(\d+(?:\.\d+)?)\s*MB/s', line)
|
||||
if speed_match:
|
||||
speed = speed_match.group(1)
|
||||
self.backup_info_labels['speed'].config(text=f"{speed} MB/s")
|
||||
except:
|
||||
pass
|
||||
|
||||
def backup_finished(self, success):
|
||||
"""Handle backup completion"""
|
||||
self.backup_running = False
|
||||
self.start_btn.config(state='normal')
|
||||
self.stop_btn.config(state='disabled')
|
||||
|
||||
if success:
|
||||
self.overall_progress.config(value=100)
|
||||
self.current_operation.config(text="✅ Backup completed successfully!")
|
||||
self.backup_info_labels['status'].config(text="Completed")
|
||||
self.log("🎉 Backup completed successfully!")
|
||||
messagebox.showinfo("Success", "Backup completed successfully!")
|
||||
else:
|
||||
self.current_operation.config(text="❌ Backup failed")
|
||||
self.backup_info_labels['status'].config(text="Failed")
|
||||
self.log("❌ Backup failed")
|
||||
messagebox.showerror("Error", "Backup failed. Check the log for details.")
|
||||
|
||||
# Clean up
|
||||
if hasattr(self, 'backup_process'):
|
||||
self.backup_process = None
|
||||
|
||||
def stop_backup(self):
|
||||
"""Stop the running backup"""
|
||||
if self.backup_process:
|
||||
self.log("Stopping backup...")
|
||||
self.backup_process.terminate()
|
||||
self.backup_finished(False)
|
||||
|
||||
def verify_backup(self):
|
||||
"""Verify the backup integrity"""
|
||||
if not self.target_vg.get():
|
||||
messagebox.showerror("Error", "Please select a target drive to verify")
|
||||
return
|
||||
|
||||
target_vg = self.target_vg.get().split()[0]
|
||||
|
||||
self.log(f"Verifying backup on {target_vg}...")
|
||||
|
||||
def verify_thread():
|
||||
try:
|
||||
# Run filesystem checks
|
||||
result = subprocess.run(['sudo', 'fsck', '-n', f'/dev/{target_vg}/root'],
|
||||
capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
self.log("✅ Root filesystem verification passed")
|
||||
else:
|
||||
self.log("⚠️ Root filesystem verification issues detected")
|
||||
|
||||
result = subprocess.run(['sudo', 'fsck', '-n', f'/dev/{target_vg}/boot'],
|
||||
capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
self.log("✅ Boot filesystem verification passed")
|
||||
else:
|
||||
self.log("⚠️ Boot filesystem verification issues detected")
|
||||
|
||||
self.log("Verification completed")
|
||||
messagebox.showinfo("Verification", "Backup verification completed. Check log for details.")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Verification error: {e}")
|
||||
messagebox.showerror("Error", f"Verification failed: {e}")
|
||||
|
||||
thread = threading.Thread(target=verify_thread)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
# Check if running as root
|
||||
if os.geteuid() != 0:
|
||||
messagebox.showerror("Permission Error",
|
||||
"This application requires root privileges.\n\n" +
|
||||
"Please run with: sudo python3 lvm_backup_gui.py")
|
||||
return
|
||||
|
||||
root = tk.Tk()
|
||||
app = LVMBackupGUI(root)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=LVM Backup Manager
|
||||
Comment=Professional LVM volume backup with GUI
|
||||
Exec=pkexec lvm-backup-manager
|
||||
Icon=lvm-backup-manager
|
||||
Categories=System;Administration;
|
||||
Keywords=backup;lvm;snapshot;clone;system;recovery;
|
||||
StartupNotify=true
|
||||
StartupWMClass=LVM Backup Manager
|
||||
@@ -0,0 +1,58 @@
|
||||
# LVM Backup Manager
|
||||
|
||||
## Overview
|
||||
|
||||
LVM Backup Manager is a professional graphical interface for creating block-level backups of LVM (Logical Volume Manager) volumes. It provides an intuitive way to clone entire volume groups while maintaining data consistency through LVM snapshots.
|
||||
|
||||
## Features
|
||||
|
||||
* **Intuitive GUI**: Easy-to-use interface with drive selection and progress monitoring
|
||||
* **Real-time Progress**: Live progress bars and speed indicators during backup
|
||||
* **Safety Features**: Confirmation dialogs and verification tools
|
||||
* **Professional Logging**: Detailed logs with timestamps for troubleshooting
|
||||
* **Snapshot-based**: Uses LVM snapshots for consistent, live backups
|
||||
* **Block-level Cloning**: Creates exact, bootable copies of your volumes
|
||||
|
||||
## System Requirements
|
||||
|
||||
* Linux system with LVM2 installed
|
||||
* Python 3.x with Tkinter support
|
||||
* Root/sudo privileges for LVM operations
|
||||
* External storage device with LVM volume group
|
||||
|
||||
## Usage
|
||||
|
||||
### GUI Application
|
||||
```bash
|
||||
sudo lvm-backup-manager
|
||||
```
|
||||
|
||||
### Command Line (legacy)
|
||||
```bash
|
||||
sudo lvm-block-backup
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Detection**: Scans for available LVM volume groups
|
||||
2. **Selection**: Choose source and target volume groups
|
||||
3. **Calculation**: Estimates backup time and data size
|
||||
4. **Snapshots**: Creates consistent snapshots of source volumes
|
||||
5. **Cloning**: Performs block-level copy using dd command
|
||||
6. **Verification**: Checks filesystem integrity of backup
|
||||
7. **Cleanup**: Removes temporary snapshots
|
||||
|
||||
## Safety Notes
|
||||
|
||||
* **Always verify** your target drive selection
|
||||
* **Backup process will overwrite** all data on target volume group
|
||||
* **Test your backups** using the verification feature
|
||||
* **Keep multiple backup copies** for critical data
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions, check the log output in the GUI application or examine system logs.
|
||||
|
||||
## License
|
||||
|
||||
This software is provided as-is for backup and recovery purposes.
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="diskGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#FFC107;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#FF9800;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background circle -->
|
||||
<circle cx="32" cy="32" r="30" fill="url(#bgGradient)" stroke="#1565C0" stroke-width="2"/>
|
||||
|
||||
<!-- Hard drive icon -->
|
||||
<rect x="12" y="20" width="20" height="14" rx="2" fill="url(#diskGradient)" stroke="#E65100" stroke-width="1"/>
|
||||
<rect x="14" y="22" width="16" height="2" fill="#FFF3E0" opacity="0.8"/>
|
||||
<rect x="14" y="25" width="16" height="2" fill="#FFF3E0" opacity="0.6"/>
|
||||
<rect x="14" y="28" width="16" height="2" fill="#FFF3E0" opacity="0.4"/>
|
||||
<circle cx="29" cy="30" r="1.5" fill="#E65100"/>
|
||||
|
||||
<!-- Arrow pointing right -->
|
||||
<path d="M 35 27 L 42 27 L 38 23 M 42 27 L 38 31" stroke="#4CAF50" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
|
||||
<!-- Second hard drive -->
|
||||
<rect x="44" y="20" width="20" height="14" rx="2" fill="url(#diskGradient)" stroke="#E65100" stroke-width="1"/>
|
||||
<rect x="46" y="22" width="16" height="2" fill="#FFF3E0" opacity="0.8"/>
|
||||
<rect x="46" y="25" width="16" height="2" fill="#FFF3E0" opacity="0.6"/>
|
||||
<rect x="46" y="28" width="16" height="2" fill="#FFF3E0" opacity="0.4"/>
|
||||
<circle cx="61" cy="30" r="1.5" fill="#E65100"/>
|
||||
|
||||
<!-- Shield/protection symbol -->
|
||||
<path d="M 32 40 L 28 44 L 32 48 L 36 44 Z" fill="#4CAF50" stroke="#2E7D32" stroke-width="1"/>
|
||||
<path d="M 30 44 L 32 46 L 36 42" stroke="white" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||||
|
||||
<!-- LVM text -->
|
||||
<text x="32" y="58" text-anchor="middle" font-family="Arial" font-size="8" font-weight="bold" fill="white">LVM</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
Reference in New Issue
Block a user