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