From 72f9838f551b23afe41a5eff999134a23ac79884 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 9 Oct 2025 00:30:03 +0200 Subject: [PATCH] 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) --- README.md | 456 +---- boot_repair_tools.sh | 389 ---- direct_clone_backup.sh | 667 ------- improved_lvm_migration.sh | 723 ------- old_scripts/BOOT_FIX_SUMMARY.md | 154 ++ .../DRIVE_SELECTION_REFERENCE.md | 0 old_scripts/INTERNAL_DRIVE_RECOVERY.md | 234 +++ .../LIVE_USB_MIGRATION_GUIDE.md | 0 .../LVM_MIGRATION_GUIDE.txt | 0 .../README_BACKUP.md | 0 old_scripts/README_FIRST.txt | 105 + .../README_FIRST.txt.old | 0 old_scripts/README_OLD.md | 391 ++++ .../SIMPLE_CLONE_SOLUTION.md | 0 .../START_SIMPLE_CLONE.txt | 0 old_scripts/boot_repair_tools.sh | 0 old_scripts/build-deb.sh | 39 + old_scripts/clear_bios_boot_flag.sh | 107 + old_scripts/deb-package/DEBIAN/control | 19 + old_scripts/deb-package/DEBIAN/postinst | 21 + .../deb-package/usr/bin/lvm-backup-manager | 23 + .../deb-package/usr/bin/lvm-block-backup | 0 .../deb-package/usr/bin/lvm_backup_gui.py | 541 ++++++ .../applications/lvm-backup-manager.desktop | 11 + .../usr/share/doc/lvm-backup-manager/README | 58 + .../usr/share/pixmaps/lvm-backup-manager.svg | 40 + old_scripts/direct_clone_backup.sh | 0 old_scripts/improved_lvm_migration.sh | 245 ++- old_scripts/install_borg.sh | 36 + old_scripts/lvm_backup_gui.py | 1722 +++++++++++++++++ old_scripts/lvm_backup_gui_fixed.py | 459 +++++ old_scripts/lvm_block_backup.sh | 193 ++ old_scripts/verify_boot_readiness.sh | 0 old_scripts/verify_internal_boot.sh | 57 + old_scripts/verify_internal_config.sh | 147 ++ verify_boot_readiness.sh | 481 ----- 36 files changed, 4657 insertions(+), 2661 deletions(-) delete mode 100755 boot_repair_tools.sh delete mode 100755 direct_clone_backup.sh delete mode 100755 improved_lvm_migration.sh create mode 100644 old_scripts/BOOT_FIX_SUMMARY.md rename DRIVE_SELECTION_REFERENCE.md => old_scripts/DRIVE_SELECTION_REFERENCE.md (100%) create mode 100644 old_scripts/INTERNAL_DRIVE_RECOVERY.md rename LIVE_USB_MIGRATION_GUIDE.md => old_scripts/LIVE_USB_MIGRATION_GUIDE.md (100%) rename LVM_MIGRATION_GUIDE.txt => old_scripts/LVM_MIGRATION_GUIDE.txt (100%) rename README_BACKUP.md => old_scripts/README_BACKUP.md (100%) create mode 100644 old_scripts/README_FIRST.txt rename README_FIRST.txt => old_scripts/README_FIRST.txt.old (100%) create mode 100644 old_scripts/README_OLD.md rename SIMPLE_CLONE_SOLUTION.md => old_scripts/SIMPLE_CLONE_SOLUTION.md (100%) rename START_SIMPLE_CLONE.txt => old_scripts/START_SIMPLE_CLONE.txt (100%) mode change 100755 => 100644 old_scripts/boot_repair_tools.sh create mode 100644 old_scripts/build-deb.sh create mode 100755 old_scripts/clear_bios_boot_flag.sh create mode 100644 old_scripts/deb-package/DEBIAN/control create mode 100644 old_scripts/deb-package/DEBIAN/postinst create mode 100644 old_scripts/deb-package/usr/bin/lvm-backup-manager rename lvm_block_backup.sh => old_scripts/deb-package/usr/bin/lvm-block-backup (100%) mode change 100755 => 100644 create mode 100644 old_scripts/deb-package/usr/bin/lvm_backup_gui.py create mode 100644 old_scripts/deb-package/usr/share/applications/lvm-backup-manager.desktop create mode 100644 old_scripts/deb-package/usr/share/doc/lvm-backup-manager/README create mode 100644 old_scripts/deb-package/usr/share/pixmaps/lvm-backup-manager.svg mode change 100755 => 100644 old_scripts/direct_clone_backup.sh mode change 100755 => 100644 old_scripts/improved_lvm_migration.sh create mode 100644 old_scripts/install_borg.sh create mode 100644 old_scripts/lvm_backup_gui.py create mode 100644 old_scripts/lvm_backup_gui_fixed.py create mode 100755 old_scripts/lvm_block_backup.sh mode change 100755 => 100644 old_scripts/verify_boot_readiness.sh create mode 100755 old_scripts/verify_internal_boot.sh create mode 100755 old_scripts/verify_internal_config.sh delete mode 100755 verify_boot_readiness.sh diff --git a/README.md b/README.md index ff8b666..3380a1f 100644 --- a/README.md +++ b/README.md @@ -1,391 +1,119 @@ -# System Backup to External M.2 SSD +# Enhanced Simple LVM Backup System -A comprehensive backup solution for Linux systems that provides both GUI and command-line interfaces for cloning your internal drive to an external M.2 SSD. +A reliable, straightforward backup system for LVM-enabled Linux systems. Born from the need for simple, dependable backups without complex logic that can cause system issues. + +## Philosophy + +**Simple is better.** This system does exactly three things: +1. Create LVM snapshot +2. Copy data with dd/pv +3. Clean up snapshot + +No complex migration logic, no fancy features that can break. Just reliable backups. ## Features -## Features +### Three Backup Modes -- **GUI Interface**: User-friendly graphical interface built with Python Tkinter -- **Command Line Interface**: Full CLI support for automated and scripted operations -- **Smart Drive Detection**: Automatically detects internal drive and external M.2 SSDs -- **Full System Backup**: Complete drive cloning with dd for exact system replication -- **Smart Sync Backup**: ⚡ NEW! Fast incremental backups using rsync for minor changes -- **Change Analysis**: Analyze filesystem changes to recommend sync vs full backup -- **Restore Functionality**: Complete system restore from external drive -- **Portable Tools**: Backup tools survive on external drive and remain accessible after cloning -- **Reboot Integration**: Optional reboot before backup/restore operations -- **Progress Monitoring**: Real-time progress display and logging -- **Safety Features**: Multiple confirmations and drive validation -- **Desktop Integration**: Create desktop shortcuts for easy access +- **LV → LV**: Update existing logical volume backups +- **LV → Raw**: Create fresh backups on raw devices +- **VG → Raw**: Clone entire volume groups with LVM metadata + +### Two Interfaces + +- **GUI**: `simple_backup_gui.py` - User-friendly interface +- **CLI**: `enhanced_simple_backup.sh` - Command-line power user interface + +## Quick Start + +### 1. See Available Options +```bash +sudo ./list_drives.sh +``` + +### 2. Choose Your Backup Mode + +**Update existing backup:** +```bash +sudo ./enhanced_simple_backup.sh lv-to-lv /dev/internal-vg/root /dev/backup-vg/root +``` + +**Fresh backup to external drive:** +```bash +sudo ./enhanced_simple_backup.sh lv-to-raw /dev/internal-vg/root /dev/sdb +``` + +**Clone entire system:** +```bash +sudo ./enhanced_simple_backup.sh vg-to-raw internal-vg /dev/sdb +``` + +**Use the GUI:** +```bash +sudo python3 simple_backup_gui.py +``` + +## Files + +### Core System +- `simple_backup_gui.py` - GUI interface with mode selection +- `enhanced_simple_backup.sh` - CLI with three backup modes +- `list_drives.sh` - Helper to show available sources/targets +- `SIMPLE_BACKUP_README.md` - Detailed documentation + +### Legacy Files +- `old_scripts/` - Previous complex implementations (archived) +- Various `.md` files - Documentation from previous iterations ## Requirements -- Linux system (tested on Ubuntu/Debian) -- Python 3.6+ with tkinter -- External M.2 SSD in USB enclosure -- sudo privileges for drive operations - -## Installation - -1. Clone or download this repository: - ```bash - git clone - cd backup_to_external_m.2 - ``` - -2. Make scripts executable: - ```bash - chmod +x backup_script.sh - chmod +x backup_manager.py - ``` - -3. Install dependencies (if needed): - ```bash - sudo apt update - sudo apt install python3-tk pv parted - ``` - -4. **Optional**: Set up portable tools on external M.2 SSD: - ```bash - ./setup_portable_tools.sh - ``` - This creates a separate partition on your external drive that preserves backup tools even after cloning. - -## Usage - -### GUI Application - -Launch the graphical backup manager: - -```bash -python3 backup_manager.py -``` - -**Features:** -- Automatic drive detection and classification -- Source and target drive selection with smart defaults -- Real-time progress monitoring -- **Backup Modes**: - - **Smart Sync Backup**: ⚡ Fast incremental backup using rsync (requires existing backup) - - **Analyze Changes**: Analyze what has changed since last backup - - **Start Backup**: Full drive clone (immediate backup while system running) - - **Reboot & Backup**: Reboot system then full backup (recommended for first backup) -- **Restore Modes**: - - **Restore from External**: Immediate restore from external to internal - - **Reboot & Restore**: Reboot system then restore (recommended) -- **Drive Swap Button**: Easily swap source and target for restore operations - -### Command Line Script - -For command-line usage: - -```bash -# List available drives -./backup_script.sh --list - -# Analyze changes without performing backup -./backup_script.sh --analyze --target /dev/sdb - -# Smart sync backup (fast incremental update) -sudo ./backup_script.sh --sync --target /dev/sdb - -# Perform full backup with specific drives -sudo ./backup_script.sh --source /dev/nvme0n1 --target /dev/sda - -# Restore from external to internal (note the restore flag) -sudo ./backup_script.sh --restore --source /dev/sda --target /dev/nvme0n1 - -# Or more simply in restore mode (source/target are swapped automatically) -sudo ./backup_script.sh --restore --source /dev/nvme0n1 --target /dev/sda - -# Create desktop entry -./backup_script.sh --desktop - -# Launch GUI from script -./backup_script.sh --gui -``` - -### Desktop Integration - -Create a desktop shortcut: - -```bash -./backup_script.sh --desktop -``` - -This creates a clickable icon on your desktop that launches the backup tool. - -## Restore Operations - -### When to Restore -- **System Failure**: Internal drive crashed or corrupted -- **System Migration**: Moving to new hardware -- **Rollback**: Reverting to previous system state -- **Testing**: Restoring test environment - -### How to Restore - -#### GUI Method -1. **Connect External Drive**: Plug in your M.2 SSD with backup -2. **Launch GUI**: `python3 backup_manager.py` -3. **Check Drive Selection**: - - Source should be external drive (your backup) - - Target should be internal drive (will be overwritten) -4. **Use Swap Button**: If needed, click "Swap Source↔Target" -5. **Choose Restore Mode**: - - **"Restore from External"**: Immediate restore - - **"Reboot & Restore"**: Reboot then restore (recommended) - -#### Command Line Method -```bash -# Restore with automatic drive detection -sudo ./backup_script.sh --restore - -# Restore with specific drives -sudo ./backup_script.sh --restore --source /dev/sda --target /dev/nvme0n1 -``` - -### ⚠️ Restore Safety Warnings -- **Data Loss**: Target drive is completely overwritten -- **Irreversible**: No undo after restore starts -- **Multiple Confirmations**: System requires explicit confirmation -- **Drive Verification**: Double-check source and target drives -- **Boot Issues**: Ensure external drive contains valid system backup - -## Portable Tools (Boot from External M.2) - -### Setup Portable Tools -Run this once to set up backup tools on your external M.2 SSD: - -```bash -./setup_portable_tools.sh -``` - -This will: -- Create a 512MB tools partition on your external drive -- Install backup tools that survive cloning operations -- Set up automatic tool restoration after each backup - -### Using Portable Tools - -#### When Booted from External M.2 SSD: - -1. **Access Tools**: - ```bash - # Easy access helper - ./access_tools.sh - - # Or manually: - sudo mount LABEL=BACKUP_TOOLS /mnt/tools - cd /mnt/tools/backup_system - ./launch_backup_tools.sh - ``` - -2. **Restore Internal Drive**: - - Launch backup tools from external drive - - External drive (source) → Internal drive (target) - - Click "Reboot & Restore" for safest operation - -3. **Create Desktop Entry**: - ```bash - cd /mnt/tools/backup_system - ./create_desktop_entry.sh - ``` - -### Disaster Recovery Workflow - -1. **Normal Operation**: Internal drive fails -2. **Boot from External**: Use M.2 SSD as boot drive -3. **Access Tools**: Run `./access_tools.sh` -4. **Restore System**: Use backup tools to restore to new internal drive -5. **Back to Normal**: Boot from restored internal drive +- LVM-enabled Linux system +- Root access (`sudo`) +- Python 3 + tkinter (for GUI) +- `pv` command (optional, for progress display) ## Safety Features -- **Drive Validation**: Prevents accidental overwriting of wrong drives -- **Size Checking**: Ensures target drive is large enough -- **Confirmation Prompts**: Multiple confirmation steps before destructive operations -- **Mount Detection**: Automatically unmounts target drives before backup -- **Progress Monitoring**: Real-time feedback during backup operations +- Clear confirmations before destructive operations +- Automatic snapshot cleanup on errors +- Emergency stop functionality (GUI) +- Input validation and error handling -## File Structure +## What This System Does NOT Do -``` -backup_to_external_m.2/ -├── backup_manager.py # GUI application -├── backup_script.sh # Command-line script -├── install.sh # Installation script -├── systemd/ # Systemd service files -│ └── backup-service.service -└── README.md # This file -``` +- Complex migration between different LVM setups +- Automatic boot repair or GRUB handling +- Multiple backup formats or compression +- Network backups or cloud integration +- File-level incremental backups -## Smart Sync Technology ⚡ +For those features, use dedicated tools like Borg Backup, CloneZilla, or rsync. -The backup system now includes advanced **Smart Sync** functionality that dramatically reduces backup time for incremental updates: +## Why This Approach? -### How Smart Sync Works +Previous versions of this project included complex migration logic that occasionally caused system issues. This version returns to basics: -1. **Analysis Phase**: Compares source and target filesystems to determine changes -2. **Decision Engine**: Recommends sync vs full clone based on amount of changes: - - **< 2GB changes**: Smart sync recommended (much faster) - - **2-10GB changes**: Smart sync beneficial - - **> 10GB changes**: Full clone may be more appropriate -3. **Sync Operation**: Uses rsync to transfer only changed files and metadata +✅ Uses standard LVM commands only +✅ Minimal chance of errors +✅ Easy to understand and debug +✅ Predictable behavior +✅ No hidden "smart" features -### Smart Sync Benefits +## Recovery -- **Speed**: 10-100x faster than full clone for minor changes -- **No Downtime**: System remains usable during sync operation -- **Efficiency**: Only transfers changed data, preserving bandwidth and storage wear -- **Safety**: Preserves backup tools and maintains full system consistency +### From LV Backup +Mount the backup LV and copy files, or use it to restore your system. -### When to Use Smart Sync vs Full Clone +### From Raw Device Backup +Boot directly from the device, or restore using dd in reverse. -**Use Smart Sync when:** -- You have an existing backup on the target drive -- Regular incremental updates (daily/weekly backups) -- Minimal system changes since last backup -- You want faster backup with minimal downtime - -**Use Full Clone when:** -- First-time backup to a new drive -- Major system changes (OS upgrade, large software installations) -- Corrupted or incomplete previous backup -- Maximum compatibility and reliability needed - -### Smart Sync Usage - -**GUI Method:** -1. Click "Analyze Changes" to see what has changed -2. Review the recommendation and estimated time savings -3. Click "Smart Sync Backup" to perform incremental update - -**Command Line:** -```bash -# Analyze changes first -./backup_script.sh --analyze --target /dev/sdb - -# Perform smart sync -sudo ./backup_script.sh --sync --target /dev/sdb -``` - -## Traditional Full Backup - -For comprehensive system backup, the system uses proven `dd` cloning technology: - -### Backup Process -1. **Drive Detection**: Automatically scans for available drives -2. **Auto-Selection**: Internal drive as source, external as target -3. **Operation Selection**: Choose backup or restore mode -4. **Validation**: Confirms drives exist and are different -5. **Execution**: Uses `dd` command to clone entire drive -6. **Progress**: Shows real-time progress and logging - -### Backup vs Restore -- **Backup**: Internal → External (preserves your system on external drive) -- **Restore**: External → Internal (overwrites internal with backup data) -- **Swap Button**: Easily switch source/target for restore operations - -### Reboot vs Immediate Operations -- **Immediate**: Faster start, but system is running (potential file locks) -- **Reboot Mode**: System restarts first, then operates (cleaner, more reliable) -- **Recommendation**: Use reboot mode for critical operations - -### Reboot & Backup Mode -1. **Script Creation**: Creates a backup script for post-reboot execution -2. **Reboot Scheduling**: Schedules system reboot -3. **Auto-Execution**: Backup starts automatically after reboot -4. **Notification**: Shows completion status - -### Command Line Mode -- Direct `dd` cloning with progress monitoring -- Comprehensive logging to `/var/log/system_backup.log` -- Drive size validation and safety checks - -## Technical Details - -### Backup Method -- Uses `dd` command for bit-perfect drive cloning -- Block size optimized at 4MB for performance -- `fdatasync` ensures all data is written to disk - -### Drive Detection -- Uses `lsblk` to enumerate block devices -- Filters for actual disk drives -- Shows drive sizes for easy identification - -### Safety Mechanisms -- Root privilege verification -- Block device validation -- Source/target drive comparison -- Mount status checking -- Size compatibility verification - -## Troubleshooting - -### Common Issues - -1. **Permission Denied** - ```bash - # Run with sudo for drive operations - sudo python3 backup_manager.py - ``` - -2. **Drive Not Detected** - ```bash - # Check if drive is connected and recognized - lsblk - dmesg | tail - ``` - -3. **Backup Fails** - ```bash - # Check system logs - sudo journalctl -f - tail -f /var/log/system_backup.log - ``` - -### Performance Tips - -- Use USB 3.0+ connection for external M.2 SSD -- Ensure sufficient power supply for external enclosure -- Close unnecessary applications during backup -- Use SSD for better performance than traditional HDD - -## Security Considerations - -- **Data Destruction**: Target drive data is completely overwritten -- **Root Access**: Scripts require elevated privileges -- **Verification**: Always verify backup integrity after completion -- **Testing**: Test restore process with non-critical data first - -## Limitations - -- Only works with block devices (entire drives) -- Cannot backup to smaller drives -- Requires manual drive selection for safety -- No incremental backup support (full clone only) +### From VG Clone +Import the volume group or boot directly from the cloned device. ## Contributing -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Test thoroughly -5. Submit a pull request +Keep it simple. Any additions should follow the core principle: minimal logic, maximum reliability. ## License -This project is open source. Use at your own risk. - -## Disclaimer - -**WARNING**: This tool performs low-level disk operations that can result in data loss. Always: -- Verify drive selections carefully -- Test with non-critical data first -- Maintain separate backups of important data -- Understand the risks involved - -The authors are not responsible for any data loss or system damage. +Open source - use and modify as needed. \ No newline at end of file diff --git a/boot_repair_tools.sh b/boot_repair_tools.sh deleted file mode 100755 index 1e7b9f6..0000000 --- a/boot_repair_tools.sh +++ /dev/null @@ -1,389 +0,0 @@ -#!/bin/bash - -# Boot Repair and Emergency Recovery Tools -# Provides tools to fix boot issues after cloning - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -TARGET_DRIVE="" -WORK_DIR="/mnt/repair_work" - -log() { - echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" -} - -error() { - echo -e "${RED}[ERROR]${NC} $1" >&2 - exit 1 -} - -warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -detect_target_drive() { - log "Detecting target drive to repair..." - - # List available drives - echo "Available drives:" - lsblk -dpno NAME,SIZE,MODEL | grep -v "loop\|ram" - echo - - read -p "Enter the drive to repair (e.g., /dev/sdb): " TARGET_DRIVE - - if [ ! -b "$TARGET_DRIVE" ]; then - error "Drive $TARGET_DRIVE not found" - fi - - echo "Selected drive for repair: $TARGET_DRIVE" - lsblk "$TARGET_DRIVE" - echo - - read -p "Is this correct? [y/N]: " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - error "Operation cancelled" - fi - - success "Target drive selected: $TARGET_DRIVE" -} - -mount_target_system() { - log "Mounting target system for repair..." - - mkdir -p "$WORK_DIR" - - # Find partitions - local partitions=($(lsblk -pno NAME "$TARGET_DRIVE" | grep -v "^$TARGET_DRIVE$")) - local root_partition="" - local boot_partition="" - local efi_partition="" - - for part in "${partitions[@]}"; do - local fstype=$(lsblk -no FSTYPE "$part") - local size_bytes=$(lsblk -bno SIZE "$part") - - # Detect partitions by filesystem and size - if [[ "$fstype" == "vfat" && "$size_bytes" -lt 1073741824 ]]; then # Less than 1GB - efi_partition="$part" - elif [[ "$fstype" == "ext4" && "$size_bytes" -lt 5368709120 ]]; then # Between 100MB and 5GB - boot_partition="$part" - elif [[ "$fstype" == "ext4" && "$size_bytes" -gt 5368709120 ]]; then # Larger than 5GB - root_partition="$part" - elif [[ "$fstype" == "crypto_LUKS" ]]; then - # Try to unlock encrypted partition - local crypt_name="repair_$(basename "$part")" - echo "Found encrypted partition: $part" - echo "Please enter the password to unlock for repair:" - if cryptsetup open "$part" "$crypt_name"; then - local decrypted_fs=$(lsblk -no FSTYPE "/dev/mapper/$crypt_name") - if [[ "$decrypted_fs" == "ext4" ]]; then - root_partition="/dev/mapper/$crypt_name" - log "Using decrypted partition as root: $root_partition" - fi - else - warning "Could not unlock encrypted partition" - fi - fi - done - - if [ -z "$root_partition" ]; then - error "Could not find root partition to repair" - fi - - # Mount filesystems - log "Mounting root partition: $root_partition" - mount "$root_partition" "$WORK_DIR" || error "Failed to mount root partition" - - if [ -n "$boot_partition" ]; then - log "Mounting boot partition: $boot_partition" - mkdir -p "$WORK_DIR/boot" - mount "$boot_partition" "$WORK_DIR/boot" || warning "Failed to mount boot partition" - fi - - if [ -n "$efi_partition" ]; then - log "Mounting EFI partition: $efi_partition" - mkdir -p "$WORK_DIR/boot/efi" - mount "$efi_partition" "$WORK_DIR/boot/efi" || warning "Failed to mount EFI partition" - fi - - success "Target system mounted at $WORK_DIR" -} - -repair_grub() { - log "Repairing GRUB bootloader..." - - # Bind mount necessary filesystems - mount --bind /dev "$WORK_DIR/dev" - mount --bind /proc "$WORK_DIR/proc" - mount --bind /sys "$WORK_DIR/sys" - mount --bind /run "$WORK_DIR/run" - - # Repair GRUB - chroot "$WORK_DIR" /bin/bash -c " - echo 'Updating initramfs...' - update-initramfs -u -k all - - echo 'Reinstalling GRUB...' - grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=debian --recheck $TARGET_DRIVE - - echo 'Updating GRUB configuration...' - update-grub - - echo 'GRUB repair completed' - " - - # Unmount bind mounts - umount "$WORK_DIR/dev" 2>/dev/null || true - umount "$WORK_DIR/proc" 2>/dev/null || true - umount "$WORK_DIR/sys" 2>/dev/null || true - umount "$WORK_DIR/run" 2>/dev/null || true - - success "GRUB repair completed" -} - -fix_fstab() { - log "Checking and fixing /etc/fstab..." - - if [ ! -f "$WORK_DIR/etc/fstab" ]; then - warning "/etc/fstab not found" - return 0 - fi - - # Backup current fstab - cp "$WORK_DIR/etc/fstab" "$WORK_DIR/etc/fstab.backup.$(date +%Y%m%d_%H%M%S)" - - # Get current UUIDs of all partitions - local partitions=($(lsblk -pno NAME "$TARGET_DRIVE" | grep -v "^$TARGET_DRIVE$")) - - echo "Current partition UUIDs:" - for part in "${partitions[@]}"; do - local uuid=$(lsblk -no UUID "$part") - local fstype=$(lsblk -no FSTYPE "$part") - if [ -n "$uuid" ]; then - echo " $part ($fstype): $uuid" - fi - done - - echo - echo "Current /etc/fstab content:" - cat "$WORK_DIR/etc/fstab" - echo - - read -p "Do you want to regenerate /etc/fstab with current UUIDs? [y/N]: " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - # Generate new fstab - echo "# /etc/fstab: static file system information." > "$WORK_DIR/etc/fstab.new" - echo "# Regenerated by repair script on $(date)" >> "$WORK_DIR/etc/fstab.new" - echo "#" >> "$WORK_DIR/etc/fstab.new" - echo "# " >> "$WORK_DIR/etc/fstab.new" - - for part in "${partitions[@]}"; do - local uuid=$(lsblk -no UUID "$part") - local fstype=$(lsblk -no FSTYPE "$part") - - if [ -n "$uuid" ]; then - case "$fstype" in - "vfat") - echo "UUID=$uuid /boot/efi vfat defaults 0 2" >> "$WORK_DIR/etc/fstab.new" - ;; - "ext4") - # Determine if it's boot or root by size - local size_bytes=$(lsblk -bno SIZE "$part") - if [ "$size_bytes" -lt 5368709120 ]; then # Less than 5GB = boot - echo "UUID=$uuid /boot ext4 defaults 0 2" >> "$WORK_DIR/etc/fstab.new" - else # Root partition - echo "UUID=$uuid / ext4 defaults 0 1" >> "$WORK_DIR/etc/fstab.new" - fi - ;; - "swap") - echo "UUID=$uuid none swap sw 0 0" >> "$WORK_DIR/etc/fstab.new" - ;; - esac - fi - done - - # Add tmpfs - echo "tmpfs /tmp tmpfs defaults,noatime,mode=1777 0 0" >> "$WORK_DIR/etc/fstab.new" - - # Show new fstab - echo - echo "New /etc/fstab content:" - cat "$WORK_DIR/etc/fstab.new" - echo - - read -p "Apply this new /etc/fstab? [y/N]: " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - mv "$WORK_DIR/etc/fstab.new" "$WORK_DIR/etc/fstab" - success "/etc/fstab updated" - else - rm "$WORK_DIR/etc/fstab.new" - log "New fstab not applied" - fi - fi - - success "fstab check completed" -} - -check_boot_configuration() { - log "Checking boot configuration..." - - # Check if EFI boot entry exists - if command -v efibootmgr >/dev/null 2>&1; then - echo "Current EFI boot entries:" - efibootmgr - echo - fi - - # Check GRUB configuration - if [ -f "$WORK_DIR/etc/default/grub" ]; then - echo "GRUB configuration (/etc/default/grub):" - cat "$WORK_DIR/etc/default/grub" - echo - fi - - # Check if initramfs exists - echo "Available kernels and initramfs:" - ls -la "$WORK_DIR/boot/vmlinuz-"* 2>/dev/null || echo "No kernels found" - ls -la "$WORK_DIR/boot/initrd.img-"* 2>/dev/null || echo "No initramfs found" - echo - - success "Boot configuration check completed" -} - -regenerate_initramfs() { - log "Regenerating initramfs..." - - # Bind mount necessary filesystems - mount --bind /dev "$WORK_DIR/dev" - mount --bind /proc "$WORK_DIR/proc" - mount --bind /sys "$WORK_DIR/sys" - mount --bind /run "$WORK_DIR/run" - - chroot "$WORK_DIR" /bin/bash -c " - echo 'Regenerating initramfs for all kernels...' - update-initramfs -u -k all - echo 'Initramfs regeneration completed' - " - - # Unmount bind mounts - umount "$WORK_DIR/dev" 2>/dev/null || true - umount "$WORK_DIR/proc" 2>/dev/null || true - umount "$WORK_DIR/sys" 2>/dev/null || true - umount "$WORK_DIR/run" 2>/dev/null || true - - success "Initramfs regenerated" -} - -cleanup_repair() { - log "Cleaning up repair environment..." - - # Unmount all filesystems - umount "$WORK_DIR/boot/efi" 2>/dev/null || true - umount "$WORK_DIR/boot" 2>/dev/null || true - umount "$WORK_DIR" 2>/dev/null || true - - # Close encrypted volumes - for mapper in /dev/mapper/repair_*; do - if [ -b "$mapper" ]; then - local crypt_name=$(basename "$mapper") - cryptsetup close "$crypt_name" 2>/dev/null || true - fi - done - - # Remove work directory - rmdir "$WORK_DIR" 2>/dev/null || true - - success "Cleanup completed" -} - -main() { - echo -e "${GREEN}=== Boot Repair and Emergency Recovery Tools ===${NC}" - echo "This script helps fix boot issues after cloning" - echo - - echo "Available repair options:" - echo "1. Full repair (mount system, fix fstab, repair GRUB, regenerate initramfs)" - echo "2. GRUB repair only" - echo "3. fstab fix only" - echo "4. Initramfs regeneration only" - echo "5. Check boot configuration" - echo - - read -p "Select repair option [1-5]: " -n 1 -r - echo - - case $REPLY in - 1) - log "Performing full repair..." - detect_target_drive - mount_target_system - fix_fstab - regenerate_initramfs - repair_grub - check_boot_configuration - cleanup_repair - success "Full repair completed!" - ;; - 2) - log "Performing GRUB repair only..." - detect_target_drive - mount_target_system - repair_grub - cleanup_repair - success "GRUB repair completed!" - ;; - 3) - log "Fixing fstab only..." - detect_target_drive - mount_target_system - fix_fstab - cleanup_repair - success "fstab fix completed!" - ;; - 4) - log "Regenerating initramfs only..." - detect_target_drive - mount_target_system - regenerate_initramfs - cleanup_repair - success "Initramfs regeneration completed!" - ;; - 5) - log "Checking boot configuration..." - detect_target_drive - mount_target_system - check_boot_configuration - cleanup_repair - success "Boot configuration check completed!" - ;; - *) - error "Invalid option selected" - ;; - esac - - echo - echo -e "${BLUE}Repair completed. Next steps:${NC}" - echo "1. Reboot your system" - echo "2. Set the repaired drive as first boot device in BIOS/UEFI" - echo "3. Try booting from the external drive" - echo "4. If issues persist, run this script again or check system logs" -} - -# Trap to ensure cleanup on exit -trap cleanup_repair EXIT - -main "$@" \ No newline at end of file diff --git a/direct_clone_backup.sh b/direct_clone_backup.sh deleted file mode 100755 index 1365f3b..0000000 --- a/direct_clone_backup.sh +++ /dev/null @@ -1,667 +0,0 @@ -#!/bin/bash - -# Direct 1-to-1 Clone Script -# Creates an exact copy of internal drive to external drive without LVM conversion -# Preserves all partitions, LUKS encryption, and boot structures exactly as they are -# MUST BE RUN FROM A LIVE USB SYSTEM - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Configuration variables -INTERNAL_DRIVE="" -EXTERNAL_DRIVE="" -WORK_DIR="/mnt/clone_work" - -# Partition mapping -declare -A PARTITION_MAP -declare -A PARTITION_FILESYSTEMS -declare -A PARTITION_LABELS -declare -A PARTITION_UUIDS - -log() { - echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" -} - -error() { - echo -e "${RED}[ERROR]${NC} $1" >&2 - exit 1 -} - -warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -confirm_action() { - echo -e "${YELLOW}$1${NC}" - read -p "Do you want to continue? [y/N] " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - error "Operation aborted by user" - fi -} - -detect_drives() { - log "Detecting available drives..." - - # Find all block devices that are disks (not partitions), excluding the live USB - local all_drives=($(lsblk -dpno NAME,TYPE,SIZE,MODEL | grep "disk" | awk '{print $1}')) - local drives=() - - # Filter out the USB stick we're running from (if running from live system) - for drive in "${all_drives[@]}"; do - # Check if this drive contains the live system - if mount | grep -q "$drive" && mount | grep -q "/lib/live\|/media.*live\|overlay"; then - log "Excluding live USB drive: $drive" - continue - fi - drives+=("$drive") - done - - if [ ${#drives[@]} -lt 2 ]; then - error "Need at least 2 drives for cloning. Found only ${#drives[@]} suitable drives" - echo "Available drives:" - for drive in "${all_drives[@]}"; do - local info=$(lsblk -dpno NAME,SIZE,MODEL "$drive" | awk '{print $2 " " $3}') - echo " $drive - $info" - done - exit 1 - fi - - echo - echo "Available drives for cloning:" - for i in "${!drives[@]}"; do - local drive="${drives[$i]}" - local info=$(lsblk -dpno NAME,SIZE,MODEL "$drive" | awk '{print $2 " " $3}' | xargs) - local is_usb="" - - # Check if it's a USB drive - if udevadm info --query=property --name="$drive" 2>/dev/null | grep -q "ID_BUS=usb"; then - is_usb=" (USB)" - fi - - echo "$((i+1)). $drive - $info$is_usb" - - # Show partition layout - echo " Partitions:" - lsblk "$drive" | tail -n +2 | sed 's/^/ /' - echo - done - - # Auto-detection with user confirmation - local suggested_internal="" - local suggested_external="" - - # Try to suggest internal drive (prefer NVMe, then non-USB drives) - for drive in "${drives[@]}"; do - if [[ "$drive" == *"nvme"* ]]; then - suggested_internal="$drive" - break - fi - done - - if [ -z "$suggested_internal" ]; then - # If no NVMe, prefer non-USB drives - for drive in "${drives[@]}"; do - if ! udevadm info --query=property --name="$drive" 2>/dev/null | grep -q "ID_BUS=usb"; then - suggested_internal="$drive" - break - fi - done - fi - - # Try to suggest external drive (prefer USB, larger capacity) - for drive in "${drives[@]}"; do - if [ "$drive" != "$suggested_internal" ]; then - if udevadm info --query=property --name="$drive" 2>/dev/null | grep -q "ID_BUS=usb"; then - suggested_external="$drive" - break - fi - fi - done - - if [ -z "$suggested_external" ]; then - # If no USB found, use the other drive - for drive in "${drives[@]}"; do - if [ "$drive" != "$suggested_internal" ]; then - suggested_external="$drive" - break - fi - done - fi - - # Show suggestions and get user confirmation - if [ -n "$suggested_internal" ] && [ -n "$suggested_external" ]; then - echo "Suggested configuration:" - local internal_info=$(lsblk -dpno SIZE,MODEL "$suggested_internal" | xargs) - local external_info=$(lsblk -dpno SIZE,MODEL "$suggested_external" | xargs) - echo " Internal (source): $suggested_internal - $internal_info" - echo " External (target): $suggested_external - $external_info" - echo - read -p "Use this configuration? [Y/n]: " -n 1 -r - echo - if [[ $REPLY =~ ^[Nn]$ ]]; then - # Manual selection - INTERNAL_DRIVE="" - EXTERNAL_DRIVE="" - else - INTERNAL_DRIVE="$suggested_internal" - EXTERNAL_DRIVE="$suggested_external" - fi - fi - - # Manual selection if auto-detection failed or user declined - if [ -z "$INTERNAL_DRIVE" ]; then - echo "Select INTERNAL drive (source - your current system):" - for i in "${!drives[@]}"; do - local drive="${drives[$i]}" - local info=$(lsblk -dpno SIZE,MODEL "$drive" | xargs) - echo "$((i+1)). $drive - $info" - done - read -p "Enter number [1-${#drives[@]}]: " choice - if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#drives[@]}" ]; then - INTERNAL_DRIVE="${drives[$((choice-1))]}" - else - error "Invalid selection" - fi - fi - - if [ -z "$EXTERNAL_DRIVE" ]; then - echo - echo "Select EXTERNAL drive (target - will be completely overwritten!):" - for i in "${!drives[@]}"; do - local drive="${drives[$i]}" - if [ "$drive" != "$INTERNAL_DRIVE" ]; then - local info=$(lsblk -dpno SIZE,MODEL "$drive" | xargs) - echo "$((i+1)). $drive - $info" - fi - done - read -p "Enter number [1-${#drives[@]}]: " choice - if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le "${#drives[@]}" ]; then - local selected_drive="${drives[$((choice-1))]}" - if [ "$selected_drive" != "$INTERNAL_DRIVE" ]; then - EXTERNAL_DRIVE="$selected_drive" - else - error "Cannot use the same drive as both source and target!" - fi - else - error "Invalid selection" - fi - fi - - # Final validation - if [ "$INTERNAL_DRIVE" = "$EXTERNAL_DRIVE" ]; then - error "Internal and external drives cannot be the same!" - fi - - # Check drive sizes - local internal_size_bytes=$(lsblk -bno SIZE "$INTERNAL_DRIVE") - local external_size_bytes=$(lsblk -bno SIZE "$EXTERNAL_DRIVE") - - if [ "$external_size_bytes" -lt "$internal_size_bytes" ]; then - error "External drive ($EXTERNAL_DRIVE) is smaller than internal drive ($INTERNAL_DRIVE). Cannot clone." - fi - - echo - echo "Final drive selection:" - echo " Internal (source): $INTERNAL_DRIVE ($(lsblk -dpno SIZE,MODEL "$INTERNAL_DRIVE" | xargs))" - echo " External (target): $EXTERNAL_DRIVE ($(lsblk -dpno SIZE,MODEL "$EXTERNAL_DRIVE" | xargs))" - - success "Drive detection completed" - - # Final safety check - echo - echo -e "${RED}⚠️ FINAL SAFETY CHECK ⚠️${NC}" - echo "You are about to COMPLETELY CLONE this drive:" - echo " Source: $INTERNAL_DRIVE" - echo " Target: $EXTERNAL_DRIVE (will be completely overwritten!)" - echo - echo "Current partitions on target drive that will be DESTROYED:" - lsblk "$EXTERNAL_DRIVE" || true - echo - echo -e "${RED}This will DESTROY ALL DATA on $EXTERNAL_DRIVE!${NC}" - echo "The entire drive will be overwritten with an exact copy of $INTERNAL_DRIVE" - echo - read -p "Type 'CLONE' to confirm you want to overwrite $EXTERNAL_DRIVE: " confirmation - if [ "$confirmation" != "CLONE" ]; then - error "Clone operation cancelled by user for safety" - fi -} - -analyze_source_drive() { - log "Analyzing source drive structure..." - - # Get partition information - local partitions=($(lsblk -pno NAME "$INTERNAL_DRIVE" | grep -v "^$INTERNAL_DRIVE$")) - - echo "Source drive structure:" - lsblk "$INTERNAL_DRIVE" - echo - - for part in "${partitions[@]}"; do - local size=$(lsblk -no SIZE "$part") - local fstype=$(lsblk -no FSTYPE "$part") - local label=$(lsblk -no LABEL "$part") - local uuid=$(lsblk -no UUID "$part") - local mountpoint=$(lsblk -no MOUNTPOINT "$part") - - echo " $part:" - echo " Size: $size" - echo " Filesystem: ${fstype:-'unknown'}" - echo " Label: ${label:-'none'}" - echo " UUID: ${uuid:-'none'}" - echo " Mounted at: ${mountpoint:-'not mounted'}" - - # Store information for later use - PARTITION_FILESYSTEMS["$part"]="$fstype" - PARTITION_LABELS["$part"]="$label" - PARTITION_UUIDS["$part"]="$uuid" - - # Check if it's encrypted - if [[ "$fstype" == "crypto_LUKS" ]]; then - log "Found LUKS encrypted partition: $part" - fi - - echo - done - - success "Source drive analysis completed" -} - -check_prerequisites() { - log "Checking prerequisites..." - - # Check if running from live system - local root_device=$(df / | tail -1 | awk '{print $1}') - if [[ "$root_device" == *"loop"* ]] || [[ "$root_device" == *"overlay"* ]] || [[ "$root_device" == *"tmpfs"* ]]; then - success "Running from live system - good!" - else - warning "This might not be a live system. For safety, this should be run from a live USB!" - confirm_action "Continue anyway? (Not recommended)" - fi - - # Check for required tools - local missing_tools=() - for tool in dd pv rsync cryptsetup grub-install lsblk blkid partprobe; do - if ! command -v $tool >/dev/null 2>&1; then - missing_tools+=("$tool") - fi - done - - if [ ${#missing_tools[@]} -gt 0 ]; then - warning "Missing required tools: ${missing_tools[*]}" - log "Installing missing tools..." - apt update && apt install -y util-linux pv rsync cryptsetup grub-common grub-efi-amd64 || { - error "Failed to install required tools. Please install manually: ${missing_tools[*]}" - } - fi - - success "Prerequisites check passed" -} - -perform_direct_clone() { - log "Starting direct drive clone..." - log "This will create an exact bit-for-bit copy of $INTERNAL_DRIVE to $EXTERNAL_DRIVE" - - # Get drive size for progress tracking - local total_size_bytes=$(lsblk -bno SIZE "$INTERNAL_DRIVE") - local total_size_human=$(lsblk -no SIZE "$INTERNAL_DRIVE") - - log "Cloning $total_size_human from $INTERNAL_DRIVE to $EXTERNAL_DRIVE" - log "This will take a while depending on drive size and speed..." - - # Use dd with progress monitoring via pv - if dd if="$INTERNAL_DRIVE" bs=64M status=none | pv -s "$total_size_bytes" -w 80 | dd of="$EXTERNAL_DRIVE" bs=64M status=none; then - success "Drive clone completed successfully" - else - error "Drive clone failed!" - fi - - # Force kernel to re-read partition table - log "Updating partition table on cloned drive..." - partprobe "$EXTERNAL_DRIVE" || warning "Failed to update partition table" - sync - sleep 3 - - success "Direct clone operation completed" -} - -fix_uuids_and_boot() { - log "Fixing UUIDs and boot configuration on cloned drive..." - - mkdir -p "$WORK_DIR" - - # Get new partition list from cloned drive - local new_partitions=($(lsblk -pno NAME "$EXTERNAL_DRIVE" | grep -v "^$EXTERNAL_DRIVE$")) - - # Create new UUIDs for all partitions to avoid conflicts - for part in "${new_partitions[@]}"; do - local fstype=$(lsblk -no FSTYPE "$part") - local old_uuid=$(lsblk -no UUID "$part") - - log "Processing partition $part (filesystem: ${fstype:-'unknown'})" - - # Skip encrypted partitions - they'll keep their UUID - if [[ "$fstype" == "crypto_LUKS" ]]; then - log "Skipping LUKS partition UUID change - encryption handles this" - continue - fi - - # Skip swap partitions for now - we'll handle them separately - if [[ "$fstype" == "swap" ]]; then - log "Regenerating swap UUID for $part" - swapoff "$part" 2>/dev/null || true - mkswap -U random "$part" || warning "Failed to regenerate swap UUID" - continue - fi - - # Generate new UUID for filesystem partitions - if [[ -n "$fstype" && "$fstype" != "crypto_LUKS" ]]; then - case "$fstype" in - "ext2"|"ext3"|"ext4") - log "Generating new UUID for ext filesystem on $part" - tune2fs -U random "$part" || warning "Failed to change UUID for $part" - ;; - "vfat") - log "Generating new UUID for FAT filesystem on $part" - # For FAT32, we'll use mlabel (part of mtools) if available, or skip - if command -v mlabel >/dev/null 2>&1; then - # Generate a random 8-character hex string for FAT32 - local new_fat_uuid=$(openssl rand -hex 4 | tr '[:lower:]' '[:upper:]') - echo "mtools_skip_check=1" > ~/.mtoolsrc - mlabel -i "$part" -N "${new_fat_uuid:0:8}" || warning "Failed to change FAT UUID" - rm -f ~/.mtoolsrc - else - warning "Cannot change FAT UUID - mtools not available" - fi - ;; - *) - log "Skipping UUID change for unknown filesystem type: $fstype" - ;; - esac - fi - done - - # Now we need to update /etc/fstab on the cloned system - log "Mounting cloned system to update configuration..." - - # Find the root partition (usually the largest ext4 partition or check for typical structure) - local root_partition="" - local boot_partition="" - local efi_partition="" - - for part in "${new_partitions[@]}"; do - local fstype=$(lsblk -no FSTYPE "$part") - local size_bytes=$(lsblk -bno SIZE "$part") - - # Detect EFI partition (usually small FAT32) - if [[ "$fstype" == "vfat" && "$size_bytes" -lt 1073741824 ]]; then # Less than 1GB - efi_partition="$part" - log "Detected EFI partition: $part" - fi - - # Detect boot partition (usually ext4, smaller than root) - if [[ "$fstype" == "ext4" && "$size_bytes" -lt 5368709120 && "$size_bytes" -gt 104857600 ]]; then # Between 100MB and 5GB - boot_partition="$part" - log "Detected boot partition: $part" - fi - - # Detect root partition (usually largest ext4 or check for encrypted) - if [[ "$fstype" == "ext4" && "$size_bytes" -gt 5368709120 ]]; then # Larger than 5GB - root_partition="$part" - log "Detected root partition: $part" - fi - - # Handle LUKS encrypted partitions - if [[ "$fstype" == "crypto_LUKS" ]]; then - log "Found encrypted partition that might be root: $part" - # We'll try to unlock it if needed - local crypt_name="cloned_root_$(basename "$part")" - echo "This appears to be an encrypted partition. Please enter the password to mount and update configuration:" - if cryptsetup open "$part" "$crypt_name"; then - # Check if the decrypted partition is the root - local decrypted_fs=$(lsblk -no FSTYPE "/dev/mapper/$crypt_name") - if [[ "$decrypted_fs" == "ext4" ]]; then - root_partition="/dev/mapper/$crypt_name" - log "Using decrypted partition as root: $root_partition" - fi - else - warning "Could not unlock encrypted partition. Configuration update may be incomplete." - continue - fi - fi - done - - if [ -z "$root_partition" ]; then - warning "Could not automatically detect root partition. Manual configuration may be needed." - return 0 - fi - - # Mount the cloned root filesystem - log "Mounting cloned root filesystem: $root_partition" - mount "$root_partition" "$WORK_DIR" || { - error "Failed to mount cloned root filesystem" - } - - # Mount boot partition if exists - if [ -n "$boot_partition" ]; then - log "Mounting boot partition: $boot_partition" - mkdir -p "$WORK_DIR/boot" - mount "$boot_partition" "$WORK_DIR/boot" || warning "Failed to mount boot partition" - fi - - # Mount EFI partition if exists - if [ -n "$efi_partition" ]; then - log "Mounting EFI partition: $efi_partition" - mkdir -p "$WORK_DIR/boot/efi" - mount "$efi_partition" "$WORK_DIR/boot/efi" || warning "Failed to mount EFI partition" - fi - - # Update /etc/fstab with new UUIDs - if [ -f "$WORK_DIR/etc/fstab" ]; then - log "Updating /etc/fstab with new UUIDs..." - cp "$WORK_DIR/etc/fstab" "$WORK_DIR/etc/fstab.backup" - - # Generate new fstab with current UUIDs - echo "# /etc/fstab: static file system information." > "$WORK_DIR/etc/fstab.new" - echo "# Updated after cloning $(date)" >> "$WORK_DIR/etc/fstab.new" - echo "#" >> "$WORK_DIR/etc/fstab.new" - echo "# " >> "$WORK_DIR/etc/fstab.new" - - # Add entries for each partition with current UUIDs - for part in "${new_partitions[@]}"; do - local current_uuid=$(lsblk -no UUID "$part") - local fstype=$(lsblk -no FSTYPE "$part") - - if [ -n "$current_uuid" ]; then - case "$part" in - *"1") - if [[ "$fstype" == "vfat" ]]; then - echo "UUID=$current_uuid /boot/efi vfat defaults 0 2" >> "$WORK_DIR/etc/fstab.new" - fi - ;; - *"2") - if [[ "$fstype" == "ext4" ]]; then - # Could be boot or root - determine by size - local size_bytes=$(lsblk -bno SIZE "$part") - if [ "$size_bytes" -lt 5368709120 ]; then # Less than 5GB = boot - echo "UUID=$current_uuid /boot ext4 defaults 0 2" >> "$WORK_DIR/etc/fstab.new" - else # Root partition - echo "UUID=$current_uuid / ext4 defaults 0 1" >> "$WORK_DIR/etc/fstab.new" - fi - fi - ;; - *"3") - if [[ "$fstype" == "ext4" ]]; then - echo "UUID=$current_uuid / ext4 defaults 0 1" >> "$WORK_DIR/etc/fstab.new" - elif [[ "$fstype" == "swap" ]]; then - echo "UUID=$current_uuid none swap sw 0 0" >> "$WORK_DIR/etc/fstab.new" - fi - ;; - *) - if [[ "$fstype" == "swap" ]]; then - echo "UUID=$current_uuid none swap sw 0 0" >> "$WORK_DIR/etc/fstab.new" - fi - ;; - esac - fi - done - - # Add tmpfs entry - echo "tmpfs /tmp tmpfs defaults,noatime,mode=1777 0 0" >> "$WORK_DIR/etc/fstab.new" - - # Replace old fstab with new one - mv "$WORK_DIR/etc/fstab.new" "$WORK_DIR/etc/fstab" - - success "Updated /etc/fstab with new UUIDs" - else - warning "/etc/fstab not found in cloned system" - fi - - success "UUID and boot configuration updated" -} - -install_bootloader() { - log "Installing/repairing bootloader on cloned drive..." - - # Bind mount necessary filesystems for chroot - mount --bind /dev "$WORK_DIR/dev" - mount --bind /proc "$WORK_DIR/proc" - mount --bind /sys "$WORK_DIR/sys" - mount --bind /run "$WORK_DIR/run" - - # Update GRUB configuration and reinstall bootloader - chroot "$WORK_DIR" /bin/bash -c " - # Update initramfs to ensure all modules are included - update-initramfs -u -k all - - # Reinstall GRUB bootloader - grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=debian --recheck $EXTERNAL_DRIVE - - # Update GRUB configuration - update-grub - " || warning "Some bootloader operations failed but continuing..." - - # Unmount bind mounts - umount "$WORK_DIR/dev" 2>/dev/null || true - umount "$WORK_DIR/proc" 2>/dev/null || true - umount "$WORK_DIR/sys" 2>/dev/null || true - umount "$WORK_DIR/run" 2>/dev/null || true - - success "Bootloader installation completed" -} - -cleanup_clone() { - log "Cleaning up..." - - # Unmount all filesystems - umount "$WORK_DIR/boot/efi" 2>/dev/null || true - umount "$WORK_DIR/boot" 2>/dev/null || true - umount "$WORK_DIR" 2>/dev/null || true - - # Close any encrypted volumes we opened - for mapper in /dev/mapper/cloned_*; do - if [ -b "$mapper" ]; then - local crypt_name=$(basename "$mapper") - cryptsetup close "$crypt_name" 2>/dev/null || true - fi - done - - # Remove work directory - rmdir "$WORK_DIR" 2>/dev/null || true - - success "Cleanup completed" -} - -verify_clone() { - log "Performing basic verification of cloned drive..." - - # Check if partition table was copied correctly - log "Verifying partition table..." - local internal_partcount=$(lsblk -no NAME "$INTERNAL_DRIVE" | grep -c "^[├└]─") - local external_partcount=$(lsblk -no NAME "$EXTERNAL_DRIVE" | grep -c "^[├└]─") - - if [ "$internal_partcount" -eq "$external_partcount" ]; then - success "Partition count matches: $internal_partcount partitions" - else - warning "Partition count mismatch: internal=$internal_partcount, external=$external_partcount" - fi - - # Show final layout - echo - echo "Original drive layout:" - lsblk "$INTERNAL_DRIVE" - echo - echo "Cloned drive layout:" - lsblk "$EXTERNAL_DRIVE" - - success "Basic verification completed" -} - -main() { - echo -e "${GREEN}=== Direct 1-to-1 Clone Script ===${NC}" - echo "This script creates an exact copy of your internal drive to external drive" - echo "WITHOUT any LVM conversion - preserves original structure exactly" - echo "Run this from a live USB system for best results" - echo - - check_prerequisites - detect_drives - analyze_source_drive - - echo - echo "Clone Summary:" - echo " Source: $INTERNAL_DRIVE" - echo " Target: $EXTERNAL_DRIVE (will be completely overwritten)" - echo " Operation: Bit-perfect clone preserving all partitions and structures" - echo - - confirm_action "WARNING: This will COMPLETELY OVERWRITE $EXTERNAL_DRIVE!" - - perform_direct_clone - fix_uuids_and_boot - install_bootloader - verify_clone - cleanup_clone - - success "Direct 1-to-1 clone completed successfully!" - echo - echo -e "${GREEN}=== CLONE COMPLETE ===${NC}" - echo "✅ Exact copy created on external drive" - echo "✅ UUIDs updated to prevent conflicts" - echo "✅ Bootloader installed and configured" - echo "✅ Original internal drive unchanged" - echo - echo -e "${BLUE}Next steps:${NC}" - echo "1. Reboot your system" - echo "2. Enter BIOS/UEFI settings and configure:" - echo " • Set external drive as first boot device" - echo " • Ensure UEFI mode is enabled (if system uses UEFI)" - echo " • Disable Secure Boot if having issues" - echo "3. Boot from external drive" - echo "4. Should ask for LUKS password (if encrypted) and boot normally" - echo - echo -e "${GREEN}Your cloned system features:${NC}" - echo "• Identical to original - same encryption, same structure" - echo "• Independent UUIDs to avoid conflicts" - echo "• Original internal drive preserved as backup" - echo "• No LVM complexity - simple and reliable" - echo - echo -e "${YELLOW}🎉 Simple 1-to-1 clone completed successfully!${NC}" - echo "The external drive should boot exactly like your original system!" -} - -# Trap to ensure cleanup on exit -trap cleanup_clone EXIT - -main "$@" \ No newline at end of file diff --git a/improved_lvm_migration.sh b/improved_lvm_migration.sh deleted file mode 100755 index 0cde749..0000000 --- a/improved_lvm_migration.sh +++ /dev/null @@ -1,723 +0,0 @@ -#!/bin/bash - -# Improved LVM Migration Script -# Fixes the boot issues from the previous failed LVM migration -# Properly handles LUKS + LVM combination with robust boot configuration - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Configuration variables -INTERNAL_DRIVE="" -EXTERNAL_DRIVE="" -VG_NAME="migration-vg" # Changed to avoid conflict with existing system-vg -ROOT_LV="root" -HOME_LV="home" -SWAP_LV="swap" -BOOT_LV="boot" - -# Work directory -WORK_DIR="/mnt/lvm_migration" - -# Detected partitions and info -declare -A INTERNAL_PARTITIONS -declare -A PARTITION_FILESYSTEMS -declare -A PARTITION_SIZES - -log() { - echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" -} - -error() { - echo -e "${RED}[ERROR]${NC} $1" >&2 - exit 1 -} - -warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -confirm_action() { - echo -e "${YELLOW}$1${NC}" - read -p "Do you want to continue? [y/N] " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - error "Operation aborted by user" - fi -} - -detect_drives() { - log "Detecting available drives..." - - local all_drives=($(lsblk -dpno NAME,TYPE | grep "disk" | awk '{print $1}')) - local drives=() - - # Filter out the USB stick we're running from - for drive in "${all_drives[@]}"; do - if mount | grep -q "$drive" && mount | grep -q "/lib/live\|overlay\|/media.*live"; then - log "Excluding live USB drive: $drive" - continue - fi - drives+=("$drive") - done - - if [ ${#drives[@]} -lt 2 ]; then - error "Need at least 2 drives for migration. Found only ${#drives[@]} suitable drives" - fi - - echo "Available drives:" - for i in "${!drives[@]}"; do - local drive="${drives[$i]}" - local info=$(lsblk -dpno SIZE,MODEL "$drive" | xargs) - echo "$((i+1)). $drive - $info" - lsblk "$drive" | tail -n +2 | sed 's/^/ /' - echo - done - - # Auto-detect with user confirmation - local suggested_internal="" - local suggested_external="" - - # Prefer NVMe for internal, USB for external - for drive in "${drives[@]}"; do - if [[ "$drive" == *"nvme"* ]]; then - suggested_internal="$drive" - break - fi - done - - for drive in "${drives[@]}"; do - if [ "$drive" != "$suggested_internal" ]; then - if udevadm info --query=property --name="$drive" 2>/dev/null | grep -q "ID_BUS=usb"; then - suggested_external="$drive" - break - fi - fi - done - - if [ -n "$suggested_internal" ] && [ -n "$suggested_external" ]; then - echo "Suggested configuration:" - echo " Internal (source): $suggested_internal" - echo " External (LVM target): $suggested_external" - read -p "Use this configuration? [Y/n]: " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Nn]$ ]]; then - INTERNAL_DRIVE="$suggested_internal" - EXTERNAL_DRIVE="$suggested_external" - fi - fi - - # Manual selection if needed - if [ -z "$INTERNAL_DRIVE" ]; then - read -p "Select INTERNAL drive number: " choice - INTERNAL_DRIVE="${drives[$((choice-1))]}" - fi - - if [ -z "$EXTERNAL_DRIVE" ]; then - read -p "Select EXTERNAL drive number: " choice - EXTERNAL_DRIVE="${drives[$((choice-1))]}" - fi - - # Safety checks - if [ "$INTERNAL_DRIVE" = "$EXTERNAL_DRIVE" ]; then - error "Internal and external drives cannot be the same!" - fi - - local external_size_bytes=$(lsblk -bno SIZE "$EXTERNAL_DRIVE" | head -1 | tr -d ' ') - local internal_size_bytes=$(lsblk -bno SIZE "$INTERNAL_DRIVE" | head -1 | tr -d ' ') - - if [ -n "$external_size_bytes" ] && [ -n "$internal_size_bytes" ] && [ "$external_size_bytes" -lt "$internal_size_bytes" ]; then - error "External drive is smaller than internal drive" - fi - - echo - echo "Final configuration:" - echo " Internal (source): $INTERNAL_DRIVE" - echo " External (LVM target): $EXTERNAL_DRIVE" - - # Final safety confirmation - echo - echo -e "${RED}⚠️ FINAL SAFETY CHECK ⚠️${NC}" - echo "This will COMPLETELY WIPE: $EXTERNAL_DRIVE" - echo "Current partitions that will be DESTROYED:" - lsblk "$EXTERNAL_DRIVE" - echo - read -p "Type 'MIGRATE' to confirm LVM migration: " confirmation - if [ "$confirmation" != "MIGRATE" ]; then - error "Migration cancelled by user" - fi - - success "Drive selection completed" -} - -analyze_source_system() { - log "Analyzing source system..." - - # Get partitions using a more reliable method - local partitions=($(lsblk -lpno NAME "$INTERNAL_DRIVE" | tail -n +2)) - - echo "Source drive partitions:" - for part in "${partitions[@]}"; do - # Check if partition actually exists before querying - if [ ! -b "$part" ]; then - continue - fi - - local size=$(lsblk -no SIZE "$part" 2>/dev/null || echo "unknown") - local fstype=$(lsblk -no FSTYPE "$part" 2>/dev/null || echo "") - local label=$(lsblk -no LABEL "$part" 2>/dev/null || echo "") - local mountpoint=$(lsblk -no MOUNTPOINT "$part" 2>/dev/null || echo "") - - echo " $part: $size, $fstype, ${label:-'no label'}" - - PARTITION_FILESYSTEMS["$part"]="$fstype" - PARTITION_SIZES["$part"]="$size" - - # Identify partitions - if [[ "$fstype" == "vfat" ]] && [[ "$part" == *"1" ]] || [[ "$part" == *"2" ]]; then - INTERNAL_PARTITIONS["efi"]="$part" - elif [[ "$mountpoint" == "/" ]] || [[ "$label" == "root"* ]] || [[ "$fstype" == "ext4" && "$part" == *"1" ]]; then - INTERNAL_PARTITIONS["root"]="$part" - elif [[ "$mountpoint" == "/home" ]] || [[ "$label" == "home"* ]]; then - INTERNAL_PARTITIONS["home"]="$part" - elif [[ "$fstype" == "crypto_LUKS" ]]; then - # Encrypted partition - likely home - INTERNAL_PARTITIONS["encrypted_home"]="$part" - fi - done - - success "Source system analysis completed" -} - -calculate_partition_sizes() { - log "Calculating required partition sizes..." - - # Get actual source partition sizes - local source_root_gb=0 - local source_home_gb=0 - - # Check actual partition sizes from the source - for part_name in "${!INTERNAL_PARTITIONS[@]}"; do - local part_device="${INTERNAL_PARTITIONS[$part_name]}" - if [ -b "$part_device" ]; then - local size_bytes=$(lsblk -bno SIZE "$part_device" 2>/dev/null | head -1 | tr -d ' ') - if [ -n "$size_bytes" ]; then - local size_gb=$((size_bytes / 1024 / 1024 / 1024)) - - case "$part_name" in - "root") - source_root_gb=$size_gb - log "Source root: ${size_gb}GB" - ;; - "home"|"encrypted_home") - source_home_gb=$size_gb - log "Source home: ${size_gb}GB" - ;; - esac - fi - fi - done - - # Get target drive total space - local total_space_bytes=$(lsblk -bno SIZE "$EXTERNAL_DRIVE" | head -1 | tr -d ' ') - local total_space_gb=$((total_space_bytes / 1024 / 1024 / 1024)) - - log "Target drive total space: ${total_space_gb}GB" - - # Fixed sizes for system partitions - local swap_size="8G" - local boot_size="2G" - - # Calculate available space for data partitions (leave 2GB for overhead/EFI) - local available_for_data=$((total_space_gb - 8 - 2 - 2)) # 464GB available - - # For same-size drives, distribute space proportionally to source - local total_source_data=$((source_root_gb + source_home_gb)) - - if [ "$total_source_data" -gt "$available_for_data" ]; then - # Source is larger than target, scale down proportionally - local scale_factor_percent=$((available_for_data * 100 / total_source_data)) - local root_size="$((source_root_gb * scale_factor_percent / 100))G" - local home_size="$((source_home_gb * scale_factor_percent / 100))G" - - warning "Scaling down partitions to fit target drive:" - warning " Scale factor: ${scale_factor_percent}%" - else - # Target has enough space, use source sizes with small buffers - local root_size="$((source_root_gb + 5))G" # 5GB buffer for root - local remaining_space=$((available_for_data - source_root_gb - 5)) - local home_size="${remaining_space}G" # Use all remaining space for home - fi - - # Export calculated sizes - CALCULATED_ROOT_SIZE="$root_size" - CALCULATED_HOME_SIZE="$home_size" - CALCULATED_SWAP_SIZE="$swap_size" - CALCULATED_BOOT_SIZE="$boot_size" - - log "Final calculated sizes:" - log " Root: $CALCULATED_ROOT_SIZE" - log " Home: $CALCULATED_HOME_SIZE" - log " Swap: $CALCULATED_SWAP_SIZE" - log " Boot: $CALCULATED_BOOT_SIZE" - - # Verify total fits - local total_allocated=$((${CALCULATED_ROOT_SIZE%G} + ${CALCULATED_HOME_SIZE%G} + ${CALCULATED_SWAP_SIZE%G} + ${CALCULATED_BOOT_SIZE%G})) - log "Total allocated: ${total_allocated}GB of ${total_space_gb}GB" - - success "Partition sizes calculated" -} - -check_prerequisites() { - log "Checking prerequisites and installing required tools..." - - # Check if running from live system - if ! df / | grep -q "loop\|overlay\|tmpfs"; then - warning "Not running from live system - this may cause issues" - confirm_action "Continue anyway?" - fi - - # Install/update required packages - log "Installing required packages..." - apt update >/dev/null 2>&1 - apt install -y lvm2 cryptsetup rsync parted pv grub-efi-amd64 grub-common \ - e2fsprogs dosfstools bc util-linux initramfs-tools \ - efibootmgr os-prober >/dev/null 2>&1 - - # Ensure LVM2 is properly loaded - modprobe dm-mod - modprobe dm-crypt - vgchange -ay 2>/dev/null || true - - success "Prerequisites installed" -} - -create_lvm_layout() { - log "Creating LVM layout on external drive..." - - # Use the calculated sizes - local root_size="$CALCULATED_ROOT_SIZE" - local home_size="$CALCULATED_HOME_SIZE" - local swap_size="$CALCULATED_SWAP_SIZE" - local boot_size="$CALCULATED_BOOT_SIZE" - - log "Using calculated sizes: Root=$root_size, Home=$home_size, Swap=$swap_size, Boot=$boot_size" - - # Properly unmount and deactivate any existing LVM on the target drive - log "Cleaning up existing LVM on target drive..." - - # Check if target drive partitions are currently mounted or in use - local partitions_in_use=false - for part in "${EXTERNAL_DRIVE}"*; do - if [ -b "$part" ]; then - if mount | grep -q "$part"; then - log "Unmounting $part..." - umount "$part" 2>/dev/null || { - warning "Could not unmount $part - it may be in use" - partitions_in_use=true - } - fi - - # Check if this partition has a VG on it - local vg_on_part=$(pvs --noheadings -o vg_name "$part" 2>/dev/null | tr -d ' ') - if [ -n "$vg_on_part" ]; then - log "Deactivating VG '$vg_on_part' on $part" - vgchange -an "$vg_on_part" 2>/dev/null || true - vgremove -f "$vg_on_part" 2>/dev/null || true - fi - fi - done - - if [ "$partitions_in_use" = true ]; then - warning "Some partitions are in use. Continuing anyway..." - fi - - # Remove any existing LVM structures with force (only on target drive) - log "Removing existing PV structures on target drive..." - for part in "${EXTERNAL_DRIVE}"*; do - if [ -b "$part" ]; then - pvremove -ff "$part" 2>/dev/null || true - fi - done - - # Wipe filesystem signatures and partition table - log "Wiping drive signatures..." - wipefs -af "$EXTERNAL_DRIVE" 2>/dev/null || true - dd if=/dev/zero of="$EXTERNAL_DRIVE" bs=1M count=100 2>/dev/null || true - - # Force kernel to re-read partition table - partprobe "$EXTERNAL_DRIVE" 2>/dev/null || true - sleep 2 - - # Create new partition table - log "Creating new partition table..." - parted -s "$EXTERNAL_DRIVE" mklabel gpt - - # Create EFI partition (512MB) - log "Creating EFI partition..." - parted -s "$EXTERNAL_DRIVE" mkpart primary fat32 1MiB 513MiB - parted -s "$EXTERNAL_DRIVE" set 1 boot on - parted -s "$EXTERNAL_DRIVE" set 1 esp on - - # Create LVM partition (rest of disk) - log "Creating LVM partition..." - parted -s "$EXTERNAL_DRIVE" mkpart primary 513MiB 100% - parted -s "$EXTERNAL_DRIVE" set 2 lvm on - - # Force kernel to re-read the new partition table - log "Refreshing partition table..." - partprobe "$EXTERNAL_DRIVE" || { - warning "partprobe failed, trying alternative methods..." - echo 1 > /sys/block/$(basename "$EXTERNAL_DRIVE")/device/rescan 2>/dev/null || true - hdparm -z "$EXTERNAL_DRIVE" 2>/dev/null || true - } - - # Wait for partitions to appear - local retry_count=0 - while [ ! -b "${EXTERNAL_DRIVE}1" ] || [ ! -b "${EXTERNAL_DRIVE}2" ]; do - sleep 2 - retry_count=$((retry_count + 1)) - if [ $retry_count -gt 10 ]; then - error "Partitions not appearing after 20 seconds. Please reboot and try again." - fi - log "Waiting for partitions to appear... ($retry_count/10)" - done - - log "Partitions created successfully" - - # Create filesystems - mkfs.fat -F32 "${EXTERNAL_DRIVE}1" || error "Failed to create EFI filesystem" - - # Setup LVM with force flag to handle existing signatures - log "Creating physical volume..." - pvcreate -ff "${EXTERNAL_DRIVE}2" || error "Failed to create physical volume" - - # Check if VG name already exists and handle it - if vgs "$VG_NAME" >/dev/null 2>&1; then - log "Volume group $VG_NAME already exists, removing it first..." - vgremove -f "$VG_NAME" 2>/dev/null || true - # Wait a moment for cleanup - sleep 2 - fi - - log "Creating volume group..." - vgcreate "$VG_NAME" "${EXTERNAL_DRIVE}2" || error "Failed to create volume group" - - # Verify VG creation - if ! vgs "$VG_NAME" >/dev/null 2>&1; then - error "Volume group $VG_NAME was not created successfully" - fi - - # Create logical volumes - lvcreate -L "$root_size" -n "$ROOT_LV" "$VG_NAME" || error "Failed to create root LV" - lvcreate -L "$home_size" -n "$HOME_LV" "$VG_NAME" || error "Failed to create home LV" - lvcreate -L "$swap_size" -n "$SWAP_LV" "$VG_NAME" || error "Failed to create swap LV" - lvcreate -L "$boot_size" -n "$BOOT_LV" "$VG_NAME" || error "Failed to create boot LV" - - # Create filesystems on LVM volumes - mkfs.ext4 -L "root" "/dev/$VG_NAME/$ROOT_LV" || error "Failed to create root filesystem" - mkfs.ext4 -L "home" "/dev/$VG_NAME/$HOME_LV" || error "Failed to create home filesystem" - mkfs.ext4 -L "boot" "/dev/$VG_NAME/$BOOT_LV" || error "Failed to create boot filesystem" - mkswap -L "swap" "/dev/$VG_NAME/$SWAP_LV" || error "Failed to create swap" - - success "LVM layout created successfully" -} - -handle_encrypted_partitions() { - log "Handling encrypted partitions..." - - for part_name in "${!INTERNAL_PARTITIONS[@]}"; do - local part_device="${INTERNAL_PARTITIONS[$part_name]}" - local fstype="${PARTITION_FILESYSTEMS[$part_device]}" - - if [[ "$fstype" == "crypto_LUKS" ]]; then - log "Found encrypted partition: $part_device" - local crypt_name="migration_${part_name}" - - echo "Please enter password for encrypted partition ($part_device):" - if cryptsetup open "$part_device" "$crypt_name"; then - success "Unlocked $part_device as /dev/mapper/$crypt_name" - INTERNAL_PARTITIONS["$part_name"]="/dev/mapper/$crypt_name" - - # Update filesystem type - local decrypted_fs=$(lsblk -no FSTYPE "/dev/mapper/$crypt_name") - PARTITION_FILESYSTEMS["/dev/mapper/$crypt_name"]="$decrypted_fs" - else - error "Failed to unlock encrypted partition" - fi - fi - done - - success "Encrypted partitions handled" -} - -mount_filesystems() { - log "Mounting filesystems..." - - mkdir -p "$WORK_DIR"/{internal_root,internal_home,external_root,external_home,external_boot} - - # Mount internal filesystems - if [ -n "${INTERNAL_PARTITIONS[root]}" ]; then - mount "${INTERNAL_PARTITIONS[root]}" "$WORK_DIR/internal_root" - fi - - if [ -n "${INTERNAL_PARTITIONS[home]}" ]; then - mount "${INTERNAL_PARTITIONS[home]}" "$WORK_DIR/internal_home" - elif [ -n "${INTERNAL_PARTITIONS[encrypted_home]}" ]; then - mount "${INTERNAL_PARTITIONS[encrypted_home]}" "$WORK_DIR/internal_home" - fi - - # Mount external LVM filesystems - mount "/dev/$VG_NAME/$ROOT_LV" "$WORK_DIR/external_root" - mount "/dev/$VG_NAME/$HOME_LV" "$WORK_DIR/external_home" - mount "/dev/$VG_NAME/$BOOT_LV" "$WORK_DIR/external_boot" - - # Mount EFI - mkdir -p "$WORK_DIR/external_root/boot/efi" - mount "${EXTERNAL_DRIVE}1" "$WORK_DIR/external_root/boot/efi" - - success "Filesystems mounted" -} - -copy_system_data() { - log "Copying system data..." - - # Copy root filesystem - if [ -d "$WORK_DIR/internal_root" ]; then - log "Copying root filesystem..." - rsync -avxHAX --progress \ - --exclude=/home/* --exclude=/proc/* --exclude=/sys/* \ - --exclude=/dev/* --exclude=/run/* --exclude=/tmp/* \ - --exclude=/var/tmp/* --exclude=/mnt/* --exclude=/media/* \ - "$WORK_DIR/internal_root/" "$WORK_DIR/external_root/" - fi - - # Copy home filesystem - if [ -d "$WORK_DIR/internal_home" ]; then - log "Copying home filesystem..." - rsync -avxHAX --progress "$WORK_DIR/internal_home/" "$WORK_DIR/external_home/" - elif [ -d "$WORK_DIR/internal_root/home" ]; then - log "Copying /home from root filesystem..." - rsync -avxHAX --progress "$WORK_DIR/internal_root/home/" "$WORK_DIR/external_home/" - fi - - # Copy boot files - if [ -d "$WORK_DIR/internal_root/boot" ]; then - log "Copying boot files..." - rsync -avxHAX --progress \ - --exclude=/boot/efi/* \ - "$WORK_DIR/internal_root/boot/" "$WORK_DIR/external_boot/" - fi - - success "System data copied" -} - -configure_lvm_system() { - log "Configuring LVM system..." - - # Get UUIDs - local root_uuid=$(blkid -s UUID -o value "/dev/$VG_NAME/$ROOT_LV") - local home_uuid=$(blkid -s UUID -o value "/dev/$VG_NAME/$HOME_LV") - local boot_uuid=$(blkid -s UUID -o value "/dev/$VG_NAME/$BOOT_LV") - local efi_uuid=$(blkid -s UUID -o value "${EXTERNAL_DRIVE}1") - local swap_uuid=$(blkid -s UUID -o value "/dev/$VG_NAME/$SWAP_LV") - - # Create new fstab - cat > "$WORK_DIR/external_root/etc/fstab" << EOF -# /etc/fstab: static file system information for LVM system -UUID=$root_uuid / ext4 defaults 0 1 -UUID=$efi_uuid /boot/efi vfat defaults 0 2 -UUID=$boot_uuid /boot ext4 defaults 0 2 -UUID=$home_uuid /home ext4 defaults 0 2 -UUID=$swap_uuid none swap sw 0 0 -tmpfs /tmp tmpfs defaults,noatime,mode=1777 0 0 -EOF - - # Configure LVM in initramfs - echo "$VG_NAME" >> "$WORK_DIR/external_root/etc/initramfs-tools/conf.d/lvm" - echo "BOOT=local" >> "$WORK_DIR/external_root/etc/initramfs-tools/conf.d/resume" - - # Ensure LVM modules are included - cat > "$WORK_DIR/external_root/etc/initramfs-tools/modules" << EOF -# LVM modules -dm-mod -dm-crypt -dm-snapshot -EOF - - # Update GRUB configuration for LVM - sed -i 's/#GRUB_ENABLE_CRYPTODISK=y/GRUB_ENABLE_CRYPTODISK=y/' "$WORK_DIR/external_root/etc/default/grub" - echo 'GRUB_PRELOAD_MODULES="lvm"' >> "$WORK_DIR/external_root/etc/default/grub" - - success "LVM system configured" -} - -install_bootloader() { - log "Installing bootloader with LVM support..." - - # Bind mount necessary filesystems - mount --bind /dev "$WORK_DIR/external_root/dev" - mount --bind /proc "$WORK_DIR/external_root/proc" - mount --bind /sys "$WORK_DIR/external_root/sys" - mount --bind /run "$WORK_DIR/external_root/run" - - # Install and configure bootloader in chroot - chroot "$WORK_DIR/external_root" /bin/bash -c " - # Ensure LVM is available - vgscan - vgchange -ay - - # Update initramfs with LVM support - echo 'MODULES=dep' > /etc/initramfs-tools/initramfs.conf - echo 'BOOT=local' >> /etc/initramfs-tools/initramfs.conf - update-initramfs -u -k all - - # Install GRUB with LVM support - grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=debian --recheck $EXTERNAL_DRIVE - - # Update GRUB configuration - update-grub - - # Verify LVM tools are available - which lvm && echo 'LVM tools available' - ls -la /boot/initrd.img-* | head -1 - " - - # Unmount bind mounts - umount "$WORK_DIR/external_root/dev" 2>/dev/null || true - umount "$WORK_DIR/external_root/proc" 2>/dev/null || true - umount "$WORK_DIR/external_root/sys" 2>/dev/null || true - umount "$WORK_DIR/external_root/run" 2>/dev/null || true - - success "Bootloader installed with LVM support" -} - -verify_lvm_boot() { - log "Verifying LVM boot configuration..." - - # Check if initramfs contains LVM modules - local initrd_file=$(ls "$WORK_DIR/external_boot/initrd.img-"* 2>/dev/null | head -1) - if [ -n "$initrd_file" ]; then - if lsinitramfs "$initrd_file" | grep -q "dm-mod\|lvm"; then - success "Initramfs contains LVM modules" - else - warning "Initramfs may be missing LVM modules" - fi - fi - - # Check GRUB configuration - if grep -q "lvm" "$WORK_DIR/external_boot/grub/grub.cfg"; then - success "GRUB configuration includes LVM support" - else - warning "GRUB configuration may not have proper LVM support" - fi - - # Check fstab - if grep -q "/dev/$VG_NAME" "$WORK_DIR/external_root/etc/fstab"; then - success "fstab configured for LVM" - else - warning "fstab configuration issue" - fi - - success "LVM boot verification completed" -} - -cleanup() { - log "Cleaning up..." - - # Unmount filesystems in reverse order - umount "$WORK_DIR/external_root/boot/efi" 2>/dev/null || true - umount "$WORK_DIR/external_root/dev" 2>/dev/null || true - umount "$WORK_DIR/external_root/proc" 2>/dev/null || true - umount "$WORK_DIR/external_root/sys" 2>/dev/null || true - umount "$WORK_DIR/external_root/run" 2>/dev/null || true - umount "$WORK_DIR/external_root" 2>/dev/null || true - umount "$WORK_DIR/external_home" 2>/dev/null || true - umount "$WORK_DIR/external_boot" 2>/dev/null || true - umount "$WORK_DIR/internal_root" 2>/dev/null || true - umount "$WORK_DIR/internal_home" 2>/dev/null || true - - # Deactivate LVM volumes - if [ -n "$VG_NAME" ]; then - vgchange -an "$VG_NAME" 2>/dev/null || true - fi - - # Close encrypted partitions - for mapper in /dev/mapper/migration_*; do - if [ -b "$mapper" ]; then - cryptsetup close "$(basename "$mapper")" 2>/dev/null || true - fi - done - - # Remove work directory - rm -rf "$WORK_DIR" 2>/dev/null || true - - success "Cleanup completed" -} - -main() { - echo -e "${GREEN}=== Improved LVM Migration Script ===${NC}" - echo "This script migrates your system to LVM with proper boot configuration" - echo "Fixes the issues from the previous failed migration" - echo - - check_prerequisites - detect_drives - analyze_source_system - calculate_partition_sizes - - echo - echo "Migration Summary:" - echo " Source: $INTERNAL_DRIVE (current system)" - echo " Target: $EXTERNAL_DRIVE (new LVM system)" - echo " VG Name: $VG_NAME" - echo " Planned sizes:" - echo " Root: $CALCULATED_ROOT_SIZE" - echo " Home: $CALCULATED_HOME_SIZE" - echo " Swap: $CALCULATED_SWAP_SIZE" - echo " Boot: $CALCULATED_BOOT_SIZE" - echo - - confirm_action "Start LVM migration?" - - create_lvm_layout - handle_encrypted_partitions - mount_filesystems - copy_system_data - configure_lvm_system - install_bootloader - verify_lvm_boot - cleanup - - success "LVM migration completed successfully!" - echo - echo -e "${GREEN}=== MIGRATION COMPLETE ===${NC}" - echo "✅ System migrated to LVM with proper boot support" - echo "✅ Initramfs configured with LVM modules" - echo "✅ GRUB installed with LVM support" - echo "✅ Boot configuration verified" - echo - echo -e "${BLUE}Next steps:${NC}" - echo "1. Reboot system" - echo "2. Set external drive as first boot device in BIOS" - echo "3. Boot should work without reset loops" - echo "4. System will ask for any encryption passwords as normal" - echo - echo -e "${YELLOW}🎉 Improved LVM migration completed!${NC}" - echo "This version fixes the boot issues from the previous attempt." -} - -# Trap for cleanup -trap cleanup EXIT - -main "$@" \ No newline at end of file diff --git a/old_scripts/BOOT_FIX_SUMMARY.md b/old_scripts/BOOT_FIX_SUMMARY.md new file mode 100644 index 0000000..2484431 --- /dev/null +++ b/old_scripts/BOOT_FIX_SUMMARY.md @@ -0,0 +1,154 @@ +# Internal NVMe Boot Repair Summary + +## Problem +After resizing the home partition, the internal NVMe drive (nvme0n1) was showing a "reset system" message on boot, suspected GRUB issue. + +## Root Cause +The GRUB bootloader needed to be reinstalled after the partition resize, and boot failure flags needed to be cleared from the GRUB environment. + +## Solution Applied + +### 1. Updated GRUB Configuration +```bash +sudo update-grub +``` +- Regenerated `/boot/grub/grub.cfg` with current system configuration +- Found and registered kernel images: 6.8.0-84 and 6.8.0-83 + +### 2. Reinstalled GRUB Bootloader +```bash +sudo grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=ubuntu --recheck /dev/nvme0n1 +``` +- Installed GRUB to internal NVMe drive +- Files installed to `/boot/efi/EFI/ubuntu/`: + - shimx64.efi (secure boot shim) + - grubx64.efi (GRUB bootloader) + - grub.cfg (initial configuration) + +### 3. Cleared Boot Failure Flags +```bash +sudo grub-editenv - unset recordfail +sudo grub-editenv - unset boot_success +``` +- Removed any boot failure tracking that might cause "reset system" message +- GRUB environment now clean + +## Current System Status + +### Boot Configuration +- **Currently Running From**: External USB M.2 (sda - migration-vg) + - Root: `/dev/mapper/migration-vg-root` → mounted as `/` + - Home: `/dev/mapper/luks-home-internal` (on external) → mounted as `/home` + +- **Internal Drive**: NVMe (nvme0n1 - internal-vg) - NOW BOOTABLE + - EFI: `/dev/nvme0n1p1` → mounted as `/boot/efi` + - Boot: `/dev/internal-vg/boot` → mounted as `/boot` + - Root: `/dev/internal-vg/root` (ready but not currently root) + - Home: `/dev/internal-vg/home` (LUKS encrypted, not currently mounted) + +### EFI Boot Entries +``` +BootOrder: 0001,001C,001B,0000,... (Ubuntu is first) +Boot0001* Ubuntu - Points to /boot/efi/EFI/ubuntu/shimx64.efi +Boot001F* USB HDD - Currently active (external M.2) +``` + +### Partition Layout +**Internal NVMe (nvme0n1):** +- nvme0n1p1: 511MB EFI partition (757B-A377) +- nvme0n1p2: 476.4GB LVM (internal-vg) + - internal-vg/root: 56GB ext4 + - internal-vg/boot: 2GB ext4 + - internal-vg/home: 404GB LUKS encrypted + - internal-vg/swap: 8GB swap + +**External M.2 (sda):** +- sda1: EFI partition +- sda2: 476.4GB LVM (migration-vg) + - Identical structure to internal-vg + +## Testing the Fix + +### To Boot from Internal Drive: +1. **Shutdown the system**: + ```bash + sudo shutdown -h now + ``` + +2. **Disconnect the external USB M.2 drive** (sda) + +3. **Power on the system** + +4. **Expected Behavior**: + - System should boot from internal NVMe + - No more "reset system" message + - GRUB menu should appear normally + - System will boot into internal-vg volumes + +### If You Want to Keep Both Drives Connected: +1. Change boot order in BIOS to prefer internal NVMe over USB +2. Or use BIOS boot menu (usually F12) to manually select internal drive + +## Verification Commands + +Check internal drive boot readiness: +```bash +./verify_internal_boot.sh +``` + +Check EFI boot entries: +```bash +efibootmgr -v +``` + +Verify GRUB configuration: +```bash +sudo cat /boot/grub/grub.cfg | grep -A5 "menuentry" +``` + +## Important Notes + +1. **Both drives have identical LVM structure** (internal-vg and migration-vg) + - Both have root, boot, home, and swap volumes + - Home volumes are both LUKS encrypted + +2. **Current /etc/fstab** is configured for internal-vg: + ``` + /dev/internal-vg/root → / + /dev/internal-vg/boot → /boot + /dev/internal-vg/home → /home (via LUKS) + ``` + This is correct for booting from internal drive. + +3. **System is currently running from external drive** due to boot order/USB boot + - This is temporary for testing + - Internal drive is fully configured and ready + +4. **No data loss risk** - all changes were to bootloader configuration only + +## Next Steps + +Once you've verified the internal drive boots correctly: + +1. **Run the backup** to clone internal-vg to migration-vg: + ```bash + sudo ./lvm_block_backup.sh + ``` + This will create a block-level backup of internal drive to external drive. + +2. **Keep external drive as emergency backup**: + - Can boot from it if internal drive fails + - Already has identical LVM structure + +## Files Created +- `verify_internal_boot.sh` - Script to verify boot configuration +- `BOOT_FIX_SUMMARY.md` - This documentation + +## What Was Fixed +✅ GRUB bootloader reinstalled to internal NVMe +✅ GRUB configuration updated with current kernels +✅ Boot failure flags cleared +✅ EFI boot entry verified +✅ Internal drive ready to boot + +The "reset system" message should no longer appear when booting from the internal drive. diff --git a/DRIVE_SELECTION_REFERENCE.md b/old_scripts/DRIVE_SELECTION_REFERENCE.md similarity index 100% rename from DRIVE_SELECTION_REFERENCE.md rename to old_scripts/DRIVE_SELECTION_REFERENCE.md diff --git a/old_scripts/INTERNAL_DRIVE_RECOVERY.md b/old_scripts/INTERNAL_DRIVE_RECOVERY.md new file mode 100644 index 0000000..0d50a1a --- /dev/null +++ b/old_scripts/INTERNAL_DRIVE_RECOVERY.md @@ -0,0 +1,234 @@ +# Internal Drive Recovery - Complete Configuration + +## ✅ What Was Fixed + +Your internal NVMe drive is now **fully configured to boot independently** from the external M.2 backup drive. + +### Changes Made: + +#### 1. **Fixed LUKS Encryption Configuration** (`/etc/crypttab`) +**Problem**: Both internal and external drives have the same LUKS UUID, causing the system to mount home from whichever drive it found first (which was the external). + +**Solution**: Changed `/etc/crypttab` to use device path instead of UUID: +```bash +# OLD (using UUID - ambiguous): +luks-home-internal UUID=e4d30f4f-3aac-48a7-a1ca-83539598555b none luks + +# NEW (using device path - specific): +luks-home-internal /dev/internal-vg/home none luks +``` + +**Backup**: `/etc/crypttab.backup.20251006` + +#### 2. **Updated Initramfs** +Regenerated the initial RAM filesystem to include the new crypttab configuration: +```bash +sudo update-initramfs -u -k all +``` +This ensures the system uses the internal drive's home partition during boot. + +#### 3. **Reinstalled GRUB Bootloader** +```bash +sudo grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=ubuntu +``` +- Created Ubuntu EFI boot entry (Boot0001) +- Installed to internal drive's EFI partition (nvme0n1p1) + +#### 4. **Set EFI Boot Order** +```bash +sudo efibootmgr -o 0001,001C,001B,0000,... +``` +- **Ubuntu (Boot0001)** is now the FIRST boot option +- System will boot from internal drive by default +- External drive boot entries (001F USB HDD) are lower priority + +## 📊 Current System Configuration + +### Internal NVMe Drive (nvme0n1) - PRIMARY +``` +nvme0n1 (476.9 GB) +├─ nvme0n1p1 → /boot/efi (EFI partition, 511MB) +└─ nvme0n1p2 → internal-vg (LVM) + ├─ root (56GB) → / (root filesystem) + ├─ boot (2GB) → /boot (kernel images) + ├─ home (404GB) → /home (LUKS encrypted) ← NOW CONFIGURED + └─ swap (8GB) → swap +``` + +### External M.2 USB (sda) - BACKUP +``` +sda (476.9 GB) +├─ sda1 → EFI partition (unmounted) +└─ sda2 → migration-vg (LVM) + ├─ root (56GB) → (unmounted) + ├─ boot (2GB) → (unmounted) + ├─ home (404GB) → (unmounted, LUKS encrypted) + └─ swap (8GB) → (inactive) +``` + +### Mount Points (After Reboot) +``` +/dev/internal-vg/root → / +/dev/internal-vg/boot → /boot +/dev/mapper/luks-home-internal → /home (from internal-vg/home) +/dev/internal-vg/swap → swap +/dev/nvme0n1p1 → /boot/efi +``` + +## 🔐 LUKS Encryption Details + +Both drives have identical LUKS setup: +- **UUID**: e4d30f4f-3aac-48a7-a1ca-83539598555b (same on both!) +- **Type**: LUKS2 +- **Cipher**: aes-xts-plain64 +- **Keysize**: 512 bits + +The system now uses device path (`/dev/internal-vg/home`) instead of UUID to ensure it mounts the internal drive's home partition. + +## 🚀 Testing Instructions + +### Test 1: Boot with Both Drives Connected +```bash +sudo reboot +``` +**Expected**: System boots from internal drive, uses internal home partition. + +**Verify after boot**: +```bash +mount | grep -E "/ |/boot|/home" +# Should show all mounts from internal-vg +``` + +### Test 2: Boot with ONLY Internal Drive (Full Independence Test) +```bash +# 1. Shutdown system +sudo shutdown -h now + +# 2. Physically disconnect external USB M.2 drive (sda) + +# 3. Power on system + +# Expected: System boots completely from internal NVMe +``` + +**If everything works**, you'll have: +- GRUB menu appears (no "reset system" message should appear after first clean boot) +- System boots to login screen +- Can log in with encrypted home partition +- All data accessible from internal drive + +## 🔍 Verification Commands + +After reboot, run these to verify internal drive is being used: + +```bash +# Check what's mounted where +lsblk -o NAME,SIZE,TYPE,MOUNTPOINT + +# Verify home is from internal drive +sudo cryptsetup status luks-home-internal +# Should show: device: /dev/mapper/internal--vg-home + +# Check boot order +efibootmgr | grep -A1 BootOrder +# Should show: BootOrder: 0001,... (Ubuntu first) + +# Verify all internal volumes +mount | grep internal-vg +``` + +## 📝 Configuration Files Modified + +1. **`/etc/crypttab`** - Updated to use internal-vg/home device path + - Backup: `/etc/crypttab.backup.20251006` + +2. **`/boot/initrd.img-*`** - Regenerated with new crypttab + +3. **`/boot/efi/EFI/ubuntu/`** - GRUB bootloader reinstalled + +4. **UEFI NVRAM** - Boot order updated (Boot0001 first) + +## 🛡️ About the "Reset System" Message + +The "reset system" message you saw **before GRUB** is a **Lenovo BIOS/UEFI firmware notification**, not a GRUB error. + +### Why it appears: +- BIOS detected improper shutdown or boot failure +- Partition resize triggered hardware change detection +- BIOS boot counter wasn't acknowledged + +### How to clear it: +**Option 1** (Easiest): Enter BIOS setup during boot +- Press **F1** during Lenovo logo +- Just enter BIOS (don't need to change anything) +- Press **F10** to save and exit +- This acknowledges the BIOS warning + +**Option 2**: Clean shutdown cycle +```bash +sudo shutdown -h now +# Wait 10 seconds +# Power on +# BIOS may clear flag after successful boot +``` + +**Option 3**: Wait for successful boots +- After 2-3 successful clean boots, BIOS usually clears the flag automatically + +## ✨ What You Can Do Now + +### 1. Use Internal Drive Normally +Your internal NVMe is fully functional and independent. You can: +- Boot without external drive connected +- Use system normally with all data on internal drive +- Keep external drive disconnected + +### 2. Run the Backup +Now you can create a proper block-level backup: +```bash +cd ~/Nextcloud/entwicklung/Werkzeuge/backup_to_external_m.2 +sudo ./lvm_block_backup.sh +``` + +This will clone internal-vg to migration-vg on the external drive. + +### 3. Keep External as Emergency Backup +The external drive can serve as: +- **Bootable backup**: If internal fails, boot from external +- **Recovery system**: Access data if internal has issues +- **Migration tool**: Already has identical LVM structure + +## 📚 Related Files in This Directory + +- `lvm_block_backup.sh` - Block-level backup script (internal → external) +- `BOOT_FIX_SUMMARY.md` - Details about GRUB reinstallation +- `verify_internal_boot.sh` - Boot configuration verification script +- `clear_bios_boot_flag.sh` - BIOS message diagnostic tool +- `README_BACKUP.md` - Backup strategy documentation + +## ⚠️ Important Notes + +1. **Same LUKS UUID**: Both drives have identical UUIDs because they were cloned. The device path in crypttab ensures the right one is used. + +2. **Don't mix drives during boot**: If both drives are connected and you manually select the wrong one in BIOS boot menu, the system might use the external drive instead. + +3. **Backup before cloning**: The `lvm_block_backup.sh` will **OVERWRITE** the external drive's volumes. Make sure any unique data on the external is backed up first. + +4. **Home data**: Currently your home directory data is on the external drive. After the first clean reboot, verify if you need to copy any recent data from external to internal before running the backup. + +## 🎯 Summary + +**Status**: ✅ **Internal drive is fully configured and ready** + +**Changes**: +- Crypttab uses internal drive explicitly +- Initramfs updated +- GRUB reinstalled +- Boot order corrected (Ubuntu first) + +**Next Step**: +1. **Test** - Reboot to verify internal drive works +2. **Backup** - Run `lvm_block_backup.sh` to clone to external +3. **Normal Use** - Use internal drive, keep external as backup + +**BIOS Message**: Will likely clear after 1-2 clean boots or entering BIOS setup (F1) diff --git a/LIVE_USB_MIGRATION_GUIDE.md b/old_scripts/LIVE_USB_MIGRATION_GUIDE.md similarity index 100% rename from LIVE_USB_MIGRATION_GUIDE.md rename to old_scripts/LIVE_USB_MIGRATION_GUIDE.md diff --git a/LVM_MIGRATION_GUIDE.txt b/old_scripts/LVM_MIGRATION_GUIDE.txt similarity index 100% rename from LVM_MIGRATION_GUIDE.txt rename to old_scripts/LVM_MIGRATION_GUIDE.txt diff --git a/README_BACKUP.md b/old_scripts/README_BACKUP.md similarity index 100% rename from README_BACKUP.md rename to old_scripts/README_BACKUP.md diff --git a/old_scripts/README_FIRST.txt b/old_scripts/README_FIRST.txt new file mode 100644 index 0000000..85e2079 --- /dev/null +++ b/old_scripts/README_FIRST.txt @@ -0,0 +1,105 @@ +╔═══════════════════════════════════════════════════════════════════╗ +║ ║ +║ ✅ INTERNAL DRIVE RECOVERY COMPLETE ║ +║ ║ +╚═══════════════════════════════════════════════════════════════════╝ + +Your internal NVMe drive has been fully configured and is ready to +boot independently! + +═══════════════════════════════════════════════════════════════════ + +📋 WHAT WAS DONE: + +1. ✅ Fixed /etc/crypttab to use internal-vg/home explicitly +2. ✅ Updated initramfs with new LUKS configuration +3. ✅ Reinstalled GRUB bootloader on internal NVMe +4. ✅ Created Ubuntu EFI boot entry (Boot0001) +5. ✅ Set boot order: Ubuntu FIRST in boot priority + +═══════════════════════════════════════════════════════════════════ + +🚀 NEXT STEPS: + +STEP 1: REBOOT THE SYSTEM + $ sudo reboot + + After reboot, the system will use the internal drive's home + partition instead of the external one. + +STEP 2: VERIFY CONFIGURATION + $ cd ~/Nextcloud/entwicklung/Werkzeuge/backup_to_external_m.2 + $ ./verify_internal_config.sh + + All 7 checks should pass after reboot. + +STEP 3: TEST FULL INDEPENDENCE (Optional but Recommended) + $ sudo shutdown -h now + + Then: + - Physically disconnect the external USB M.2 drive + - Power on the system + - System should boot completely from internal NVMe + - You should be able to log in and access all your data + +STEP 4: RECONNECT EXTERNAL & CREATE BACKUP + After verifying internal drive works: + - Reconnect external drive + - Run the backup script: + $ sudo ./lvm_block_backup.sh + + This will create a block-level clone of internal → external + +═══════════════════════════════════════════════════════════════════ + +⚠️ ABOUT THE "RESET SYSTEM" MESSAGE: + +The message you see BEFORE GRUB is from Lenovo BIOS, not GRUB. + +To clear it: +• Press F1 during boot → Enter BIOS → Save & Exit (F10) + OR +• After 1-2 clean boots, it should disappear automatically + +This is just a BIOS notification - your system IS bootable! + +═══════════════════════════════════════════════════════════════════ + +📚 DOCUMENTATION: + +All changes are documented in: +• INTERNAL_DRIVE_RECOVERY.md - Complete technical details +• BOOT_FIX_SUMMARY.md - GRUB bootloader fixes +• verify_internal_config.sh - Configuration verification + +═══════════════════════════════════════════════════════════════════ + +🎯 CURRENT STATUS: + +✅ Internal drive fully configured +✅ Boot order corrected (Ubuntu first) +✅ GRUB properly installed +⏳ PENDING: Reboot to activate home partition change + +═══════════════════════════════════════════════════════════════════ + +💾 BACKUP STRATEGY: + +Once verified, you'll have: + +INTERNAL NVMe (nvme0n1) = PRIMARY SYSTEM +- Daily use +- Main operating system +- All your data + +EXTERNAL M.2 (sda) = BACKUP/EMERGENCY BOOT +- Block-level clone of internal +- Bootable rescue system +- Emergency access if internal fails + +═══════════════════════════════════════════════════════════════════ + +Ready to reboot? Run: + sudo reboot + +═══════════════════════════════════════════════════════════════════ diff --git a/README_FIRST.txt b/old_scripts/README_FIRST.txt.old similarity index 100% rename from README_FIRST.txt rename to old_scripts/README_FIRST.txt.old diff --git a/old_scripts/README_OLD.md b/old_scripts/README_OLD.md new file mode 100644 index 0000000..ff8b666 --- /dev/null +++ b/old_scripts/README_OLD.md @@ -0,0 +1,391 @@ +# System Backup to External M.2 SSD + +A comprehensive backup solution for Linux systems that provides both GUI and command-line interfaces for cloning your internal drive to an external M.2 SSD. + +## Features + +## Features + +- **GUI Interface**: User-friendly graphical interface built with Python Tkinter +- **Command Line Interface**: Full CLI support for automated and scripted operations +- **Smart Drive Detection**: Automatically detects internal drive and external M.2 SSDs +- **Full System Backup**: Complete drive cloning with dd for exact system replication +- **Smart Sync Backup**: ⚡ NEW! Fast incremental backups using rsync for minor changes +- **Change Analysis**: Analyze filesystem changes to recommend sync vs full backup +- **Restore Functionality**: Complete system restore from external drive +- **Portable Tools**: Backup tools survive on external drive and remain accessible after cloning +- **Reboot Integration**: Optional reboot before backup/restore operations +- **Progress Monitoring**: Real-time progress display and logging +- **Safety Features**: Multiple confirmations and drive validation +- **Desktop Integration**: Create desktop shortcuts for easy access + +## Requirements + +- Linux system (tested on Ubuntu/Debian) +- Python 3.6+ with tkinter +- External M.2 SSD in USB enclosure +- sudo privileges for drive operations + +## Installation + +1. Clone or download this repository: + ```bash + git clone + cd backup_to_external_m.2 + ``` + +2. Make scripts executable: + ```bash + chmod +x backup_script.sh + chmod +x backup_manager.py + ``` + +3. Install dependencies (if needed): + ```bash + sudo apt update + sudo apt install python3-tk pv parted + ``` + +4. **Optional**: Set up portable tools on external M.2 SSD: + ```bash + ./setup_portable_tools.sh + ``` + This creates a separate partition on your external drive that preserves backup tools even after cloning. + +## Usage + +### GUI Application + +Launch the graphical backup manager: + +```bash +python3 backup_manager.py +``` + +**Features:** +- Automatic drive detection and classification +- Source and target drive selection with smart defaults +- Real-time progress monitoring +- **Backup Modes**: + - **Smart Sync Backup**: ⚡ Fast incremental backup using rsync (requires existing backup) + - **Analyze Changes**: Analyze what has changed since last backup + - **Start Backup**: Full drive clone (immediate backup while system running) + - **Reboot & Backup**: Reboot system then full backup (recommended for first backup) +- **Restore Modes**: + - **Restore from External**: Immediate restore from external to internal + - **Reboot & Restore**: Reboot system then restore (recommended) +- **Drive Swap Button**: Easily swap source and target for restore operations + +### Command Line Script + +For command-line usage: + +```bash +# List available drives +./backup_script.sh --list + +# Analyze changes without performing backup +./backup_script.sh --analyze --target /dev/sdb + +# Smart sync backup (fast incremental update) +sudo ./backup_script.sh --sync --target /dev/sdb + +# Perform full backup with specific drives +sudo ./backup_script.sh --source /dev/nvme0n1 --target /dev/sda + +# Restore from external to internal (note the restore flag) +sudo ./backup_script.sh --restore --source /dev/sda --target /dev/nvme0n1 + +# Or more simply in restore mode (source/target are swapped automatically) +sudo ./backup_script.sh --restore --source /dev/nvme0n1 --target /dev/sda + +# Create desktop entry +./backup_script.sh --desktop + +# Launch GUI from script +./backup_script.sh --gui +``` + +### Desktop Integration + +Create a desktop shortcut: + +```bash +./backup_script.sh --desktop +``` + +This creates a clickable icon on your desktop that launches the backup tool. + +## Restore Operations + +### When to Restore +- **System Failure**: Internal drive crashed or corrupted +- **System Migration**: Moving to new hardware +- **Rollback**: Reverting to previous system state +- **Testing**: Restoring test environment + +### How to Restore + +#### GUI Method +1. **Connect External Drive**: Plug in your M.2 SSD with backup +2. **Launch GUI**: `python3 backup_manager.py` +3. **Check Drive Selection**: + - Source should be external drive (your backup) + - Target should be internal drive (will be overwritten) +4. **Use Swap Button**: If needed, click "Swap Source↔Target" +5. **Choose Restore Mode**: + - **"Restore from External"**: Immediate restore + - **"Reboot & Restore"**: Reboot then restore (recommended) + +#### Command Line Method +```bash +# Restore with automatic drive detection +sudo ./backup_script.sh --restore + +# Restore with specific drives +sudo ./backup_script.sh --restore --source /dev/sda --target /dev/nvme0n1 +``` + +### ⚠️ Restore Safety Warnings +- **Data Loss**: Target drive is completely overwritten +- **Irreversible**: No undo after restore starts +- **Multiple Confirmations**: System requires explicit confirmation +- **Drive Verification**: Double-check source and target drives +- **Boot Issues**: Ensure external drive contains valid system backup + +## Portable Tools (Boot from External M.2) + +### Setup Portable Tools +Run this once to set up backup tools on your external M.2 SSD: + +```bash +./setup_portable_tools.sh +``` + +This will: +- Create a 512MB tools partition on your external drive +- Install backup tools that survive cloning operations +- Set up automatic tool restoration after each backup + +### Using Portable Tools + +#### When Booted from External M.2 SSD: + +1. **Access Tools**: + ```bash + # Easy access helper + ./access_tools.sh + + # Or manually: + sudo mount LABEL=BACKUP_TOOLS /mnt/tools + cd /mnt/tools/backup_system + ./launch_backup_tools.sh + ``` + +2. **Restore Internal Drive**: + - Launch backup tools from external drive + - External drive (source) → Internal drive (target) + - Click "Reboot & Restore" for safest operation + +3. **Create Desktop Entry**: + ```bash + cd /mnt/tools/backup_system + ./create_desktop_entry.sh + ``` + +### Disaster Recovery Workflow + +1. **Normal Operation**: Internal drive fails +2. **Boot from External**: Use M.2 SSD as boot drive +3. **Access Tools**: Run `./access_tools.sh` +4. **Restore System**: Use backup tools to restore to new internal drive +5. **Back to Normal**: Boot from restored internal drive + +## Safety Features + +- **Drive Validation**: Prevents accidental overwriting of wrong drives +- **Size Checking**: Ensures target drive is large enough +- **Confirmation Prompts**: Multiple confirmation steps before destructive operations +- **Mount Detection**: Automatically unmounts target drives before backup +- **Progress Monitoring**: Real-time feedback during backup operations + +## File Structure + +``` +backup_to_external_m.2/ +├── backup_manager.py # GUI application +├── backup_script.sh # Command-line script +├── install.sh # Installation script +├── systemd/ # Systemd service files +│ └── backup-service.service +└── README.md # This file +``` + +## Smart Sync Technology ⚡ + +The backup system now includes advanced **Smart Sync** functionality that dramatically reduces backup time for incremental updates: + +### How Smart Sync Works + +1. **Analysis Phase**: Compares source and target filesystems to determine changes +2. **Decision Engine**: Recommends sync vs full clone based on amount of changes: + - **< 2GB changes**: Smart sync recommended (much faster) + - **2-10GB changes**: Smart sync beneficial + - **> 10GB changes**: Full clone may be more appropriate +3. **Sync Operation**: Uses rsync to transfer only changed files and metadata + +### Smart Sync Benefits + +- **Speed**: 10-100x faster than full clone for minor changes +- **No Downtime**: System remains usable during sync operation +- **Efficiency**: Only transfers changed data, preserving bandwidth and storage wear +- **Safety**: Preserves backup tools and maintains full system consistency + +### When to Use Smart Sync vs Full Clone + +**Use Smart Sync when:** +- You have an existing backup on the target drive +- Regular incremental updates (daily/weekly backups) +- Minimal system changes since last backup +- You want faster backup with minimal downtime + +**Use Full Clone when:** +- First-time backup to a new drive +- Major system changes (OS upgrade, large software installations) +- Corrupted or incomplete previous backup +- Maximum compatibility and reliability needed + +### Smart Sync Usage + +**GUI Method:** +1. Click "Analyze Changes" to see what has changed +2. Review the recommendation and estimated time savings +3. Click "Smart Sync Backup" to perform incremental update + +**Command Line:** +```bash +# Analyze changes first +./backup_script.sh --analyze --target /dev/sdb + +# Perform smart sync +sudo ./backup_script.sh --sync --target /dev/sdb +``` + +## Traditional Full Backup + +For comprehensive system backup, the system uses proven `dd` cloning technology: + +### Backup Process +1. **Drive Detection**: Automatically scans for available drives +2. **Auto-Selection**: Internal drive as source, external as target +3. **Operation Selection**: Choose backup or restore mode +4. **Validation**: Confirms drives exist and are different +5. **Execution**: Uses `dd` command to clone entire drive +6. **Progress**: Shows real-time progress and logging + +### Backup vs Restore +- **Backup**: Internal → External (preserves your system on external drive) +- **Restore**: External → Internal (overwrites internal with backup data) +- **Swap Button**: Easily switch source/target for restore operations + +### Reboot vs Immediate Operations +- **Immediate**: Faster start, but system is running (potential file locks) +- **Reboot Mode**: System restarts first, then operates (cleaner, more reliable) +- **Recommendation**: Use reboot mode for critical operations + +### Reboot & Backup Mode +1. **Script Creation**: Creates a backup script for post-reboot execution +2. **Reboot Scheduling**: Schedules system reboot +3. **Auto-Execution**: Backup starts automatically after reboot +4. **Notification**: Shows completion status + +### Command Line Mode +- Direct `dd` cloning with progress monitoring +- Comprehensive logging to `/var/log/system_backup.log` +- Drive size validation and safety checks + +## Technical Details + +### Backup Method +- Uses `dd` command for bit-perfect drive cloning +- Block size optimized at 4MB for performance +- `fdatasync` ensures all data is written to disk + +### Drive Detection +- Uses `lsblk` to enumerate block devices +- Filters for actual disk drives +- Shows drive sizes for easy identification + +### Safety Mechanisms +- Root privilege verification +- Block device validation +- Source/target drive comparison +- Mount status checking +- Size compatibility verification + +## Troubleshooting + +### Common Issues + +1. **Permission Denied** + ```bash + # Run with sudo for drive operations + sudo python3 backup_manager.py + ``` + +2. **Drive Not Detected** + ```bash + # Check if drive is connected and recognized + lsblk + dmesg | tail + ``` + +3. **Backup Fails** + ```bash + # Check system logs + sudo journalctl -f + tail -f /var/log/system_backup.log + ``` + +### Performance Tips + +- Use USB 3.0+ connection for external M.2 SSD +- Ensure sufficient power supply for external enclosure +- Close unnecessary applications during backup +- Use SSD for better performance than traditional HDD + +## Security Considerations + +- **Data Destruction**: Target drive data is completely overwritten +- **Root Access**: Scripts require elevated privileges +- **Verification**: Always verify backup integrity after completion +- **Testing**: Test restore process with non-critical data first + +## Limitations + +- Only works with block devices (entire drives) +- Cannot backup to smaller drives +- Requires manual drive selection for safety +- No incremental backup support (full clone only) + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +## License + +This project is open source. Use at your own risk. + +## Disclaimer + +**WARNING**: This tool performs low-level disk operations that can result in data loss. Always: +- Verify drive selections carefully +- Test with non-critical data first +- Maintain separate backups of important data +- Understand the risks involved + +The authors are not responsible for any data loss or system damage. diff --git a/SIMPLE_CLONE_SOLUTION.md b/old_scripts/SIMPLE_CLONE_SOLUTION.md similarity index 100% rename from SIMPLE_CLONE_SOLUTION.md rename to old_scripts/SIMPLE_CLONE_SOLUTION.md diff --git a/START_SIMPLE_CLONE.txt b/old_scripts/START_SIMPLE_CLONE.txt similarity index 100% rename from START_SIMPLE_CLONE.txt rename to old_scripts/START_SIMPLE_CLONE.txt diff --git a/old_scripts/boot_repair_tools.sh b/old_scripts/boot_repair_tools.sh old mode 100755 new mode 100644 diff --git a/old_scripts/build-deb.sh b/old_scripts/build-deb.sh new file mode 100644 index 0000000..2abe23e --- /dev/null +++ b/old_scripts/build-deb.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Build script for LVM Backup Manager .deb package + +set -e + +echo "🔨 Building LVM Backup Manager .deb package..." + +# Cleanup old builds +rm -f lvm-backup-manager_*.deb + +# Set proper permissions +find deb-package -type f -exec chmod 644 {} \; +find deb-package -type d -exec chmod 755 {} \; +chmod 755 deb-package/DEBIAN/postinst +chmod 755 deb-package/usr/bin/* + +# Calculate package size +PACKAGE_SIZE=$(du -s deb-package | cut -f1) +echo "Installed-Size: $PACKAGE_SIZE" >> deb-package/DEBIAN/control + +# Build the package +echo "📦 Creating .deb package..." +dpkg-deb --build deb-package lvm-backup-manager_1.0.0_all.deb + +# Verify the package +echo "✅ Verifying package..." +dpkg-deb --info lvm-backup-manager_1.0.0_all.deb +dpkg-deb --contents lvm-backup-manager_1.0.0_all.deb + +echo "" +echo "🎉 Package built successfully!" +echo "📦 File: lvm-backup-manager_1.0.0_all.deb" +echo "" +echo "To install:" +echo " sudo dpkg -i lvm-backup-manager_1.0.0_all.deb" +echo " sudo apt-get install -f # Fix any missing dependencies" +echo "" +echo "To test the GUI:" +echo " sudo lvm-backup-manager" \ No newline at end of file diff --git a/old_scripts/clear_bios_boot_flag.sh b/old_scripts/clear_bios_boot_flag.sh new file mode 100755 index 0000000..28df70c --- /dev/null +++ b/old_scripts/clear_bios_boot_flag.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# Clear Lenovo BIOS Boot Flags +# This script helps clear the "reset system" message shown before GRUB + +set -e + +echo "=== Lenovo BIOS Boot Flag Clear Utility ===" +echo "" +echo "The 'reset system' message you see before GRUB is from the Lenovo BIOS/UEFI firmware." +echo "This typically indicates the BIOS detected an improper shutdown or boot failure." +echo "" + +echo "Current boot configuration:" +echo "- Root: internal-vg on nvme0n1 (internal drive)" +echo "- Boot: internal-vg on nvme0n1 (internal drive)" +echo "- EFI: nvme0n1p1 (internal drive)" +echo "- Home: migration-vg on sda (external drive)" +echo "" + +echo "Checking GRUB installation on internal drive..." +if [ -f /boot/efi/EFI/ubuntu/shimx64.efi ]; then + echo "✓ GRUB bootloader is properly installed on internal nvme0n1" +else + echo "✗ GRUB bootloader missing - need to reinstall" + exit 1 +fi +echo "" + +echo "Checking EFI boot entries..." +if efibootmgr | grep -q "ubuntu"; then + echo "✓ Ubuntu boot entry exists in UEFI firmware" + efibootmgr | grep ubuntu +else + echo "✗ Ubuntu boot entry missing" + exit 1 +fi +echo "" + +echo "=== Solutions to Clear BIOS 'Reset System' Message ===" +echo "" +echo "OPTION 1: Clean Shutdown and Boot Sequence (Recommended)" +echo " 1. Disconnect the external M.2 USB drive (sda)" +echo " 2. Perform a clean shutdown: sudo shutdown -h now" +echo " 3. Wait 10 seconds after system powers off" +echo " 4. Power on the system" +echo " 5. The BIOS should recognize the clean boot and clear the flag" +echo "" +echo "OPTION 2: Enter BIOS Setup" +echo " 1. Reboot and press F1 (or F2) during POST to enter BIOS" +echo " 2. Go to 'Startup' or 'Boot' menu" +echo " 3. Check for any warnings or errors" +echo " 4. Save and exit BIOS (F10)" +echo " 5. This acknowledges any BIOS messages and clears flags" +echo "" +echo "OPTION 3: Reset BIOS Boot Order" +echo " 1. Enter BIOS (F1 during boot)" +echo " 2. Go to Startup → Boot menu" +echo " 3. Ensure 'Ubuntu' is at the top of boot order" +echo " 4. Disable 'Fast Boot' if enabled (can cause issues with dual boot)" +echo " 5. Save and exit" +echo "" +echo "OPTION 4: Update UEFI Boot Entry Priority (from Linux)" +echo " This will ensure Ubuntu on internal drive is the first boot option:" +echo "" + +read -p "Do you want to set Ubuntu as the first boot option now? [y/N] " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + UBUNTU_ENTRY=$(efibootmgr | grep -i ubuntu | cut -d'*' -f1 | cut -d't' -f2 | head -1) + if [ -n "$UBUNTU_ENTRY" ]; then + echo "Setting Boot$UBUNTU_ENTRY (Ubuntu) as first boot option..." + sudo efibootmgr -n $UBUNTU_ENTRY + echo "✓ Next boot will use Ubuntu entry" + echo "" + echo "To make this permanent:" + BOOT_ORDER=$(efibootmgr | grep BootOrder | cut -d':' -f2 | tr -d ' ') + NEW_ORDER="$UBUNTU_ENTRY,${BOOT_ORDER//,${UBUNTU_ENTRY}/}" + NEW_ORDER="${NEW_ORDER//${UBUNTU_ENTRY},,/,}" + echo "Run: sudo efibootmgr -o $NEW_ORDER" + else + echo "Could not find Ubuntu boot entry" + fi +fi +echo "" + +echo "=== Additional Diagnostics ===" +echo "" +echo "Check system journal for boot issues:" +echo " sudo journalctl -b -p err" +echo "" +echo "Check hardware clock:" +echo " sudo hwclock --show" +echo "" +echo "Verify GRUB is working:" +echo " sudo grub-editenv list" +echo "" + +echo "=== Why This Happens ===" +echo "" +echo "The Lenovo BIOS shows 'reset system' when:" +echo "1. System was not shut down properly (power loss, crash, forced shutdown)" +echo "2. BIOS detected a boot failure and wants you to acknowledge it" +echo "3. Hardware configuration changed (like resizing partitions)" +echo "4. BIOS boot counter wasn't properly reset after successful boot" +echo "" +echo "Since your GRUB is properly installed and working, this is just a BIOS" +echo "notification that needs to be acknowledged through clean boot cycle." diff --git a/old_scripts/deb-package/DEBIAN/control b/old_scripts/deb-package/DEBIAN/control new file mode 100644 index 0000000..e3838de --- /dev/null +++ b/old_scripts/deb-package/DEBIAN/control @@ -0,0 +1,19 @@ +Package: lvm-backup-manager +Version: 1.0.0 +Section: utils +Priority: optional +Architecture: all +Depends: python3, python3-tk, lvm2 +Maintainer: LVM Backup Team +Description: Professional LVM Backup Manager with GUI + A modern, user-friendly graphical interface for creating + block-level backups of LVM volumes. Features include: + . + * Intuitive drive selection with size information + * Real-time progress monitoring + * Time estimation and speed indicators + * Professional logging and verification + * Safe snapshot-based backup process + . + This tool creates exact, bootable clones of your LVM + volumes for complete system backup and recovery. \ No newline at end of file diff --git a/old_scripts/deb-package/DEBIAN/postinst b/old_scripts/deb-package/DEBIAN/postinst new file mode 100644 index 0000000..3522431 --- /dev/null +++ b/old_scripts/deb-package/DEBIAN/postinst @@ -0,0 +1,21 @@ +#!/bin/bash +# Post-installation script for LVM Backup Manager + +set -e + +# Set proper permissions +chmod +x /usr/bin/lvm-backup-manager +chmod +x /usr/bin/lvm-block-backup + +# Create symlink for easy access +if [ ! -L /usr/local/bin/lvm-backup-gui ]; then + ln -s /usr/bin/lvm-backup-manager /usr/local/bin/lvm-backup-gui +fi + +echo "LVM Backup Manager installed successfully!" +echo "You can now launch it from:" +echo " - Applications menu: System Tools → LVM Backup Manager" +echo " - Terminal: sudo lvm-backup-manager" +echo " - Desktop: Double-click the LVM Backup Manager icon" + +exit 0 \ No newline at end of file diff --git a/old_scripts/deb-package/usr/bin/lvm-backup-manager b/old_scripts/deb-package/usr/bin/lvm-backup-manager new file mode 100644 index 0000000..853c108 --- /dev/null +++ b/old_scripts/deb-package/usr/bin/lvm-backup-manager @@ -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" "$@" \ No newline at end of file diff --git a/lvm_block_backup.sh b/old_scripts/deb-package/usr/bin/lvm-block-backup old mode 100755 new mode 100644 similarity index 100% rename from lvm_block_backup.sh rename to old_scripts/deb-package/usr/bin/lvm-block-backup diff --git a/old_scripts/deb-package/usr/bin/lvm_backup_gui.py b/old_scripts/deb-package/usr/bin/lvm_backup_gui.py new file mode 100644 index 0000000..5c0a569 --- /dev/null +++ b/old_scripts/deb-package/usr/bin/lvm_backup_gui.py @@ -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('<>', 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() \ No newline at end of file diff --git a/old_scripts/deb-package/usr/share/applications/lvm-backup-manager.desktop b/old_scripts/deb-package/usr/share/applications/lvm-backup-manager.desktop new file mode 100644 index 0000000..c55ff5c --- /dev/null +++ b/old_scripts/deb-package/usr/share/applications/lvm-backup-manager.desktop @@ -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 \ No newline at end of file diff --git a/old_scripts/deb-package/usr/share/doc/lvm-backup-manager/README b/old_scripts/deb-package/usr/share/doc/lvm-backup-manager/README new file mode 100644 index 0000000..88339fa --- /dev/null +++ b/old_scripts/deb-package/usr/share/doc/lvm-backup-manager/README @@ -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. \ No newline at end of file diff --git a/old_scripts/deb-package/usr/share/pixmaps/lvm-backup-manager.svg b/old_scripts/deb-package/usr/share/pixmaps/lvm-backup-manager.svg new file mode 100644 index 0000000..c5621eb --- /dev/null +++ b/old_scripts/deb-package/usr/share/pixmaps/lvm-backup-manager.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LVM + \ No newline at end of file diff --git a/old_scripts/direct_clone_backup.sh b/old_scripts/direct_clone_backup.sh old mode 100755 new mode 100644 diff --git a/old_scripts/improved_lvm_migration.sh b/old_scripts/improved_lvm_migration.sh old mode 100755 new mode 100644 index 453cd09..0cde749 --- a/old_scripts/improved_lvm_migration.sh +++ b/old_scripts/improved_lvm_migration.sh @@ -16,7 +16,7 @@ NC='\033[0m' # No Color # Configuration variables INTERNAL_DRIVE="" EXTERNAL_DRIVE="" -VG_NAME="system-vg" +VG_NAME="migration-vg" # Changed to avoid conflict with existing system-vg ROOT_LV="root" HOME_LV="home" SWAP_LV="swap" @@ -133,10 +133,10 @@ detect_drives() { error "Internal and external drives cannot be the same!" fi - local external_size_bytes=$(lsblk -bno SIZE "$EXTERNAL_DRIVE") - local internal_size_bytes=$(lsblk -bno SIZE "$INTERNAL_DRIVE") + local external_size_bytes=$(lsblk -bno SIZE "$EXTERNAL_DRIVE" | head -1 | tr -d ' ') + local internal_size_bytes=$(lsblk -bno SIZE "$INTERNAL_DRIVE" | head -1 | tr -d ' ') - if [ "$external_size_bytes" -lt "$internal_size_bytes" ]; then + if [ -n "$external_size_bytes" ] && [ -n "$internal_size_bytes" ] && [ "$external_size_bytes" -lt "$internal_size_bytes" ]; then error "External drive is smaller than internal drive" fi @@ -163,14 +163,20 @@ detect_drives() { analyze_source_system() { log "Analyzing source system..." - local partitions=($(lsblk -pno NAME "$INTERNAL_DRIVE" | grep -v "^$INTERNAL_DRIVE$")) + # Get partitions using a more reliable method + local partitions=($(lsblk -lpno NAME "$INTERNAL_DRIVE" | tail -n +2)) echo "Source drive partitions:" for part in "${partitions[@]}"; do - local size=$(lsblk -no SIZE "$part") - local fstype=$(lsblk -no FSTYPE "$part") - local label=$(lsblk -no LABEL "$part") - local mountpoint=$(lsblk -no MOUNTPOINT "$part") + # Check if partition actually exists before querying + if [ ! -b "$part" ]; then + continue + fi + + local size=$(lsblk -no SIZE "$part" 2>/dev/null || echo "unknown") + local fstype=$(lsblk -no FSTYPE "$part" 2>/dev/null || echo "") + local label=$(lsblk -no LABEL "$part" 2>/dev/null || echo "") + local mountpoint=$(lsblk -no MOUNTPOINT "$part" 2>/dev/null || echo "") echo " $part: $size, $fstype, ${label:-'no label'}" @@ -193,6 +199,85 @@ analyze_source_system() { success "Source system analysis completed" } +calculate_partition_sizes() { + log "Calculating required partition sizes..." + + # Get actual source partition sizes + local source_root_gb=0 + local source_home_gb=0 + + # Check actual partition sizes from the source + for part_name in "${!INTERNAL_PARTITIONS[@]}"; do + local part_device="${INTERNAL_PARTITIONS[$part_name]}" + if [ -b "$part_device" ]; then + local size_bytes=$(lsblk -bno SIZE "$part_device" 2>/dev/null | head -1 | tr -d ' ') + if [ -n "$size_bytes" ]; then + local size_gb=$((size_bytes / 1024 / 1024 / 1024)) + + case "$part_name" in + "root") + source_root_gb=$size_gb + log "Source root: ${size_gb}GB" + ;; + "home"|"encrypted_home") + source_home_gb=$size_gb + log "Source home: ${size_gb}GB" + ;; + esac + fi + fi + done + + # Get target drive total space + local total_space_bytes=$(lsblk -bno SIZE "$EXTERNAL_DRIVE" | head -1 | tr -d ' ') + local total_space_gb=$((total_space_bytes / 1024 / 1024 / 1024)) + + log "Target drive total space: ${total_space_gb}GB" + + # Fixed sizes for system partitions + local swap_size="8G" + local boot_size="2G" + + # Calculate available space for data partitions (leave 2GB for overhead/EFI) + local available_for_data=$((total_space_gb - 8 - 2 - 2)) # 464GB available + + # For same-size drives, distribute space proportionally to source + local total_source_data=$((source_root_gb + source_home_gb)) + + if [ "$total_source_data" -gt "$available_for_data" ]; then + # Source is larger than target, scale down proportionally + local scale_factor_percent=$((available_for_data * 100 / total_source_data)) + local root_size="$((source_root_gb * scale_factor_percent / 100))G" + local home_size="$((source_home_gb * scale_factor_percent / 100))G" + + warning "Scaling down partitions to fit target drive:" + warning " Scale factor: ${scale_factor_percent}%" + else + # Target has enough space, use source sizes with small buffers + local root_size="$((source_root_gb + 5))G" # 5GB buffer for root + local remaining_space=$((available_for_data - source_root_gb - 5)) + local home_size="${remaining_space}G" # Use all remaining space for home + fi + + # Export calculated sizes + CALCULATED_ROOT_SIZE="$root_size" + CALCULATED_HOME_SIZE="$home_size" + CALCULATED_SWAP_SIZE="$swap_size" + CALCULATED_BOOT_SIZE="$boot_size" + + log "Final calculated sizes:" + log " Root: $CALCULATED_ROOT_SIZE" + log " Home: $CALCULATED_HOME_SIZE" + log " Swap: $CALCULATED_SWAP_SIZE" + log " Boot: $CALCULATED_BOOT_SIZE" + + # Verify total fits + local total_allocated=$((${CALCULATED_ROOT_SIZE%G} + ${CALCULATED_HOME_SIZE%G} + ${CALCULATED_SWAP_SIZE%G} + ${CALCULATED_BOOT_SIZE%G})) + log "Total allocated: ${total_allocated}GB of ${total_space_gb}GB" + + success "Partition sizes calculated" +} + check_prerequisites() { log "Checking prerequisites and installing required tools..." @@ -220,56 +305,124 @@ check_prerequisites() { create_lvm_layout() { log "Creating LVM layout on external drive..." - # Calculate sizes based on source - local root_size="70G" - local home_size="100G" - local swap_size="8G" - local boot_size="2G" + # Use the calculated sizes + local root_size="$CALCULATED_ROOT_SIZE" + local home_size="$CALCULATED_HOME_SIZE" + local swap_size="$CALCULATED_SWAP_SIZE" + local boot_size="$CALCULATED_BOOT_SIZE" - # Adjust based on available space - local total_space_gb=$(lsblk -bno SIZE "$EXTERNAL_DRIVE" | awk '{print int($1/1024/1024/1024)}') - if [ "$total_space_gb" -gt 500 ]; then - home_size="200G" - root_size="100G" + log "Using calculated sizes: Root=$root_size, Home=$home_size, Swap=$swap_size, Boot=$boot_size" + + # Properly unmount and deactivate any existing LVM on the target drive + log "Cleaning up existing LVM on target drive..." + + # Check if target drive partitions are currently mounted or in use + local partitions_in_use=false + for part in "${EXTERNAL_DRIVE}"*; do + if [ -b "$part" ]; then + if mount | grep -q "$part"; then + log "Unmounting $part..." + umount "$part" 2>/dev/null || { + warning "Could not unmount $part - it may be in use" + partitions_in_use=true + } + fi + + # Check if this partition has a VG on it + local vg_on_part=$(pvs --noheadings -o vg_name "$part" 2>/dev/null | tr -d ' ') + if [ -n "$vg_on_part" ]; then + log "Deactivating VG '$vg_on_part' on $part" + vgchange -an "$vg_on_part" 2>/dev/null || true + vgremove -f "$vg_on_part" 2>/dev/null || true + fi + fi + done + + if [ "$partitions_in_use" = true ]; then + warning "Some partitions are in use. Continuing anyway..." fi - log "LVM sizes: Root=$root_size, Home=$home_size, Swap=$swap_size, Boot=$boot_size" + # Remove any existing LVM structures with force (only on target drive) + log "Removing existing PV structures on target drive..." + for part in "${EXTERNAL_DRIVE}"*; do + if [ -b "$part" ]; then + pvremove -ff "$part" 2>/dev/null || true + fi + done - # Wipe and create partition table - wipefs -a "$EXTERNAL_DRIVE" + # Wipe filesystem signatures and partition table + log "Wiping drive signatures..." + wipefs -af "$EXTERNAL_DRIVE" 2>/dev/null || true + dd if=/dev/zero of="$EXTERNAL_DRIVE" bs=1M count=100 2>/dev/null || true + + # Force kernel to re-read partition table + partprobe "$EXTERNAL_DRIVE" 2>/dev/null || true + sleep 2 + + # Create new partition table + log "Creating new partition table..." parted -s "$EXTERNAL_DRIVE" mklabel gpt # Create EFI partition (512MB) + log "Creating EFI partition..." parted -s "$EXTERNAL_DRIVE" mkpart primary fat32 1MiB 513MiB parted -s "$EXTERNAL_DRIVE" set 1 boot on parted -s "$EXTERNAL_DRIVE" set 1 esp on # Create LVM partition (rest of disk) + log "Creating LVM partition..." parted -s "$EXTERNAL_DRIVE" mkpart primary 513MiB 100% parted -s "$EXTERNAL_DRIVE" set 2 lvm on - # Wait for partitions - sleep 3 - partprobe "$EXTERNAL_DRIVE" - sleep 2 + # Force kernel to re-read the new partition table + log "Refreshing partition table..." + partprobe "$EXTERNAL_DRIVE" || { + warning "partprobe failed, trying alternative methods..." + echo 1 > /sys/block/$(basename "$EXTERNAL_DRIVE")/device/rescan 2>/dev/null || true + hdparm -z "$EXTERNAL_DRIVE" 2>/dev/null || true + } - # Verify partitions exist - if [ ! -b "${EXTERNAL_DRIVE}1" ] || [ ! -b "${EXTERNAL_DRIVE}2" ]; then - error "Partitions not created properly" - fi + # Wait for partitions to appear + local retry_count=0 + while [ ! -b "${EXTERNAL_DRIVE}1" ] || [ ! -b "${EXTERNAL_DRIVE}2" ]; do + sleep 2 + retry_count=$((retry_count + 1)) + if [ $retry_count -gt 10 ]; then + error "Partitions not appearing after 20 seconds. Please reboot and try again." + fi + log "Waiting for partitions to appear... ($retry_count/10)" + done + + log "Partitions created successfully" # Create filesystems mkfs.fat -F32 "${EXTERNAL_DRIVE}1" || error "Failed to create EFI filesystem" - # Setup LVM - pvcreate "${EXTERNAL_DRIVE}2" || error "Failed to create physical volume" + # Setup LVM with force flag to handle existing signatures + log "Creating physical volume..." + pvcreate -ff "${EXTERNAL_DRIVE}2" || error "Failed to create physical volume" + + # Check if VG name already exists and handle it + if vgs "$VG_NAME" >/dev/null 2>&1; then + log "Volume group $VG_NAME already exists, removing it first..." + vgremove -f "$VG_NAME" 2>/dev/null || true + # Wait a moment for cleanup + sleep 2 + fi + + log "Creating volume group..." vgcreate "$VG_NAME" "${EXTERNAL_DRIVE}2" || error "Failed to create volume group" + # Verify VG creation + if ! vgs "$VG_NAME" >/dev/null 2>&1; then + error "Volume group $VG_NAME was not created successfully" + fi + # Create logical volumes - lvcreate -L "$ROOT_SIZE" -n "$ROOT_LV" "$VG_NAME" || error "Failed to create root LV" - lvcreate -L "$HOME_SIZE" -n "$HOME_LV" "$VG_NAME" || error "Failed to create home LV" - lvcreate -L "$SWAP_SIZE" -n "$SWAP_LV" "$VG_NAME" || error "Failed to create swap LV" - lvcreate -L "$BOOT_SIZE" -n "$BOOT_LV" "$VG_NAME" || error "Failed to create boot LV" + lvcreate -L "$root_size" -n "$ROOT_LV" "$VG_NAME" || error "Failed to create root LV" + lvcreate -L "$home_size" -n "$HOME_LV" "$VG_NAME" || error "Failed to create home LV" + lvcreate -L "$swap_size" -n "$SWAP_LV" "$VG_NAME" || error "Failed to create swap LV" + lvcreate -L "$boot_size" -n "$BOOT_LV" "$VG_NAME" || error "Failed to create boot LV" # Create filesystems on LVM volumes mkfs.ext4 -L "root" "/dev/$VG_NAME/$ROOT_LV" || error "Failed to create root filesystem" @@ -482,14 +635,23 @@ verify_lvm_boot() { cleanup() { log "Cleaning up..." - # Unmount filesystems + # Unmount filesystems in reverse order umount "$WORK_DIR/external_root/boot/efi" 2>/dev/null || true + umount "$WORK_DIR/external_root/dev" 2>/dev/null || true + umount "$WORK_DIR/external_root/proc" 2>/dev/null || true + umount "$WORK_DIR/external_root/sys" 2>/dev/null || true + umount "$WORK_DIR/external_root/run" 2>/dev/null || true umount "$WORK_DIR/external_root" 2>/dev/null || true umount "$WORK_DIR/external_home" 2>/dev/null || true umount "$WORK_DIR/external_boot" 2>/dev/null || true umount "$WORK_DIR/internal_root" 2>/dev/null || true umount "$WORK_DIR/internal_home" 2>/dev/null || true + # Deactivate LVM volumes + if [ -n "$VG_NAME" ]; then + vgchange -an "$VG_NAME" 2>/dev/null || true + fi + # Close encrypted partitions for mapper in /dev/mapper/migration_*; do if [ -b "$mapper" ]; then @@ -497,6 +659,9 @@ cleanup() { fi done + # Remove work directory + rm -rf "$WORK_DIR" 2>/dev/null || true + success "Cleanup completed" } @@ -509,12 +674,18 @@ main() { check_prerequisites detect_drives analyze_source_system + calculate_partition_sizes echo echo "Migration Summary:" echo " Source: $INTERNAL_DRIVE (current system)" echo " Target: $EXTERNAL_DRIVE (new LVM system)" echo " VG Name: $VG_NAME" + echo " Planned sizes:" + echo " Root: $CALCULATED_ROOT_SIZE" + echo " Home: $CALCULATED_HOME_SIZE" + echo " Swap: $CALCULATED_SWAP_SIZE" + echo " Boot: $CALCULATED_BOOT_SIZE" echo confirm_action "Start LVM migration?" diff --git a/old_scripts/install_borg.sh b/old_scripts/install_borg.sh new file mode 100644 index 0000000..5826af3 --- /dev/null +++ b/old_scripts/install_borg.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Install BorgBackup for cloud backups +echo "🔧 Installing BorgBackup for cloud backup functionality..." + +# Update package list +sudo apt update + +# Install borgbackup +sudo apt install -y borgbackup + +# Check installation +if command -v borg >/dev/null 2>&1; then + echo "✅ BorgBackup installed successfully!" + borg --version + echo + echo "📖 Usage:" + echo "1. Run the LVM Backup GUI" + echo "2. Click '☁️ Borg to Cloud' button" + echo "3. GUI will automatically:" + echo " - Create LVM snapshots if needed" + echo " - Mount snapshots (including encrypted home)" + echo " - Initialize Borg repository in ~/Nextcloud/backups/" + echo " - Create compressed, deduplicated backup" + echo " - Clean up automatically" + echo + echo "🔐 Benefits:" + echo " - Compressed backups (save space)" + echo " - Deduplication (only store changes)" + echo " - Encrypted storage" + echo " - Automatic Nextcloud sync" + echo " - Incremental backups" +else + echo "❌ BorgBackup installation failed" + exit 1 +fi \ No newline at end of file diff --git a/old_scripts/lvm_backup_gui.py b/old_scripts/lvm_backup_gui.py new file mode 100644 index 0000000..d08671e --- /dev/null +++ b/old_scripts/lvm_backup_gui.py @@ -0,0 +1,1722 @@ +#!/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, filedialog +import subprocess +import threading +import datetime +import os +import queue +import json + +class BorgConfigDialog(tk.Toplevel): + def __init__(self, parent, callback): + super().__init__(parent) + self.title("Borg Backup Configuration") + self.geometry("550x280") + self.callback = callback + self.repo_path = tk.StringVar(self) + self.passphrase = tk.StringVar(self) + self.create_new = tk.BooleanVar(self) + self.use_target_drive = tk.BooleanVar(self) + self.parent_gui = parent + + # Load saved settings + self.load_saved_values() + self.init_ui() + + def load_saved_values(self): + """Load previously saved values""" + settings = self.parent_gui.settings + if settings.get("last_borg_repo"): + self.repo_path.set(settings["last_borg_repo"]) + if settings.get("last_passphrase"): + self.passphrase.set(settings["last_passphrase"]) + self.create_new.set(settings.get("create_new_default", True)) + + def init_ui(self): + frame = ttk.Frame(self) + frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # Repository path section + ttk.Label(frame, text="Borg Repository:").grid(row=0, column=0, sticky=tk.W) + entry = ttk.Entry(frame, textvariable=self.repo_path, width=40) + entry.grid(row=0, column=1, sticky=tk.W) + ttk.Button(frame, text="Browse", command=self.browse_repo).grid(row=0, column=2, padx=5) + + # Option to use target drive as base path + target_frame = ttk.Frame(frame) + target_frame.grid(row=1, column=1, sticky=tk.W, pady=5) + ttk.Checkbutton(target_frame, text="Use target drive as repository base", + variable=self.use_target_drive, command=self.toggle_target_drive).pack(side=tk.LEFT) + + ttk.Checkbutton(frame, text="Create new repository if it doesn't exist", + variable=self.create_new).grid(row=2, column=1, sticky=tk.W) + + # Passphrase section + ttk.Label(frame, text="Borg Passphrase:").grid(row=3, column=0, sticky=tk.W, pady=(10,0)) + ttk.Entry(frame, textvariable=self.passphrase, show='*', width=40).grid(row=3, column=1, sticky=tk.W, pady=(10,0)) + + # Buttons + btn_frame = ttk.Frame(frame) + btn_frame.grid(row=4, column=1, pady=15, sticky=tk.E) + ttk.Button(btn_frame, text="Cancel", command=self.destroy).pack(side=tk.RIGHT, padx=5) + ttk.Button(btn_frame, text="OK", command=self.on_ok).pack(side=tk.RIGHT) + + def toggle_target_drive(self): + """Toggle between manual path and target drive path""" + if self.use_target_drive.get(): + # Get target drive from parent GUI + target_vg = self.parent_gui.target_vg.get() + if target_vg and target_vg != "Select Volume Group": + vg_name = target_vg.split()[0] + # Mount target drive and use it as base path + try: + # Create mount point + mount_point = f"/mnt/{vg_name}-borg" + subprocess.run(["mkdir", "-p", mount_point], check=True) + subprocess.run(["mount", f"/dev/{vg_name}/home", mount_point], check=True) + self.repo_path.set(f"{mount_point}/borg-repo") + except: + messagebox.showwarning("Warning", "Could not mount target drive. Please select path manually.") + self.use_target_drive.set(False) + else: + messagebox.showwarning("Warning", "Please select a target drive first in the main window.") + self.use_target_drive.set(False) + + def browse_repo(self): + if self.use_target_drive.get(): + messagebox.showinfo("Info", "Path is set to target drive. Uncheck the option to browse manually.") + return + path = filedialog.askdirectory(title="Select Borg Repository Location") + if path: + self.repo_path.set(path) + + def on_ok(self): + repo = self.repo_path.get().strip() + pw = self.passphrase.get().strip() + create = self.create_new.get() + if not repo: + messagebox.showerror("Error", "Please select a repository path.") + return + + # Save settings for next time + self.parent_gui.settings["last_borg_repo"] = repo + self.parent_gui.settings["last_passphrase"] = pw + self.parent_gui.settings["create_new_default"] = create + self.parent_gui.save_settings() + + self.callback(repo, pw, create) + self.destroy() + +class LVMBackupGUI(tk.Tk): + def __init__(self, root=None): + super().__init__() + self.title("LVM Backup Manager") + self.geometry("900x700") + self.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 + + # Settings file path + self.settings_file = os.path.expanduser("~/.lvm_backup_gui_settings.json") + self.settings = self.load_settings() + + # Apply saved geometry + if "window_geometry" in self.settings: + try: + self.geometry(self.settings["window_geometry"]) + except: + pass # Use default if geometry is invalid + + # Save settings on window close + self.protocol("WM_DELETE_WINDOW", self.on_closing) + + # Thread-safe logging queue + self.log_queue = queue.Queue() + + # Create GUI + self.create_widgets() + self.refresh_drives() + + # Start queue processing + self.process_log_queue() + + 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 load_settings(self): + """Load settings from file""" + default_settings = { + "last_borg_repo": "", + "last_passphrase": "", + "create_new_default": True, + "window_geometry": "900x700" + } + + try: + if os.path.exists(self.settings_file): + with open(self.settings_file, 'r') as f: + settings = json.load(f) + # Merge with defaults for any missing keys + for key, value in default_settings.items(): + if key not in settings: + settings[key] = value + return settings + except Exception as e: + self.log(f"Could not load settings: {e}") + + return default_settings + + def save_settings(self): + """Save current settings to file""" + try: + with open(self.settings_file, 'w') as f: + json.dump(self.settings, f, indent=2) + except Exception as e: + self.log(f"Could not save settings: {e}") + + def update_window_geometry(self): + """Update stored window geometry""" + self.settings["window_geometry"] = self.geometry() + self.save_settings() + """Process messages from background threads""" + try: + while True: + message = self.log_queue.get_nowait() + self.log_text.insert(tk.END, f"[{datetime.datetime.now().strftime('%H:%M:%S')}] {message}\n") + self.log_text.see(tk.END) + self.update_idletasks() + except queue.Empty: + pass + # Schedule next check + self.after(100, self.process_log_queue) + + def thread_log(self, message): + """Thread-safe logging method""" + self.log_queue.put(message) + + def process_log_queue(self): + """Process messages from background threads""" + try: + while True: + message = self.log_queue.get_nowait() + self.log_text.insert(tk.END, f"[{datetime.datetime.now().strftime('%H:%M:%S')}] {message}\n") + self.log_text.see(tk.END) + self.update_idletasks() + except queue.Empty: + pass + # Schedule next check + self.after(100, self.process_log_queue) + + def load_settings(self): + """Load settings from file""" + default_settings = { + "last_borg_repo": "", + "last_passphrase": "", + "create_new_default": True, + "window_geometry": "900x700" + } + + try: + if os.path.exists(self.settings_file): + with open(self.settings_file, 'r') as f: + settings = json.load(f) + # Merge with defaults for any missing keys + for key, value in default_settings.items(): + if key not in settings: + settings[key] = value + return settings + except Exception as e: + print(f"Could not load settings: {e}") + + return default_settings + + def save_settings(self): + """Save current settings to file""" + try: + with open(self.settings_file, 'w') as f: + json.dump(self.settings, f, indent=2) + except Exception as e: + print(f"Could not save settings: {e}") + + def update_window_geometry(self): + """Update stored window geometry""" + self.settings["window_geometry"] = self.geometry() + self.save_settings() + + def on_closing(self): + """Handle window closing""" + self.update_window_geometry() + self.destroy() + + def create_widgets(self): + """Create the main GUI interface""" + + # Main container with padding + main_frame = ttk.Frame(self, padding="20") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # Configure grid weights + self.columnconfigure(0, weight=1) + self.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('<>', lambda e: self.on_selection_change(var, combo)) + combo.bind('', lambda e: self.on_combobox_click(combo)) + combo.bind('', lambda e: self.on_combo_clicked(e, var)) + + 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, padx=(0, 10)) + + borg_btn = ttk.Button(frame, text="Borg Backup", command=self.open_borg_config) + borg_btn.pack(side=tk.LEFT, padx=5) + + 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 = tk.Text(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 - thread-safe""" + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + log_message = f"[{timestamp}] {message}" + self.log_queue.put(log_message) + + 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 + + # Set default selection for the first available VG if none selected + if vgs and not self.source_vg.get(): + self.source_combo.set(vgs[0]) + self.source_vg.set(vgs[0]) + + self.log(f"DEBUG: After refresh - source_vg = '{self.source_vg.get()}'") + self.log(f"DEBUG: Available VGs: {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 on_combo_clicked(self, event, var): + """Handle combobox click events""" + self.log(f"DEBUG: Combo clicked, current value: '{var.get()}'") + + def on_combo_selected(self, event, var): + """Handle combobox selection events""" + widget = event.widget + selection = widget.get() + var.set(selection) + self.log(f"DEBUG: Combo selected: '{selection}', var now: '{var.get()}'") + self.update_drive_info() + + def on_selection_change(self, var, combo): + """Handle combobox selection change""" + selected = combo.get() + var.set(selected) + self.log(f"DEBUG: Selected {selected} for {var}") + self.update_drive_info() + + def on_combobox_click(self, combo): + """Handle combobox click to ensure it's focused""" + combo.focus_set() + + 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 (handle German locale with comma) + if size_str[-1].upper() in multipliers: + number_str = size_str[:-1].replace(',', '.') + number = float(number_str) + unit = size_str[-1].upper() + else: + number_str = size_str.replace(',', '.') + number = float(number_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(datetime.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... (Check external drive LED)") + + # 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}") + + # Create backup script content directly + script_content = f'''#!/bin/bash +set -e + +SOURCE_VG="{source_vg}" +TARGET_VG="{target_vg}" +SNAPSHOT_SIZE="2G" + +log() {{ + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" +}} + +# Clean 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 + +log "Creating LVM snapshots..." +lvcreate -L $SNAPSHOT_SIZE -s -n root-backup-snap $SOURCE_VG/root +lvcreate -L $SNAPSHOT_SIZE -s -n home-backup-snap $SOURCE_VG/home +lvcreate -L 1G -s -n boot-backup-snap $SOURCE_VG/boot +log "SUCCESS Snapshots created" + +log "Unmounting target volumes..." +umount /dev/$TARGET_VG/home 2>/dev/null || true +umount /dev/$TARGET_VG/root 2>/dev/null || true +umount /dev/$TARGET_VG/boot 2>/dev/null || true + +log "Cloning root volume..." +dd if=/dev/$SOURCE_VG/root-backup-snap of=/dev/$TARGET_VG/root bs=64M status=progress 2>&1 +log "SUCCESS Root volume cloned" + +log "Cloning home volume..." +dd if=/dev/$SOURCE_VG/home-backup-snap of=/dev/$TARGET_VG/home bs=64M status=progress 2>&1 +log "SUCCESS Home volume cloned" + +log "Cloning boot volume..." +dd if=/dev/$SOURCE_VG/boot-backup-snap of=/dev/$TARGET_VG/boot bs=64M status=progress 2>&1 +log "SUCCESS Boot volume cloned" + +log "Cleaning up snapshots..." +lvremove -f $SOURCE_VG/root-backup-snap +lvremove -f $SOURCE_VG/home-backup-snap +lvremove -f $SOURCE_VG/boot-backup-snap +log "SUCCESS Backup completed successfully!" +''' + + # Write script to temp file + import tempfile + with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f: + f.write(script_content) + script_path = f.name + + # Make script executable + os.chmod(script_path, 0o755) + + # Update UI - starting + pass # UI update disabled (threading) + + # Execute the backup script + self.backup_process = subprocess.Popen( + ['sudo', 'bash', script_path], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + universal_newlines=True, + bufsize=1 + ) + + # Monitor output in real-time + for line in iter(self.backup_process.stdout.readline, ''): + if not self.backup_running: + break + + line = line.strip() + if line: + # Update UI from main thread + self.log(l) + + # Update progress based on output + if 'SUCCESS' in line: + if 'Snapshots created' in line: + pass # UI update disabled (threading) + pass # UI update disabled (threading) + elif 'Root volume cloned' in line: + pass # UI update disabled (threading) + pass # UI update disabled (threading) + elif 'Home volume cloned' in line: + pass # UI update disabled (threading) + pass # UI update disabled (threading) + elif 'Boot volume cloned' in line: + pass # UI update disabled (threading) + pass # UI update disabled (threading) + elif 'Backup completed' in line: + pass # UI update disabled (threading) + pass # UI update disabled (threading) + elif 'Cloning' in line: + if 'root' in line.lower(): + pass # UI update disabled (threading) + pass # UI update disabled (threading) + elif 'home' in line.lower(): + pass # UI update disabled (threading) + pass # UI update disabled (threading) + elif 'boot' in line.lower(): + pass # UI update disabled (threading) + pass # UI update disabled (threading) + elif any(unit in line for unit in ['MB/s', 'GB/s']): + # Parse dd progress for speed + speed_match = re.search(r'(\d+(?:\.\d+)?)\s*(MB|GB)/s', line) + if speed_match: + speed = speed_match.group(1) + unit = speed_match.group(2) + pass # UI update disabled (threading) + + # Wait for process to complete + return_code = self.backup_process.wait() + + # Clean up temp script + try: + os.unlink(script_path) + except: + pass + + # Update UI based on result + success = return_code == 0 + pass # backup_finished disabled (threading) + + except Exception as e: + error_msg = f"❌ Backup failed: {str(e)}" + self.log(error_msg) + pass # backup_finished disabled (threading) + self.monitor_backup_progress() + + except Exception as e: + self.log(f"Backup failed: {e}") + self.log("Backup failed or was interrupted") + + 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: + # Show disk activity in log for dd progress + self.log(f"💾⚡ {line} (External drive LED should be flashing)") + self.parse_dd_progress(line) + elif 'SUCCESS' in line: + if 'Root volume cloned' in line: + pass # Widget update disabled (threading) + pass # Widget update disabled (threading) + elif 'Home volume cloned' in line: + pass # Widget update disabled (threading) + pass # Widget update disabled (threading) + elif 'Boot volume cloned' in line: + pass # Widget update disabled (threading) + pass # Widget update disabled (threading) + elif 'Cloning' in line: + if 'root' in line.lower(): + pass # Widget update disabled (threading) + elif 'home' in line.lower(): + pass # Widget update disabled (threading) + elif 'boot' in line.lower(): + pass # Widget update disabled (threading) + + # Check if process is still running + if self.backup_process.poll() is None: + # Schedule next check + self.after(100, self.monitor_backup_progress) + else: + # Process finished + success = self.backup_process.returncode == 0 + self.log("Backup failed or was interrupted") + + except Exception as e: + self.log(f"Error monitoring progress: {e}") + self.log("Backup failed or was interrupted") + + 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 open_borg_config(self): + """Open Borg repository configuration dialog""" + # Check if source is selected + source_vg = self.source_vg.get() + self.log(f"DEBUG: source_vg value = '{source_vg}'") + self.log(f"DEBUG: source_combo current = '{self.source_combo.get()}'") + + # Try to get value directly from combobox if StringVar is empty + if not source_vg and self.source_combo.get(): + source_vg = self.source_combo.get() + self.source_vg.set(source_vg) + self.log(f"DEBUG: Fixed source_vg to '{source_vg}'") + + if not source_vg or source_vg == "Select Volume Group": + messagebox.showerror("Error", "Please select a source drive first.") + return + BorgConfigDialog(self, self.start_borg_backup_with_config) + + def start_borg_backup_with_config(self, repo_path, passphrase, create_new): + """Start Borg backup with configuration from dialog""" + self.log(f"Starting Borg backup: repo={repo_path}, create_new={create_new}") + self.borg_repo_path = repo_path + self.borg_passphrase = passphrase + self.borg_create_new = create_new + + # Start the backup process + threading.Thread(target=self._run_borg_backup, args=(repo_path, passphrase, create_new), daemon=True).start() + + def start_borg_backup(self): + """Start Borg backup of LVM snapshots to Nextcloud""" + + # Check if borg is installed + try: + subprocess.run(['borg', '--version'], capture_output=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + messagebox.showerror("Borg Not Found", + "BorgBackup is not installed.\n\n" + + "Install with: sudo apt install borgbackup") + return + + # Check if snapshots exist + source_vg = self.source_vg.get().split()[0] + if not source_vg: + messagebox.showerror("Error", "Please select source drive first") + return + + snapshots = self.check_snapshots_exist(source_vg) + if not snapshots: + # Create snapshots if they don't exist + response = messagebox.askyesno("Create Snapshots", + "No snapshots found. Create them for Borg backup?") + if response: + self.create_snapshots_for_borg(source_vg) + else: + return + + # Setup Borg repository + self.setup_borg_repo() + + # Start Borg backup in thread + thread = threading.Thread(target=self.run_borg_backup, args=(source_vg,)) + thread.daemon = True + thread.start() + + def check_snapshots_exist(self, vg_name): + """Check if snapshots exist for Borg backup""" + try: + result = subprocess.run(['lvs', vg_name], capture_output=True, text=True) + snapshots = [] + for line in result.stdout.split('\n'): + if 'backup-snap' in line: + snapshots.append(line.split()[0]) + return snapshots + except: + return [] + + def create_snapshots_for_borg(self, vg_name): + """Create snapshots specifically for Borg backup""" + self.log("🔄 Creating snapshots for Borg backup...") + + volumes = ['root', 'home', 'boot'] + for vol in volumes: + try: + cmd = ['lvcreate', '-L', '2G', '-s', '-n', f'{vol}-backup-snap', f'{vg_name}/{vol}'] + subprocess.run(cmd, check=True, capture_output=True) + self.log(f"✅ Created snapshot: {vol}-backup-snap") + except subprocess.CalledProcessError as e: + # Remove if exists and try again + subprocess.run(['lvremove', '-f', f'{vg_name}/{vol}-backup-snap'], + capture_output=True) + try: + subprocess.run(cmd, check=True, capture_output=True) + self.log(f"✅ Created snapshot: {vol}-backup-snap") + except: + self.log(f"❌ Failed to create snapshot: {vol}-backup-snap") + + def setup_borg_repo(self): + """Setup Borg repository in Nextcloud""" + repo_path = "/home/rwiegand/Nextcloud/backups/borg-repo" + + # Create directory if it doesn't exist + os.makedirs(repo_path, exist_ok=True) + + # Check if repo is initialized + if not os.path.exists(os.path.join(repo_path, "README")): + self.log("🔧 Initializing Borg repository...") + + # Prompt for passphrase + from tkinter import simpledialog + passphrase = simpledialog.askstring("Borg Passphrase", + "Enter passphrase for Borg repository:", + show='*') + if not passphrase: + messagebox.showerror("Error", "Passphrase required for Borg repository") + return False + + # Initialize repo + env = os.environ.copy() + env['BORG_PASSPHRASE'] = passphrase + + try: + subprocess.run(['borg', 'init', '--encryption=repokey', repo_path], + env=env, check=True, capture_output=True) + self.log("✅ Borg repository initialized") + return True + except subprocess.CalledProcessError as e: + self.log(f"❌ Failed to initialize Borg repo: {e}") + return False + else: + self.log("✅ Borg repository already exists") + return True + + def _run_borg_backup(self, repo_path, passphrase, create_new): + # Check if source is selected + source_vg = self.source_vg.get() + if not source_vg or source_vg == "Select Volume Group": + self.thread_log("Please select a source drive first") + return + + self.thread_log(f"Starting Borg backup from {source_vg} to: {repo_path}") + + # Set up environment for Borg + env = os.environ.copy() + if passphrase: + env['BORG_PASSPHRASE'] = passphrase + + # Create temporary snapshot for backup + vg_name = source_vg.split()[0] # Extract VG name from "vg-name (size info)" + snapshot_name = "borg-backup-persistent" # Use persistent name + snapshot_lv = f"/dev/{vg_name}/{snapshot_name}" + + # Debug: Show VG structure + try: + vg_info = subprocess.run(['vgs', '--noheadings', '-o', 'vg_name,vg_size,vg_free', vg_name], + capture_output=True, text=True, check=True) + self.thread_log(f"VG Info: {vg_info.stdout.strip()}") + + lv_info = subprocess.run(['lvs', '--noheadings', '-o', 'lv_name,lv_size', vg_name], + capture_output=True, text=True, check=True) + self.thread_log(f"LVs in {vg_name}:") + for line in lv_info.stdout.strip().split('\n'): + if line.strip(): + self.thread_log(f" {line.strip()}") + except: + pass + + # Clean up any existing backup snapshots to free space + self.thread_log("Cleaning up existing backup snapshots...") + try: + existing_snaps = subprocess.run(['lvs', '--noheadings', '-o', 'lv_name', vg_name], + capture_output=True, text=True, check=True) + freed_space = 0 + for line in existing_snaps.stdout.strip().split('\n'): + lv_name = line.strip() + if ('backup-snap' in lv_name or 'borg-backup' in lv_name) and lv_name != snapshot_name: + try: + subprocess.run(['lvremove', '-f', f'/dev/{vg_name}/{lv_name}'], + capture_output=True, check=True) + self.thread_log(f"Removed old snapshot: {lv_name}") + freed_space += 1 + except subprocess.CalledProcessError: + pass + if freed_space > 0: + self.thread_log(f"Freed up space by removing {freed_space} old snapshots") + except subprocess.CalledProcessError: + pass + + # Check if persistent snapshot already exists + snapshot_exists = False + existing_snapshot_path = None + try: + result = subprocess.run(['lvs', snapshot_lv], capture_output=True, check=True) + # Check snapshot size - if it's less than 8GB, remove it for recreation + size_result = subprocess.run(['lvs', '--noheadings', '-o', 'lv_size', '--units', 'g', snapshot_lv], + capture_output=True, text=True, check=True) + size_gb = float(size_result.stdout.strip().replace('g', '').replace(',', '.')) + + if size_gb < 8.0: # Less than 8GB - too small for disaster recovery + self.thread_log(f"Existing snapshot too small ({size_gb:.1f}GB) - removing for multi-LV backup") + subprocess.run(['lvremove', '-f', snapshot_lv], capture_output=True, check=True) + snapshot_exists = False + else: + snapshot_exists = True + existing_snapshot_path = snapshot_lv + self.thread_log(f"Found existing persistent snapshot: {snapshot_lv} ({size_gb:.1f}GB)") + # If we have large existing snapshot, we'll use it as single source for now + backup_sources = [snapshot_lv] + created_snapshots = [snapshot_name] + except subprocess.CalledProcessError: + pass + + # Clean up OTHER backup snapshots (but preserve our persistent one if it exists) + if not snapshot_exists: # Only cleanup if we need to create a new snapshot + self.thread_log("Cleaning up old backup snapshots to free space...") + try: + existing_snaps = subprocess.run(['lvs', '--noheadings', '-o', 'lv_name', vg_name], + capture_output=True, text=True, check=True) + removed_count = 0 + for line in existing_snaps.stdout.strip().split('\n'): + lv_name = line.strip() + # Remove old backup snapshots but NOT our persistent snapshot + if lv_name and ('backup-snap' in lv_name or lv_name.endswith('-snap')) and lv_name != snapshot_name: + try: + subprocess.run(['lvremove', '-f', f'/dev/{vg_name}/{lv_name}'], + capture_output=True, check=True) + self.thread_log(f"Removed old snapshot: {lv_name}") + removed_count += 1 + except subprocess.CalledProcessError: + pass + if removed_count > 0: + self.thread_log(f"Freed up space by removing {removed_count} old snapshots") + except subprocess.CalledProcessError: + self.thread_log("Could not list existing snapshots") + + # Initialize variables for multi-LV backup (only if no existing snapshot) + if not snapshot_exists: + backup_sources = [] + created_snapshots = [] + + if not snapshot_exists: + try: + # Create snapshot of root volume with dynamic sizing + self.thread_log("Creating temporary snapshot for Borg backup...") + + # Get available space in VG + vg_result = subprocess.run(['vgs', '--noheadings', '-o', 'vg_free', '--units', 'm', vg_name], + capture_output=True, text=True, check=True) + free_mb = float(vg_result.stdout.strip().replace('m', '').replace(',', '.')) + + # Find the largest LV to determine appropriate snapshot size + lvs_result = subprocess.run(['lvs', '--noheadings', '-o', 'lv_name,lv_size', '--units', 'b', vg_name], + capture_output=True, text=True, check=True) + + largest_lv = None + largest_size = 0 + + for line in lvs_result.stdout.strip().split('\n'): + if line.strip(): + parts = line.strip().split() + if len(parts) >= 2: + lv_name = parts[0] + lv_size = int(parts[1].replace('B', '')) + if lv_size > largest_size: + largest_size = lv_size + largest_lv = lv_name + + if not largest_lv: + self.thread_log("No logical volumes found") + return + + largest_lv_size_gb = largest_size / (1024**3) + + # Use larger snapshot sizes for big LVs + if largest_lv_size_gb >= 100: # 100GB+ + if free_mb >= 10240: # 10GB available + snapshot_size = "10G" + elif free_mb >= 5120: # 5GB available + snapshot_size = "5G" + elif free_mb >= 2048: # 2GB available + snapshot_size = "2G" + elif free_mb >= 1024: # 1GB available + snapshot_size = "1G" + else: + self.thread_log(f"Not enough free space for large LV snapshot. Available: {free_mb:.0f}MB, need at least 1GB") + return + else: + # For smaller LVs, use the original logic + if free_mb >= 2048: + snapshot_size = "2G" + elif free_mb >= 1024: + snapshot_size = "1G" + elif free_mb >= 512: + snapshot_size = "512M" + elif free_mb >= 256: + snapshot_size = "256M" + else: + self.thread_log(f"Not enough free space for snapshot. Available: {free_mb:.0f}MB, need at least 256MB") + return + + self.thread_log(f"Creating {snapshot_size} snapshot (available: {free_mb:.0f}MB)") + + # Backup ALL important LVs for complete disaster recovery + important_lvs = [] + total_lv_size = 0 + + for line in lvs_result.stdout.strip().split('\n'): + if line.strip(): + parts = line.strip().split() + if len(parts) >= 2: + lv_name = parts[0] + lv_size = int(parts[1].replace('B', '')) + + # Include all LVs except snapshots and swap + if ('snap' not in lv_name.lower() and 'swap' not in lv_name.lower() and + lv_size > 100 * 1024 * 1024): # > 100MB + important_lvs.append((lv_name, lv_size)) + total_lv_size += lv_size + + if not important_lvs: + self.thread_log("No suitable logical volumes found for backup") + return + + self.thread_log(f"Complete disaster recovery backup - {len(important_lvs)} LVs:") + for lv_name, lv_size in important_lvs: + self.thread_log(f" {lv_name}: {lv_size / (1024**3):.1f}GB") + self.thread_log(f"Total data: {total_lv_size / (1024**3):.1f}GB") + + # Calculate snapshot requirements + snapshots_needed = [] + total_snapshot_mb = 0 + + # Check if we have enough space for minimum viable backup + min_required_mb = len(important_lvs) * 256 # Absolute minimum 256MB per LV + available_mb = free_mb * 0.95 + + if available_mb < min_required_mb: + self.thread_log(f"❌ Insufficient space for disaster recovery backup!") + self.thread_log(f" Available: {available_mb:.0f}MB") + self.thread_log(f" Minimum required: {min_required_mb}MB") + self.thread_log(f"💡 Suggestions:") + self.thread_log(f" • Free up at least {min_required_mb - available_mb:.0f}MB in volume group") + self.thread_log(f" • Use external storage for backup") + self.thread_log(f" • Consider file-level backup instead of block-level") + return + + for lv_name, lv_size in important_lvs: + # Use enhanced snapshot sizing with space-aware allocation + lv_size_gb = lv_size / (1024**3) + + # Calculate proportional allocation of available space + remaining_lvs = len(important_lvs) - len(snapshots_needed) + if remaining_lvs > 0: + remaining_space = available_mb - total_snapshot_mb + proportional_share = remaining_space / remaining_lvs + + # Apply intelligent sizing with space constraints + if lv_size_gb > 300: # Large partitions - need adequate space but respect limits + min_viable = 1024 # 1GB minimum for large partitions + max_reasonable = min(8192, remaining_space * 0.7) # Max 8GB or 70% of remaining space + + if remaining_space >= min_viable and proportional_share >= min_viable: + snap_mb = int(min(proportional_share, max_reasonable)) + elif remaining_space >= min_viable: + snap_mb = min_viable + else: + # Critical space shortage - need to trigger home shrinking later + snap_mb = max(256, int(remaining_space * 0.9)) + + elif lv_size_gb > 50: + min_viable = 512 # 512MB minimum for medium partitions + max_reasonable = min(2048, remaining_space * 0.5) + snap_mb = max(min_viable, int(min(proportional_share, max_reasonable))) + + else: + min_viable = 256 # 256MB minimum for small partitions + max_reasonable = min(1024, remaining_space * 0.3) + snap_mb = max(min_viable, int(min(proportional_share, max_reasonable))) + + # Convert to size string + if snap_mb >= 1024: + snap_size = f"{snap_mb // 1024}G" + else: + snap_size = f"{snap_mb}M" + else: + # Fallback + snap_size = "256M" + snap_mb = 256 + + snapshots_needed.append((lv_name, lv_size, snap_size, snap_mb)) + total_snapshot_mb += snap_mb + + # Check if we need more space and can temporarily shrink home LV + shrunk_home = False + original_home_size_gb = None + + # Final space check and warnings + self.thread_log(f"📊 Total snapshots needed: {total_snapshot_mb}MB ({total_snapshot_mb/1024:.1f}GB)") + self.thread_log(f"📊 Available space: {free_mb}MB ({free_mb/1024:.1f}GB)") + + if total_snapshot_mb > free_mb: + space_shortage_mb = total_snapshot_mb - free_mb + self.thread_log(f"⚠️ Need {space_shortage_mb}MB ({space_shortage_mb/1024:.1f}GB) more space for snapshots") + + # Check if space is critically low + if free_mb < 5120: # Less than 5GB available - raised threshold + self.thread_log(f"🚨 CRITICAL: Low free space ({free_mb}MB)!") + self.thread_log(f" Large home partition (404GB) needs substantial snapshot space") + + # For large home partitions, aggressive shrinking is needed + minimum_needed_gb = max(15, total_snapshot_mb // 1024 + 8) # At least 15GB or calculated need + 8GB buffer + self.thread_log(f" Minimum {minimum_needed_gb}GB needed for viable snapshots of large partitions") + + # Check if we can shrink home LV temporarily (more aggressive for large home) + home_found = False + for lv_name, lv_size in important_lvs: + if lv_name == 'home': + home_found = True + home_size_gb = lv_size // (1024**3) + self.thread_log(f"🏠 Home LV found: {home_size_gb}GB") + + if lv_size > 30 * (1024**3): # home > 30GB + # For very large home partitions (>400GB), be very aggressive + if home_size_gb > 400: + base_shrink = 40 # Start with 40GB shrink for huge home + max_shrink = 80 # Up to 80GB max + elif home_size_gb > 200: + base_shrink = 25 # 25GB for large home + max_shrink = 50 + else: + base_shrink = 15 # 15GB for medium home + max_shrink = 30 + + if free_mb < 5120: # Very low space - aggressive shrinking + shrink_gb = min(max_shrink, max(base_shrink, (space_shortage_mb + 8192) // 1024)) # Add 8GB buffer + else: + shrink_gb = min(max_shrink // 2, (space_shortage_mb + 2048) // 1024) # Normal: Add 2GB buffer + + self.thread_log(f"🔧 Attempting to temporarily shrink {home_size_gb}GB home LV by {shrink_gb}GB...") + + try: + original_home_size_gb = home_size_gb + new_size_gb = original_home_size_gb - shrink_gb + + if new_size_gb < 50: # Safety check - don't go below 50GB + self.thread_log(f"⚠️ Cannot shrink home below 50GB (would be {new_size_gb}GB)") + continue + + self.thread_log(f"📝 Home LV: {original_home_size_gb}GB → {new_size_gb}GB (temporary for backup)") + + # Shrink the LV (reduce logical volume size, not filesystem) + cmd = ["lvreduce", "-L", f"{new_size_gb}G", f"/dev/{vg_name}/home", "--yes"] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + shrunk_home = True + self.thread_log(f"✅ Home LV temporarily shrunk from {original_home_size_gb}GB to {new_size_gb}GB") + self.thread_log(f"🔓 This freed up {shrink_gb}GB for larger snapshots") + + # Recalculate free space and snapshots + vgs_result = subprocess.run(['vgs', '--noheadings', '-o', 'vg_free', '--units', 'b', vg_name], + capture_output=True, text=True, check=True) + new_free_mb = int(vgs_result.stdout.strip().replace('B', '')) // (1024*1024) + self.thread_log(f"📊 New available space: {new_free_mb}MB ({new_free_mb/1024:.1f}GB)") + + # Update the free space for the rest of the calculation + free_mb = new_free_mb + + # Recalculate snapshot sizes with much more generous allocation + self.thread_log(f"🔄 Recalculating snapshot allocation with {shrink_gb}GB additional space...") + snapshots_needed = [] + total_snapshot_mb = 0 + + for lv_name2, lv_size2 in important_lvs: + lv_size_gb2 = lv_size2 // (1024**3) + + # Much more generous allocation with freed space + if lv_name2 == 'home': + # For large home, allocate much more space + snap_mb = min(new_free_mb * 0.75, 20480) # Up to 75% or 20GB max for home + self.thread_log(f"🏠 Home gets generous allocation: {snap_mb}MB ({snap_mb/1024:.1f}GB)") + elif lv_name2 == 'root': + snap_mb = min(new_free_mb * 0.15, 4096) # Up to 15% or 4GB for root + else: + snap_mb = min(new_free_mb * 0.1, 2048) # Up to 10% or 2GB for others + + snap_size = snap_mb * (1024*1024) + self.thread_log(f"📝 {lv_name2}: allocated {snap_mb}MB ({snap_mb/1024:.1f}GB) snapshot") + snapshots_needed.append((lv_name2, lv_size2, snap_size, snap_mb)) + total_snapshot_mb += snap_mb + + break + + except subprocess.CalledProcessError as e: + self.thread_log(f"⚠️ Could not shrink home LV: {e.stderr.decode() if e.stderr else str(e)}") + self.thread_log("Proceeding with available space...") + else: + self.thread_log(f"⚠️ Cannot shrink home LV (size: {home_size_gb}GB, minimum 30GB required)") + break + + if not home_found: + self.thread_log(f"⚠️ No home LV found for shrinking") + + # Special handling for large home partitions - always check if more space needed + has_large_home = False + for lv_name, lv_size in important_lvs: + if lv_name == 'home' and lv_size > 400 * (1024**3): # 400GB+ home + has_large_home = True + self.thread_log(f"🏠 Large home partition detected: {lv_size // (1024**3)}GB") + break + + # For large home partitions, we need more space even if basic calculation fits + space_adequate = True + if has_large_home and free_mb < 15360: # Less than 15GB for large home + space_adequate = False + self.thread_log(f"🚨 Large home partition needs more snapshot space (have {free_mb}MB, recommend 15GB+)") + + # Final space validation (enhanced for large partitions) + if total_snapshot_mb > free_mb or not space_adequate: + if total_snapshot_mb <= free_mb: + # We have basic space but not adequate for large partitions + space_shortage_mb = 15360 - free_mb # Need 15GB for large home + self.thread_log(f"⚠️ Basic space available but need {space_shortage_mb}MB ({space_shortage_mb/1024:.1f}GB) more for large partition reliability") + else: + # We don't even have basic space + space_shortage_mb = total_snapshot_mb - free_mb + self.thread_log(f"⚠️ Need {space_shortage_mb}MB ({space_shortage_mb/1024:.1f}GB) more space for snapshots") + + # Try to shrink home LV to create adequate space + shrinking_attempted = False + self.thread_log(f"🔍 DEBUG: Starting shrinking attempt loop") + for lv_name, lv_size in important_lvs: + self.thread_log(f"🔍 DEBUG: Checking LV {lv_name}, size {lv_size // (1024**3)}GB") + if lv_name == 'home' and lv_size > 30 * (1024**3): # home > 30GB + shrinking_attempted = True + home_size_gb = lv_size // (1024**3) + self.thread_log(f"🏠 Home LV found: {home_size_gb}GB") + + # For very large home partitions (>400GB), be very aggressive + if home_size_gb > 400: + base_shrink = 40 # Start with 40GB shrink for huge home + max_shrink = 80 # Up to 80GB max + elif home_size_gb > 200: + base_shrink = 25 # 25GB for large home + max_shrink = 50 + else: + base_shrink = 15 # 15GB for medium home + max_shrink = 30 + + # Calculate shrink amount based on need + if not space_adequate: # Large partition needs more space + shrink_gb = min(max_shrink, max(base_shrink, (space_shortage_mb + 5120) // 1024)) # Add 5GB buffer + else: + shrink_gb = min(max_shrink // 2, (space_shortage_mb + 2048) // 1024) # Normal case + + self.thread_log(f"🔧 Attempting to temporarily shrink {home_size_gb}GB home LV by {shrink_gb}GB...") + + try: + original_home_size_gb = home_size_gb + new_size_gb = original_home_size_gb - shrink_gb + + if new_size_gb < 50: # Safety check - don't go below 50GB + self.thread_log(f"⚠️ Cannot shrink home below 50GB (would be {new_size_gb}GB)") + break + + self.thread_log(f"📝 Home LV: {original_home_size_gb}GB → {new_size_gb}GB (temporary for backup)") + + # Shrink the LV (reduce logical volume size, not filesystem) + cmd = ["lvreduce", "-L", f"{new_size_gb}G", f"/dev/{vg_name}/home", "--yes"] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + + shrunk_home = True + self.thread_log(f"✅ Home LV temporarily shrunk from {original_home_size_gb}GB to {new_size_gb}GB") + self.thread_log(f"🔓 This freed up {shrink_gb}GB for larger snapshots") + + # Recalculate free space and snapshots + vgs_result = subprocess.run(['vgs', '--noheadings', '-o', 'vg_free', '--units', 'b', vg_name], + capture_output=True, text=True, check=True) + new_free_mb = int(vgs_result.stdout.strip().replace('B', '')) // (1024*1024) + self.thread_log(f"📊 New available space: {new_free_mb}MB ({new_free_mb/1024:.1f}GB)") + + # Update the free space for the rest of the calculation + free_mb = new_free_mb + + # Recalculate snapshot sizes with much more generous allocation + self.thread_log(f"🔄 Recalculating snapshot allocation with {shrink_gb}GB additional space...") + snapshots_needed = [] + total_snapshot_mb = 0 + + for lv_name2, lv_size2 in important_lvs: + lv_size_gb2 = lv_size2 // (1024**3) + + # Much more generous allocation with freed space + if lv_name2 == 'home': + # For large home, allocate much more space + snap_mb = min(new_free_mb * 0.75, 20480) # Up to 75% or 20GB max for home + self.thread_log(f"🏠 Home gets generous allocation: {snap_mb}MB ({snap_mb/1024:.1f}GB)") + elif lv_name2 == 'root': + snap_mb = min(new_free_mb * 0.15, 4096) # Up to 15% or 4GB for root + else: + snap_mb = min(new_free_mb * 0.1, 2048) # Up to 10% or 2GB for others + + snap_size = snap_mb * (1024*1024) + self.thread_log(f"📝 {lv_name2}: allocated {snap_mb}MB ({snap_mb/1024:.1f}GB) snapshot") + snapshots_needed.append((lv_name2, lv_size2, snap_size, snap_mb)) + total_snapshot_mb += snap_mb + + break + + except subprocess.CalledProcessError as e: + self.thread_log(f"⚠️ Could not shrink home LV: {e.stderr.decode() if e.stderr else str(e)}") + self.thread_log("Proceeding with available space...") + shrunk_home = False + break # Only break after processing home LV + + # If shrinking wasn't attempted or failed, show final error + if not shrinking_attempted or not shrunk_home: + shortage = total_snapshot_mb - free_mb if total_snapshot_mb > free_mb else space_shortage_mb + self.thread_log(f"❌ Could not create adequate snapshot space.") + self.thread_log(f"📊 Need: {total_snapshot_mb}MB ({total_snapshot_mb/1024:.1f}GB)") + self.thread_log(f"📊 Have: {free_mb}MB ({free_mb/1024:.1f}GB)") + if shortage > 0: + self.thread_log(f"📊 Short: {shortage}MB ({shortage/1024:.1f}GB)") + self.thread_log(f"💡 Consider freeing up more space in volume group or reducing system activity") + return + + self.thread_log(f"✅ Space validation passed: {total_snapshot_mb}MB needed, {free_mb}MB available") + + # Show space allocation summary + self.thread_log(f"📊 Snapshot space allocation: {total_snapshot_mb}MB of {free_mb}MB available ({total_snapshot_mb/free_mb*100:.1f}%)") + + # Create snapshots for all LVs + created_snapshots = [] + backup_sources = [] + + for lv_name, lv_size, snap_size, snap_mb in snapshots_needed: + snap_name = f"borg-backup-{lv_name}" + snap_path = f"/dev/{vg_name}/{snap_name}" + + try: + cmd = ["lvcreate", "-L", snap_size, "-s", "-n", snap_name, f"/dev/{vg_name}/{lv_name}"] + subprocess.run(cmd, check=True, capture_output=True) + created_snapshots.append(snap_name) + backup_sources.append(snap_path) + self.thread_log(f"Created {snap_size} snapshot: {snap_name} -> {snap_path}") + + except subprocess.CalledProcessError as e: + self.thread_log(f"Failed to create snapshot for {lv_name}: {e.stderr.decode() if e.stderr else str(e)}") + # Clean up any snapshots created so far + for cleanup_snap in created_snapshots: + try: + subprocess.run(["lvremove", "-f", f"/dev/{vg_name}/{cleanup_snap}"], capture_output=True) + except: + pass + return + + self.thread_log(f"All snapshots created successfully! Ready for disaster recovery backup.") + + except subprocess.CalledProcessError as e: + self.thread_log(f"Failed to create snapshot: {e.stderr.decode() if e.stderr else str(e)}") + return + + # Get total size for verification + total_backup_size = 0 + for source in backup_sources: + try: + lv_result = subprocess.run(['lvs', '--noheadings', '-o', 'lv_size', '--units', 'b', source], + capture_output=True, text=True, check=True) + lv_size_bytes = int(lv_result.stdout.strip().replace('B', '')) + total_backup_size += lv_size_bytes + except subprocess.CalledProcessError: + pass + + total_backup_gb = total_backup_size / (1024**3) + self.thread_log(f"Disaster recovery backup: {len(backup_sources)} LVs, ~{total_backup_gb:.1f}GB total") + + # For resume capability, use date-based archive name but check for interrupted archives + today = datetime.datetime.now().strftime('%Y%m%d') + base_archive_name = f"lvm-backup-{vg_name}-{today}" + + # Check for interrupted archive from today that can be resumed + resume_archive = None + try: + list_result = subprocess.run(['borg', 'list', '--short', repo_path], env=env, capture_output=True, text=True, check=True) + archives = list_result.stdout.strip().split('\n') + + # Look for today's archive + for archive in archives: + if archive.startswith(base_archive_name): + # Check if archive is complete or interrupted + info_result = subprocess.run(['borg', 'info', f'{repo_path}::{archive}'], + env=env, capture_output=True, text=True) + if 'This archive:' in info_result.stdout: + resume_archive = archive + self.thread_log(f"Found today's archive: {archive} - will create new version") + break + except subprocess.CalledProcessError: + pass + + # Create unique archive name + if resume_archive: + # Create next version + version = 1 + while f"{base_archive_name}-v{version}" in archives: + version += 1 + archive_name = f"{base_archive_name}-v{version}" + else: + archive_name = base_archive_name + + env = os.environ.copy() + if passphrase: + env['BORG_PASSPHRASE'] = passphrase + + # Optionally initialize repo + if create_new and not os.path.exists(os.path.join(repo_path, 'config')): + self.thread_log("Initializing new Borg repository...") + init_cmd = ["borg", "init", "--encryption=repokey", repo_path] + try: + subprocess.run(init_cmd, env=env, check=True, capture_output=True) + self.thread_log("Repository initialized.") + except subprocess.CalledProcessError as e: + self.thread_log(f"Failed to initialize repo: {e.stderr.decode()}") + return + # Check if archive already exists (for resume) + try: + list_result = subprocess.run(['borg', 'list', repo_path], env=env, capture_output=True, text=True) + if archive_name in list_result.stdout: + self.thread_log(f"Archive {archive_name} exists - this will resume or create new version") + else: + self.thread_log(f"Creating new archive: {archive_name}") + except subprocess.CalledProcessError: + self.thread_log("Could not check existing archives - proceeding with backup") + # Run borg create with all snapshot sources for complete disaster recovery + borg_cmd = [ + "borg", "create", f"{repo_path}::{archive_name}" + ] + backup_sources + [ + "--progress", "--stats", "--read-special", + "--checkpoint-interval", "300", # Save checkpoint every 5 minutes + "--chunker-params", "19,23,21,4095" # Optimized for large files/devices + ] + self.thread_log(f"Running: {' '.join(borg_cmd)}") + io_error_detected = False + try: + proc = subprocess.Popen(borg_cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + for line in proc.stdout: + self.thread_log(line.strip()) + # Detect I/O errors that might indicate snapshot is too small + if "Input/output error" in line or "read: [Errno 5]" in line: + io_error_detected = True + self.thread_log("🚨 I/O error detected - snapshot may be too small for active filesystem") + + proc.wait() + if proc.returncode == 0: + self.thread_log("✅ Disaster recovery backup completed successfully!") + + # Fix ownership for Nextcloud access + self.thread_log("Fixing repository ownership for Nextcloud access...") + try: + # Get the original user (not root) + original_user = os.environ.get('SUDO_USER', 'rwiegand') + + # Change ownership of entire repository to original user + subprocess.run(["chown", "-R", f"{original_user}:{original_user}", repo_path], + capture_output=True, check=True) + self.thread_log(f"Repository ownership changed to {original_user}") + except subprocess.CalledProcessError as e: + self.thread_log(f"Warning: Could not fix ownership: {e}") + + # Only remove snapshots on successful completion + try: + self.thread_log("Removing all snapshots after successful backup...") + for snap_name in created_snapshots: + try: + subprocess.run(["lvremove", "-f", f"/dev/{vg_name}/{snap_name}"], capture_output=True, check=True) + self.thread_log(f"Removed snapshot: {snap_name}") + except subprocess.CalledProcessError as e: + self.thread_log(f"Warning: Could not remove snapshot {snap_name}: {e}") + + # Restore home LV to original size if it was shrunk + if shrunk_home and original_home_size_gb: + self.thread_log(f"🔧 Restoring home LV to original size ({original_home_size_gb}GB)...") + try: + cmd = ["lvextend", "-L", f"{original_home_size_gb}G", f"/dev/{vg_name}/home"] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + self.thread_log(f"✅ Home LV restored to {original_home_size_gb}GB") + except subprocess.CalledProcessError as e: + self.thread_log(f"⚠️ Could not restore home LV size: {e.stderr.decode() if e.stderr else str(e)}") + self.thread_log(f"You may need to manually restore with: lvextend -L {original_home_size_gb}G /dev/{vg_name}/home") + + except Exception as e: + self.thread_log(f"Warning during snapshot cleanup: {e}") + else: + if io_error_detected: + self.thread_log("❌ Backup failed due to I/O errors - likely snapshots too small") + self.thread_log("💡 Recommendation: Increase available space in volume group or reduce system activity during backup") + self.thread_log("📋 Current snapshots will be kept for manual cleanup or retry") + else: + self.thread_log("❌ Disaster recovery backup failed - keeping snapshots for resume") + except Exception as e: + self.thread_log(f"Exception: {e}") + self.thread_log("Keeping snapshot for potential resume") + + # Note: Snapshot is intentionally NOT removed on failure to allow resume + + + +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() \ No newline at end of file diff --git a/old_scripts/lvm_backup_gui_fixed.py b/old_scripts/lvm_backup_gui_fixed.py new file mode 100644 index 0000000..fcdf68f --- /dev/null +++ b/old_scripts/lvm_backup_gui_fixed.py @@ -0,0 +1,459 @@ +#!/usr/bin/env python3 + +import tkinter as tk +from tkinter import ttk, messagebox, scrolledtext +import subprocess +import threading +import re +import os +import time + +class LVMBackupGUI: + def __init__(self, root): + self.root = root + self.root.title("LVM Backup Manager") + self.root.geometry("900x700") + + # Configure style + style = ttk.Style() + style.theme_use('clam') + + # State variables + self.backup_process = None + self.backup_running = False + self.backup_thread = None + + # StringVars for UI + self.source_var = tk.StringVar() + self.target_var = tk.StringVar() + self.status_var = tk.StringVar(value="Ready") + self.progress_var = tk.DoubleVar() + self.stage_var = tk.StringVar(value="Waiting to start...") + self.speed_var = tk.StringVar(value="0 MB/s") + self.time_var = tk.StringVar(value="Not calculated") + self.size_var = tk.StringVar(value="Not calculated") + + self.setup_ui() + self.refresh_drives() + + def setup_ui(self): + """Setup the user interface""" + main_frame = ttk.Frame(self.root, padding="10") + main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + + # Title + title_frame = ttk.Frame(main_frame) + title_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 20)) + + title_label = ttk.Label(title_frame, text="🛡️ LVM Backup Manager", + font=('Arial', 16, 'bold')) + title_label.pack() + + # Source Drive Selection + source_frame = ttk.LabelFrame(main_frame, text="Source Drive", padding="10") + source_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) + + ttk.Label(source_frame, text="Volume Group:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10)) + self.source_combo = ttk.Combobox(source_frame, textvariable=self.source_var, + state="readonly", width=50) + self.source_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10)) + self.source_combo.bind('<>', self.on_source_selected) + + ttk.Button(source_frame, text="🔄 Refresh", + command=self.refresh_drives).grid(row=0, column=2) + + # Arrow + arrow_frame = ttk.Frame(main_frame) + arrow_frame.grid(row=2, column=0, columnspan=2, pady=10) + ttk.Label(arrow_frame, text="⬇", font=('Arial', 24)).pack() + + # Target Drive Selection + target_frame = ttk.LabelFrame(main_frame, text="Target Drive", padding="10") + target_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) + + ttk.Label(target_frame, text="Volume Group:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10)) + self.target_combo = ttk.Combobox(target_frame, textvariable=self.target_var, + state="readonly", width=50) + self.target_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 10)) + self.target_combo.bind('<>', self.on_target_selected) + + ttk.Button(target_frame, text="🔄 Refresh", + command=self.refresh_drives).grid(row=0, column=2) + + # Backup Information + info_frame = ttk.LabelFrame(main_frame, text="📊 Backup Information", padding="10") + info_frame.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) + + # Info grid + ttk.Label(info_frame, text="Total Size:").grid(row=0, column=0, sticky=tk.W, padx=(0, 20)) + ttk.Label(info_frame, textvariable=self.size_var).grid(row=0, column=1, sticky=tk.W, padx=(0, 40)) + + ttk.Label(info_frame, text="Estimated Time:").grid(row=0, column=2, sticky=tk.W, padx=(0, 20)) + ttk.Label(info_frame, textvariable=self.time_var).grid(row=0, column=3, sticky=tk.W) + + ttk.Label(info_frame, text="Transfer Speed:").grid(row=1, column=0, sticky=tk.W, padx=(0, 20)) + ttk.Label(info_frame, textvariable=self.speed_var).grid(row=1, column=1, sticky=tk.W, padx=(0, 40)) + + ttk.Label(info_frame, text="Status:").grid(row=1, column=2, sticky=tk.W, padx=(0, 20)) + ttk.Label(info_frame, textvariable=self.status_var).grid(row=1, column=3, sticky=tk.W) + + # Control Buttons + button_frame = ttk.Frame(main_frame) + button_frame.grid(row=5, column=0, columnspan=2, pady=20) + + self.start_button = ttk.Button(button_frame, text="🚀 Start Backup", + command=self.start_backup, style='Accent.TButton') + self.start_button.pack(side=tk.LEFT, padx=(0, 10)) + + self.stop_button = ttk.Button(button_frame, text="⏹ Stop Backup", + command=self.stop_backup, state='disabled') + self.stop_button.pack(side=tk.LEFT, padx=(0, 10)) + + self.verify_button = ttk.Button(button_frame, text="✅ Verify Backup", + command=self.verify_backup) + self.verify_button.pack(side=tk.LEFT) + + # Progress Section + progress_frame = ttk.LabelFrame(main_frame, text="📈 Progress", padding="10") + progress_frame.grid(row=6, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) + + ttk.Label(progress_frame, text="Overall Progress:").grid(row=0, column=0, sticky=tk.W, pady=(0, 5)) + + self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, + length=400, mode='determinate') + self.progress_bar.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) + + # Current stage + stage_frame = ttk.Frame(progress_frame) + stage_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E)) + + ttk.Label(stage_frame, text="⚙️").pack(side=tk.LEFT, padx=(0, 5)) + ttk.Label(stage_frame, textvariable=self.stage_var).pack(side=tk.LEFT) + + # Log Output + log_frame = ttk.LabelFrame(main_frame, text="📝 Log Output", padding="10") + log_frame.grid(row=7, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10)) + + self.log_text = scrolledtext.ScrolledText(log_frame, height=10, width=80) + self.log_text.pack(fill=tk.BOTH, expand=True) + + # Configure grid weights + main_frame.columnconfigure(0, weight=1) + main_frame.rowconfigure(7, weight=1) + source_frame.columnconfigure(1, weight=1) + target_frame.columnconfigure(1, weight=1) + info_frame.columnconfigure(1, weight=1) + info_frame.columnconfigure(3, weight=1) + progress_frame.columnconfigure(0, weight=1) + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + + def log(self, message): + """Add message to log""" + timestamp = time.strftime("%H:%M:%S") + self.log_text.insert(tk.END, f"[{timestamp}] {message}\\n") + self.log_text.see(tk.END) + self.root.update_idletasks() + + def refresh_drives(self): + """Refresh the list of available LVM volume groups""" + try: + # Get volume groups + result = subprocess.run(['sudo', 'vgs', '--noheadings', '-o', 'vg_name,vg_size,vg_free'], + capture_output=True, text=True) + + if result.returncode == 0: + vgs = [] + for line in result.stdout.strip().split('\\n'): + if line.strip(): + parts = line.strip().split() + if len(parts) >= 3: + vg_name = parts[0] + vg_size = parts[1] + vg_free = parts[2] + vgs.append(f"{vg_name} ({vg_size} total, {vg_free} free)") + + self.source_combo['values'] = vgs + self.target_combo['values'] = vgs + + # Auto-select if only specific VGs found + if len(vgs) >= 2: + for vg in vgs: + if 'internal-vg' in vg: + self.source_var.set(vg) + elif 'migration-vg' in vg: + self.target_var.set(vg) + + self.log(f"Found {len(vgs)} volume groups") + else: + self.log("Failed to get volume groups - are you running as root?") + + except Exception as e: + self.log(f"Error refreshing drives: {e}") + + def on_source_selected(self, event=None): + """Handle source drive selection""" + self.calculate_backup_info() + + def on_target_selected(self, event=None): + """Handle target drive selection""" + self.calculate_backup_info() + + def calculate_backup_info(self): + """Calculate backup size and time estimates""" + if not self.source_var.get(): + return + + try: + source_vg = self.source_var.get().split()[0] + + # Get logical volume sizes + result = subprocess.run(['sudo', 'lvs', source_vg, '--noheadings', '-o', 'lv_size', '--units', 'b'], + capture_output=True, text=True) + + if result.returncode == 0: + total_bytes = 0 + for line in result.stdout.strip().split('\\n'): + if line.strip(): + size_str = line.strip().rstrip('B') + try: + total_bytes += int(size_str) + except ValueError: + continue + + # Convert to GB + total_gb = total_bytes / (1024**3) + + # Estimate time (assuming 250 MB/s average) + est_seconds = total_bytes / (250 * 1024 * 1024) + est_hours = int(est_seconds // 3600) + est_mins = int((est_seconds % 3600) // 60) + + time_str = f"{est_hours}h {est_mins}m" if est_hours > 0 else f"{est_mins}m" + + self.size_var.set(f"{total_gb:.1f} GB") + self.time_var.set(time_str) + self.speed_var.set("~250 MB/s") + + except Exception as e: + self.log(f"Error calculating backup info: {e}") + + def start_backup(self): + """Start the backup process""" + if not self.source_var.get() or not self.target_var.get(): + messagebox.showerror("Error", "Please select both source and target drives") + return + + source_vg = self.source_var.get().split()[0] + target_vg = self.target_var.get().split()[0] + + if source_vg == target_vg: + messagebox.showerror("Error", "Source and target cannot be the same") + return + + # Confirm + if not messagebox.askyesno("Confirm Backup", + f"This will OVERWRITE all data on {target_vg}!\\n\\nContinue?"): + return + + # Update UI + self.backup_running = True + self.start_button.config(state='disabled') + self.stop_button.config(state='normal') + self.verify_button.config(state='disabled') + self.status_var.set("Running...") + self.progress_var.set(0) + self.stage_var.set("Starting backup...") + + # Clear log + self.log_text.delete(1.0, tk.END) + self.log("🚀 Starting LVM block-level backup...") + + # Start backup thread + self.backup_thread = threading.Thread(target=self.run_backup, + args=(source_vg, target_vg)) + self.backup_thread.daemon = True + self.backup_thread.start() + + def run_backup(self, source_vg, target_vg): + """Execute the backup process""" + try: + # Create the backup script + script_content = f'''#!/bin/bash +set -e + +SOURCE_VG="{source_vg}" +TARGET_VG="{target_vg}" +SNAPSHOT_SIZE="2G" + +echo "[$(date '+%H:%M:%S')] Checking system requirements..." + +echo "[$(date '+%H:%M:%S')] Cleaning up any existing snapshots..." +lvremove -f "$SOURCE_VG/root-backup-snap" 2>/dev/null || true +lvremove -f "$SOURCE_VG/home-backup-snap" 2>/dev/null || true +lvremove -f "$SOURCE_VG/boot-backup-snap" 2>/dev/null || true + +echo "[$(date '+%H:%M:%S')] Creating LVM snapshots..." +lvcreate -L "$SNAPSHOT_SIZE" -s -n root-backup-snap "$SOURCE_VG/root" +lvcreate -L "$SNAPSHOT_SIZE" -s -n home-backup-snap "$SOURCE_VG/home" +lvcreate -L 1G -s -n boot-backup-snap "$SOURCE_VG/boot" +echo "STAGE:Snapshots created" + +echo "[$(date '+%H:%M:%S')] Unmounting target volumes..." +umount "/dev/$TARGET_VG/home" 2>/dev/null || true +umount "/dev/$TARGET_VG/root" 2>/dev/null || true +umount "/dev/$TARGET_VG/boot" 2>/dev/null || true + +echo "[$(date '+%H:%M:%S')] Cloning root volume..." +echo "STAGE:Cloning root volume" +dd if="/dev/$SOURCE_VG/root-backup-snap" of="/dev/$TARGET_VG/root" bs=64M status=progress +echo "STAGE:Root volume completed" + +echo "[$(date '+%H:%M:%S')] Cloning home volume..." +echo "STAGE:Cloning home volume" +dd if="/dev/$SOURCE_VG/home-backup-snap" of="/dev/$TARGET_VG/home" bs=64M status=progress +echo "STAGE:Home volume completed" + +echo "[$(date '+%H:%M:%S')] Cloning boot volume..." +echo "STAGE:Cloning boot volume" +dd if="/dev/$SOURCE_VG/boot-backup-snap" of="/dev/$TARGET_VG/boot" bs=64M status=progress +echo "STAGE:Boot volume completed" + +echo "[$(date '+%H:%M:%S')] Cleaning up snapshots..." +lvremove -f "$SOURCE_VG/root-backup-snap" +lvremove -f "$SOURCE_VG/home-backup-snap" +lvremove -f "$SOURCE_VG/boot-backup-snap" + +echo "[$(date '+%H:%M:%S')] Verifying backup..." +echo "STAGE:Verifying backup" +fsck -n "/dev/$TARGET_VG/root" 2>/dev/null || echo "Root filesystem verified" +fsck -n "/dev/$TARGET_VG/boot" 2>/dev/null || echo "Boot filesystem verified" + +echo "STAGE:Backup completed successfully!" +echo "[$(date '+%H:%M:%S')] Backup completed successfully!" +''' + + # Write temp script + with open('/tmp/gui_backup.sh', 'w') as f: + f.write(script_content) + os.chmod('/tmp/gui_backup.sh', 0o755) + + # Execute backup + self.backup_process = subprocess.Popen( + ['sudo', '/tmp/gui_backup.sh'], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + bufsize=1 + ) + + # Monitor output + for line in iter(self.backup_process.stdout.readline, ''): + if not self.backup_running: + break + + line = line.strip() + if line: + self.root.after(0, lambda l=line: self.log(l)) + + # Parse progress + if line.startswith('STAGE:'): + stage = line[6:] + self.root.after(0, lambda s=stage: self.stage_var.set(s)) + + if 'Snapshots created' in stage: + self.root.after(0, lambda: self.progress_var.set(10)) + elif 'Cloning root' in stage: + self.root.after(0, lambda: self.progress_var.set(15)) + elif 'Root volume completed' in stage: + self.root.after(0, lambda: self.progress_var.set(35)) + elif 'Cloning home' in stage: + self.root.after(0, lambda: self.progress_var.set(40)) + elif 'Home volume completed' in stage: + self.root.after(0, lambda: self.progress_var.set(80)) + elif 'Cloning boot' in stage: + self.root.after(0, lambda: self.progress_var.set(85)) + elif 'Boot volume completed' in stage: + self.root.after(0, lambda: self.progress_var.set(90)) + elif 'Verifying' in stage: + self.root.after(0, lambda: self.progress_var.set(95)) + elif 'completed successfully' in stage: + self.root.after(0, lambda: self.progress_var.set(100)) + + # Parse dd speed + if 'MB/s' in line or 'GB/s' in line: + speed_match = re.search(r'(\\d+(?:\\.\\d+)?)\\s*(MB|GB)/s', line) + if speed_match: + speed = f"{speed_match.group(1)} {speed_match.group(2)}/s" + self.root.after(0, lambda s=speed: self.speed_var.set(s)) + + # Check result + return_code = self.backup_process.wait() + success = return_code == 0 + + # Cleanup temp script + try: + os.remove('/tmp/gui_backup.sh') + except: + pass + + # Update UI + self.root.after(0, lambda: self.backup_finished(success)) + + except Exception as e: + self.root.after(0, lambda: self.log(f"❌ Error: {e}")) + self.root.after(0, lambda: self.backup_finished(False)) + + def backup_finished(self, success): + """Handle backup completion""" + self.backup_running = False + self.start_button.config(state='normal') + self.stop_button.config(state='disabled') + self.verify_button.config(state='normal') + + if success: + self.status_var.set("Completed") + self.log("✅ Backup completed successfully!") + messagebox.showinfo("Success", "Backup completed successfully!\\n\\nYour external drive now contains a bootable copy.") + else: + self.status_var.set("Failed") + self.log("❌ Backup failed!") + messagebox.showerror("Error", "Backup failed. Check the log for details.") + + def stop_backup(self): + """Stop the backup process""" + if self.backup_process: + self.backup_running = False + self.backup_process.terminate() + self.log("⏹ Backup stopped by user") + self.backup_finished(False) + + def verify_backup(self): + """Verify the backup""" + if not self.target_var.get(): + messagebox.showerror("Error", "Please select a target drive first") + return + + target_vg = self.target_var.get().split()[0] + self.log(f"🔍 Verifying backup on {target_vg}...") + + # Simple verification + try: + result = subprocess.run(['sudo', 'lvs', target_vg], capture_output=True, text=True) + if result.returncode == 0: + self.log("✅ Target volumes exist and are accessible") + messagebox.showinfo("Verification", "Basic verification passed!\\nTarget volumes are accessible.") + else: + self.log("❌ Verification failed - target volumes not accessible") + messagebox.showerror("Verification", "Verification failed!") + except Exception as e: + self.log(f"❌ Verification error: {e}") + messagebox.showerror("Error", f"Verification error: {e}") + +def main(): + root = tk.Tk() + app = LVMBackupGUI(root) + root.mainloop() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/old_scripts/lvm_block_backup.sh b/old_scripts/lvm_block_backup.sh new file mode 100755 index 0000000..bba965c --- /dev/null +++ b/old_scripts/lvm_block_backup.sh @@ -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 "$@" \ No newline at end of file diff --git a/old_scripts/verify_boot_readiness.sh b/old_scripts/verify_boot_readiness.sh old mode 100755 new mode 100644 diff --git a/old_scripts/verify_internal_boot.sh b/old_scripts/verify_internal_boot.sh new file mode 100755 index 0000000..8a5b2e8 --- /dev/null +++ b/old_scripts/verify_internal_boot.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Verify Internal NVMe Boot Configuration +# This script checks if the internal drive (nvme0n1) is properly configured to boot + +set -e + +echo "=== Internal Drive Boot Verification ===" +echo "" + +echo "1. Checking EFI partition mount..." +df -h /boot/efi | grep -E "Filesystem|nvme0n1p1" && echo "✓ EFI mounted from internal drive" || echo "✗ EFI not on internal drive" +echo "" + +echo "2. Checking boot partition mount..." +df -h /boot | grep -E "Filesystem|internal--vg-boot" && echo "✓ Boot mounted from internal-vg" || echo "✗ Boot not on internal-vg" +echo "" + +echo "3. Checking GRUB installation..." +if [ -f /boot/efi/EFI/ubuntu/shimx64.efi ]; then + echo "✓ GRUB bootloader found in EFI" +else + echo "✗ GRUB bootloader missing" +fi +echo "" + +echo "4. Checking EFI boot entries..." +efibootmgr | grep -i ubuntu && echo "✓ Ubuntu boot entry exists" || echo "✗ No Ubuntu boot entry" +echo "" + +echo "5. Checking GRUB environment..." +sudo grub-editenv list +if [ -z "$(sudo grub-editenv list)" ]; then + echo "✓ GRUB environment clean (no boot failure flags)" +else + echo "⚠ GRUB environment has settings" +fi +echo "" + +echo "6. Checking current boot device..." +echo "Currently booted from: $(findmnt -n -o SOURCE /)" +echo "" + +echo "7. Internal drive UUIDs..." +echo "Root: $(sudo blkid /dev/internal-vg/root -s UUID -o value)" +echo "Boot: $(sudo blkid /dev/internal-vg/boot -s UUID -o value)" +echo "" + +echo "8. Checking /etc/fstab for internal drive..." +grep -E "internal-vg|757B-A377" /etc/fstab && echo "✓ Internal volumes in fstab" || echo "⚠ Check fstab configuration" +echo "" + +echo "=== Verification Complete ===" +echo "" +echo "To test boot from internal drive:" +echo "1. Remove/disconnect the external USB M.2 drive" +echo "2. Reboot the system" +echo "3. System should boot from internal nvme0n1" diff --git a/old_scripts/verify_internal_config.sh b/old_scripts/verify_internal_config.sh new file mode 100755 index 0000000..b7c8cc2 --- /dev/null +++ b/old_scripts/verify_internal_config.sh @@ -0,0 +1,147 @@ +#!/bin/bash +# Final Internal Drive Configuration Check +# Run this after rebooting to verify everything is working + +echo "═══════════════════════════════════════════════════════" +echo " Internal NVMe Drive Configuration Verification" +echo "═══════════════════════════════════════════════════════" +echo "" + +# Color codes +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +success_count=0 +total_checks=7 + +echo "Running system checks..." +echo "" + +# Check 1: Root filesystem +echo -n "1. Root filesystem from internal drive... " +if mount | grep -q "internal--vg-root on / "; then + echo -e "${GREEN}✓ PASS${NC}" + ((success_count++)) +else + echo -e "${RED}✗ FAIL${NC}" + mount | grep " / " +fi + +# Check 2: Boot filesystem +echo -n "2. Boot filesystem from internal drive... " +if mount | grep -q "internal--vg-boot on /boot "; then + echo -e "${GREEN}✓ PASS${NC}" + ((success_count++)) +else + echo -e "${RED}✗ FAIL${NC}" + mount | grep " /boot " +fi + +# Check 3: Home filesystem +echo -n "3. Home filesystem from internal drive... " +HOME_DEVICE=$(sudo cryptsetup status luks-home-internal 2>/dev/null | grep "device:" | awk '{print $2}') +if [[ "$HOME_DEVICE" == "/dev/mapper/internal--vg-home" ]]; then + echo -e "${GREEN}✓ PASS${NC}" + ((success_count++)) +else + echo -e "${RED}✗ FAIL${NC}" + echo " Currently using: $HOME_DEVICE" +fi + +# Check 4: EFI partition +echo -n "4. EFI partition from internal drive... " +if mount | grep -q "nvme0n1p1 on /boot/efi"; then + echo -e "${GREEN}✓ PASS${NC}" + ((success_count++)) +else + echo -e "${RED}✗ FAIL${NC}" + mount | grep "/boot/efi" +fi + +# Check 5: GRUB installation +echo -n "5. GRUB bootloader installed... " +if [ -f /boot/efi/EFI/ubuntu/shimx64.efi ] && [ -f /boot/efi/EFI/ubuntu/grubx64.efi ]; then + echo -e "${GREEN}✓ PASS${NC}" + ((success_count++)) +else + echo -e "${RED}✗ FAIL${NC}" +fi + +# Check 6: EFI boot order +echo -n "6. Ubuntu first in boot order... " +FIRST_BOOT=$(efibootmgr | grep BootOrder | cut -d':' -f2 | tr -d ' ' | cut -d',' -f1) +if [ "$FIRST_BOOT" = "0001" ] && efibootmgr | grep -q "Boot0001.*Ubuntu"; then + echo -e "${GREEN}✓ PASS${NC}" + ((success_count++)) +else + echo -e "${RED}✗ FAIL${NC}" + echo " First boot entry: $FIRST_BOOT" +fi + +# Check 7: External drive status +echo -n "7. External drive NOT in use for system... " +if ! mount | grep -q "migration--vg.*on /"; then + echo -e "${GREEN}✓ PASS${NC}" + ((success_count++)) +else + echo -e "${YELLOW}⚠ WARNING${NC}" + echo " External drive (migration-vg) is mounted for system use" + mount | grep migration-vg +fi + +echo "" +echo "═══════════════════════════════════════════════════════" +echo " Results: $success_count/$total_checks checks passed" +echo "═══════════════════════════════════════════════════════" +echo "" + +if [ $success_count -eq $total_checks ]; then + echo -e "${GREEN}✓ SUCCESS!${NC} Internal drive is fully configured and operational." + echo "" + echo "Your system is now running entirely from the internal NVMe drive." + echo "The external M.2 drive can be used as a backup." + echo "" + echo "Next steps:" + echo " 1. Test disconnecting the external drive and rebooting" + echo " 2. Run backup: sudo ./lvm_block_backup.sh" + echo "" +elif [ $success_count -ge 5 ]; then + echo -e "${YELLOW}⚠ PARTIAL SUCCESS${NC} - Most checks passed but some issues remain." + echo "" + echo "Review the failed checks above and consult INTERNAL_DRIVE_RECOVERY.md" + echo "" +else + echo -e "${RED}✗ CONFIGURATION ISSUES DETECTED${NC}" + echo "" + echo "Several checks failed. You may need to reboot for changes to take effect." + echo "If problems persist after reboot, review INTERNAL_DRIVE_RECOVERY.md" + echo "" +fi + +# Additional info +echo "═══════════════════════════════════════════════════════" +echo " Detailed Information" +echo "═══════════════════════════════════════════════════════" +echo "" + +echo "Current mount points:" +lsblk -o NAME,SIZE,TYPE,MOUNTPOINT | grep -E "NAME|nvme0n1|sda" +echo "" + +echo "LUKS device mapping:" +sudo cryptsetup status luks-home-internal 2>/dev/null | grep -E "type|device|mode" +echo "" + +echo "EFI boot configuration:" +efibootmgr | grep -E "BootCurrent|BootOrder|Boot0001" +echo "" + +if [ -b /dev/sda ]; then + echo -e "${YELLOW}Note:${NC} External drive (sda) is connected." + echo " To test full independence, shutdown and disconnect it." +else + echo -e "${GREEN}Note:${NC} External drive is not connected - full independence confirmed!" +fi +echo "" diff --git a/verify_boot_readiness.sh b/verify_boot_readiness.sh deleted file mode 100755 index 67ccd75..0000000 --- a/verify_boot_readiness.sh +++ /dev/null @@ -1,481 +0,0 @@ -#!/bin/bash - -# Boot Verification and Test Script -# Validates that a cloned drive can boot properly before declaring success - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -TARGET_DRIVE="" -WORK_DIR="/mnt/verify_work" - -log() { - echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1" -} - -error() { - echo -e "${RED}[ERROR]${NC} $1" >&2 - return 1 -} - -warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -check_partition_structure() { - log "Verifying partition structure..." - - # Check if drive exists and has partitions - if [ ! -b "$TARGET_DRIVE" ]; then - error "Target drive $TARGET_DRIVE not found" - return 1 - fi - - local partitions=($(lsblk -pno NAME "$TARGET_DRIVE" | grep -v "^$TARGET_DRIVE$")) - - if [ ${#partitions[@]} -eq 0 ]; then - error "No partitions found on $TARGET_DRIVE" - return 1 - fi - - echo "Found ${#partitions[@]} partitions:" - for part in "${partitions[@]}"; do - local size=$(lsblk -no SIZE "$part") - local fstype=$(lsblk -no FSTYPE "$part") - local uuid=$(lsblk -no UUID "$part") - echo " $part: $size, $fstype, UUID: ${uuid:-'none'}" - done - - success "Partition structure verified" - return 0 -} - -check_filesystem_integrity() { - log "Checking filesystem integrity..." - - local partitions=($(lsblk -pno NAME "$TARGET_DRIVE" | grep -v "^$TARGET_DRIVE$")) - local errors=0 - - for part in "${partitions[@]}"; do - local fstype=$(lsblk -no FSTYPE "$part") - - case "$fstype" in - "ext4"|"ext3"|"ext2") - log "Checking ext filesystem on $part..." - if e2fsck -n "$part" >/dev/null 2>&1; then - success "Filesystem on $part is clean" - else - warning "Filesystem on $part has errors (read-only check)" - ((errors++)) - fi - ;; - "vfat") - log "Checking FAT filesystem on $part..." - if fsck.fat -v "$part" >/dev/null 2>&1; then - success "FAT filesystem on $part is clean" - else - warning "FAT filesystem on $part has errors" - ((errors++)) - fi - ;; - "crypto_LUKS") - log "Skipping LUKS partition $part (encrypted)" - ;; - "swap") - log "Skipping swap partition $part" - ;; - *) - log "Skipping unknown filesystem type '$fstype' on $part" - ;; - esac - done - - if [ $errors -eq 0 ]; then - success "All filesystems are clean" - return 0 - else - warning "Found $errors filesystem(s) with potential issues" - return 1 - fi -} - -check_boot_files() { - log "Checking essential boot files..." - - mkdir -p "$WORK_DIR" - local errors=0 - - # Find and mount root partition - local root_partition="" - local boot_partition="" - local efi_partition="" - - local partitions=($(lsblk -pno NAME "$TARGET_DRIVE" | grep -v "^$TARGET_DRIVE$")) - - for part in "${partitions[@]}"; do - local fstype=$(lsblk -no FSTYPE "$part") - local size_bytes=$(lsblk -bno SIZE "$part") - - if [[ "$fstype" == "vfat" && "$size_bytes" -lt 1073741824 ]]; then - efi_partition="$part" - elif [[ "$fstype" == "ext4" && "$size_bytes" -lt 5368709120 ]]; then - boot_partition="$part" - elif [[ "$fstype" == "ext4" && "$size_bytes" -gt 5368709120 ]]; then - root_partition="$part" - elif [[ "$fstype" == "crypto_LUKS" ]]; then - # Try to unlock for verification - local crypt_name="verify_$(basename "$part")" - echo "Found encrypted partition: $part" - echo "Please enter password to verify boot files (optional - press Enter to skip):" - read -s -t 30 password || { - log "Skipping encrypted partition verification" - continue - } - if [ -n "$password" ]; then - if echo "$password" | cryptsetup open "$part" "$crypt_name" --key-file=-; then - local decrypted_fs=$(lsblk -no FSTYPE "/dev/mapper/$crypt_name") - if [[ "$decrypted_fs" == "ext4" ]]; then - root_partition="/dev/mapper/$crypt_name" - log "Using decrypted partition for verification: $root_partition" - fi - else - warning "Could not unlock encrypted partition for verification" - fi - fi - fi - done - - if [ -z "$root_partition" ]; then - error "Could not find root partition for verification" - return 1 - fi - - # Mount root partition - if ! mount "$root_partition" "$WORK_DIR"; then - error "Could not mount root partition for verification" - return 1 - fi - - # Mount boot if separate - if [ -n "$boot_partition" ]; then - mkdir -p "$WORK_DIR/boot" - mount "$boot_partition" "$WORK_DIR/boot" || warning "Could not mount boot partition" - fi - - # Mount EFI if exists - if [ -n "$efi_partition" ]; then - mkdir -p "$WORK_DIR/boot/efi" - mount "$efi_partition" "$WORK_DIR/boot/efi" || warning "Could not mount EFI partition" - fi - - # Check essential files - local essential_files=( - "/etc/fstab" - "/boot/grub/grub.cfg" - "/etc/default/grub" - ) - - for file in "${essential_files[@]}"; do - if [ -f "$WORK_DIR$file" ]; then - success "Found: $file" - else - warning "Missing: $file" - ((errors++)) - fi - done - - # Check for kernel and initramfs - local kernels=($(ls "$WORK_DIR/boot/vmlinuz-"* 2>/dev/null || true)) - local initramfs=($(ls "$WORK_DIR/boot/initrd.img-"* 2>/dev/null || true)) - - if [ ${#kernels[@]} -gt 0 ]; then - success "Found ${#kernels[@]} kernel(s)" - else - warning "No kernels found" - ((errors++)) - fi - - if [ ${#initramfs[@]} -gt 0 ]; then - success "Found ${#initramfs[@]} initramfs image(s)" - else - warning "No initramfs images found" - ((errors++)) - fi - - # Check EFI bootloader - if [ -n "$efi_partition" ]; then - if [ -f "$WORK_DIR/boot/efi/EFI/debian/grubx64.efi" ]; then - success "Found EFI bootloader" - else - warning "EFI bootloader not found" - ((errors++)) - fi - fi - - # Check fstab content - if [ -f "$WORK_DIR/etc/fstab" ]; then - log "Checking /etc/fstab content..." - local fstab_errors=0 - - # Check if UUIDs in fstab actually exist - while read -r line; do - if [[ "$line" =~ ^UUID=([a-fA-F0-9-]+) ]]; then - local uuid="${BASH_REMATCH[1]}" - if ! blkid | grep -q "$uuid"; then - warning "UUID $uuid in fstab not found on system" - ((fstab_errors++)) - fi - fi - done < "$WORK_DIR/etc/fstab" - - if [ $fstab_errors -eq 0 ]; then - success "/etc/fstab appears valid" - else - warning "/etc/fstab has $fstab_errors potential issues" - ((errors++)) - fi - fi - - # Cleanup mounts - umount "$WORK_DIR/boot/efi" 2>/dev/null || true - umount "$WORK_DIR/boot" 2>/dev/null || true - umount "$WORK_DIR" 2>/dev/null || true - - # Close encrypted volumes - for mapper in /dev/mapper/verify_*; do - if [ -b "$mapper" ]; then - local crypt_name=$(basename "$mapper") - cryptsetup close "$crypt_name" 2>/dev/null || true - fi - done - - if [ $errors -eq 0 ]; then - success "All essential boot files found" - return 0 - else - warning "Found $errors potential boot issues" - return 1 - fi -} - -check_grub_configuration() { - log "Checking GRUB configuration..." - - # Try to validate GRUB configuration without mounting - if grub-probe "$TARGET_DRIVE" >/dev/null 2>&1; then - success "GRUB can recognize the drive" - else - warning "GRUB may have issues recognizing the drive" - return 1 - fi - - return 0 -} - -perform_dry_run_boot_test() { - log "Performing dry-run boot test..." - - # Check if we can simulate boot process - warning "Note: This is a simulation - actual boot test requires reboot" - - # Check boot order in EFI (if available) - if command -v efibootmgr >/dev/null 2>&1; then - log "Current EFI boot order:" - efibootmgr | grep -E "(BootOrder|Boot[0-9]+)" || true - fi - - # Test if drive is bootable by checking MBR/GPT - if fdisk -l "$TARGET_DRIVE" | grep -q "EFI System"; then - success "Drive has EFI System partition (UEFI bootable)" - elif fdisk -l "$TARGET_DRIVE" | grep -q "Boot"; then - success "Drive has bootable partition" - else - warning "Drive may not be properly configured for booting" - return 1 - fi - - return 0 -} - -comprehensive_verification() { - log "Starting comprehensive verification of $TARGET_DRIVE..." - - local total_checks=5 - local passed_checks=0 - local failed_checks=0 - - echo "Verification Progress:" - echo "=====================" - - # Test 1: Partition Structure - echo -n "1. Partition Structure: " - if check_partition_structure; then - echo -e "${GREEN}PASS${NC}" - ((passed_checks++)) - else - echo -e "${RED}FAIL${NC}" - ((failed_checks++)) - fi - - # Test 2: Filesystem Integrity - echo -n "2. Filesystem Integrity: " - if check_filesystem_integrity; then - echo -e "${GREEN}PASS${NC}" - ((passed_checks++)) - else - echo -e "${YELLOW}WARNING${NC}" - ((passed_checks++)) # Count warnings as pass for now - fi - - # Test 3: Boot Files - echo -n "3. Essential Boot Files: " - if check_boot_files; then - echo -e "${GREEN}PASS${NC}" - ((passed_checks++)) - else - echo -e "${RED}FAIL${NC}" - ((failed_checks++)) - fi - - # Test 4: GRUB Configuration - echo -n "4. GRUB Configuration: " - if check_grub_configuration; then - echo -e "${GREEN}PASS${NC}" - ((passed_checks++)) - else - echo -e "${YELLOW}WARNING${NC}" - ((passed_checks++)) # Count as pass for compatibility - fi - - # Test 5: Boot Readiness - echo -n "5. Boot Readiness: " - if perform_dry_run_boot_test; then - echo -e "${GREEN}PASS${NC}" - ((passed_checks++)) - else - echo -e "${RED}FAIL${NC}" - ((failed_checks++)) - fi - - echo "=====================" - echo "Verification Summary:" - echo " Passed: $passed_checks/$total_checks" - echo " Failed: $failed_checks/$total_checks" - - if [ $failed_checks -eq 0 ]; then - success "All verification checks passed! Drive should boot properly." - return 0 - elif [ $failed_checks -le 2 ]; then - warning "Some checks failed but drive might still boot. Consider running boot repair." - return 1 - else - error "Multiple critical checks failed. Drive is unlikely to boot properly." - return 2 - fi -} - -cleanup_verification() { - log "Cleaning up verification environment..." - - # Unmount any remaining mounts - umount "$WORK_DIR" 2>/dev/null || true - - # Close any encrypted volumes - for mapper in /dev/mapper/verify_*; do - if [ -b "$mapper" ]; then - local crypt_name=$(basename "$mapper") - cryptsetup close "$crypt_name" 2>/dev/null || true - fi - done - - # Remove work directory - rmdir "$WORK_DIR" 2>/dev/null || true - - success "Cleanup completed" -} - -main() { - echo -e "${GREEN}=== Boot Verification and Test Script ===${NC}" - echo "This script validates that a cloned drive can boot properly" - echo - - # Get target drive - echo "Available drives:" - lsblk -dpno NAME,SIZE,MODEL | grep -v "loop\|ram" - echo - - read -p "Enter the drive to verify (e.g., /dev/sdb): " TARGET_DRIVE - - if [ ! -b "$TARGET_DRIVE" ]; then - error "Drive $TARGET_DRIVE not found" - exit 1 - fi - - echo "Selected drive for verification: $TARGET_DRIVE" - lsblk "$TARGET_DRIVE" - echo - - read -p "Verify this drive? [y/N]: " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - error "Verification cancelled" - exit 1 - fi - - # Perform comprehensive verification - if comprehensive_verification; then - echo - echo -e "${GREEN}🎉 VERIFICATION SUCCESSFUL! 🎉${NC}" - echo "The cloned drive passed all verification checks." - echo "It should boot properly when set as the primary boot device." - echo - echo -e "${BLUE}Next steps:${NC}" - echo "1. Reboot your system" - echo "2. Enter BIOS/UEFI setup" - echo "3. Set $TARGET_DRIVE as the first boot device" - echo "4. Save and exit BIOS/UEFI" - echo "5. System should boot from cloned drive" - - if lsblk "$TARGET_DRIVE" | grep -q "crypto_LUKS"; then - echo - echo -e "${YELLOW}Note:${NC} System will ask for LUKS password during boot (this is normal)" - fi - - cleanup_verification - exit 0 - else - local exit_code=$? - echo - if [ $exit_code -eq 1 ]; then - echo -e "${YELLOW}⚠️ VERIFICATION COMPLETED WITH WARNINGS ⚠️${NC}" - echo "The drive might boot but some issues were detected." - echo "Consider running the boot repair script before attempting to boot." - echo - echo "Run: ./boot_repair_tools.sh" - else - echo -e "${RED}❌ VERIFICATION FAILED ❌${NC}" - echo "The drive is unlikely to boot properly in its current state." - echo "Please run the boot repair script to fix issues." - echo - echo "Run: ./boot_repair_tools.sh" - fi - - cleanup_verification - exit $exit_code - fi -} - -# Trap to ensure cleanup on exit -trap cleanup_verification EXIT - -main "$@" \ No newline at end of file