#!/bin/bash # Migration Script: Non-LVM to LVM System Migration # This script migrates a running system to an external M.2 SSD with LVM support # MUST BE RUN FROM A LIVE USB SYSTEM - NOT FROM THE SYSTEM BEING MIGRATED 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 (will be auto-detected) INTERNAL_DRIVE="" EXTERNAL_DRIVE="" VG_NAME="system-vg" ROOT_LV="root" HOME_LV="home" SWAP_LV="swap" BOOT_LV="boot" # Size configurations (will be calculated based on source sizes) ROOT_SIZE="" HOME_SIZE="" SWAP_SIZE="" BOOT_SIZE="" # Detected partitions and filesystems declare -A INTERNAL_PARTITIONS declare -A PARTITION_MOUNTS declare -A PARTITION_FILESYSTEMS declare -A PARTITION_SIZES # Mount points WORK_DIR="/mnt/migration" INTERNAL_ROOT_MOUNT="$WORK_DIR/internal_root" INTERNAL_HOME_MOUNT="$WORK_DIR/internal_home" INTERNAL_BOOT_MOUNT="$WORK_DIR/internal_boot" EXTERNAL_ROOT_MOUNT="$WORK_DIR/external_root" EXTERNAL_HOME_MOUNT="$WORK_DIR/external_home" EXTERNAL_BOOT_MOUNT="$WORK_DIR/external_boot" 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.*MIGRATION_TOOLS"; 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" 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 migration:" 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" done echo echo "Please identify your drives:" echo "- Internal drive: Usually NVMe (like nvme0n1) or first SATA (like sda)" echo "- External M.2: Usually USB-connected, larger capacity" echo # 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 wiped!):" 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 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" # Additional safety check - confirm the target drive echo echo -e "${RED}⚠️ FINAL SAFETY CHECK ⚠️${NC}" echo "You have selected to COMPLETELY WIPE this drive:" echo " Device: $EXTERNAL_DRIVE" echo " Size: $(lsblk -dpno SIZE "$EXTERNAL_DRIVE")" echo " Model: $(lsblk -dpno MODEL "$EXTERNAL_DRIVE")" echo echo "Current partitions on target drive:" lsblk "$EXTERNAL_DRIVE" || true echo echo -e "${RED}This will DESTROY ALL DATA on $EXTERNAL_DRIVE!${NC}" echo read -p "Type 'YES' to confirm you want to wipe $EXTERNAL_DRIVE: " confirmation if [ "$confirmation" != "YES" ]; then error "Migration cancelled by user for safety" fi } analyze_internal_system() { log "Analyzing internal system layout..." # Get all partitions on internal drive local partitions=($(lsblk -pno NAME "$INTERNAL_DRIVE" | grep -v "^$INTERNAL_DRIVE$")) echo "Found partitions on $INTERNAL_DRIVE:" for part in "${partitions[@]}"; do local size=$(lsblk -no SIZE "$part") local fstype=$(lsblk -no FSTYPE "$part") local mountpoint=$(lsblk -no MOUNTPOINT "$part") local label=$(lsblk -no LABEL "$part") echo " $part: $size, $fstype, mounted at: ${mountpoint:-'not mounted'}, label: ${label:-'no label'}" # Store partition information PARTITION_FILESYSTEMS["$part"]="$fstype" PARTITION_SIZES["$part"]="$size" # Try to identify partition purpose if [[ "$fstype" == "vfat" ]] && [[ "$part" == *"1" ]]; then INTERNAL_PARTITIONS["efi"]="$part" BOOT_SIZE="1G" # Default EFI size elif [[ "$mountpoint" == "/" ]] || [[ "$label" == "root"* ]]; then INTERNAL_PARTITIONS["root"]="$part" # Parse size more carefully, handle G/M/K suffixes local size_num=$(echo "$size" | sed 's/[^0-9.]//g') if [[ "$size" == *"G"* ]]; then ROOT_SIZE="$(echo "$size_num + 10" | bc)G" # Add some extra space elif [[ "$size" == *"M"* ]]; then ROOT_SIZE="$(echo "scale=1; $size_num / 1024 + 1" | bc)G" else ROOT_SIZE="${size_num}G" fi elif [[ "$mountpoint" == "/home" ]] || [[ "$label" == "home"* ]]; then INTERNAL_PARTITIONS["home"]="$part" # Parse size more carefully local size_num=$(echo "$size" | sed 's/[^0-9.]//g') if [[ "$size" == *"G"* ]]; then HOME_SIZE="$(echo "$size_num + 5" | bc)G" # Add some extra space elif [[ "$size" == *"M"* ]]; then HOME_SIZE="$(echo "scale=1; $size_num / 1024 + 1" | bc)G" else HOME_SIZE="${size_num}G" fi elif [[ "$mountpoint" == "/boot" ]] || [[ "$label" == "boot"* ]]; then INTERNAL_PARTITIONS["boot"]="$part" BOOT_SIZE="2G" # Standard boot size elif [[ "$fstype" == "swap" ]]; then INTERNAL_PARTITIONS["swap"]="$part" local size_num=$(echo "$size" | sed 's/[^0-9.]//g') if [[ "$size" == *"G"* ]]; then SWAP_SIZE="${size_num}G" elif [[ "$size" == *"M"* ]]; then SWAP_SIZE="$(echo "scale=1; $size_num / 1024" | bc)G" else SWAP_SIZE="${size_num}G" fi elif [[ "$fstype" == "crypto_LUKS" ]]; then log "Found encrypted partition: $part" # This might be encrypted home or root fi done # If we didn't find a separate home partition, it's probably in root if [ -z "${INTERNAL_PARTITIONS[home]}" ]; then log "No separate /home partition found - assuming /home is in root partition" # Increase root size to accommodate home if [ -n "$ROOT_SIZE" ]; then local root_num=$(echo "$ROOT_SIZE" | sed 's/G//') ROOT_SIZE="$((root_num + 50))G" # Add extra space fi HOME_SIZE="50G" # Create separate home in LVM fi # Set default swap size if not found if [ -z "$SWAP_SIZE" ]; then local mem_gb=$(free -g | awk '/^Mem:/ {print $2}') SWAP_SIZE="$((mem_gb + 2))G" log "No swap partition found, creating ${SWAP_SIZE} swap based on system memory" fi # Set sensible defaults if sizes are missing [ -z "$ROOT_SIZE" ] && ROOT_SIZE="70G" [ -z "$HOME_SIZE" ] && HOME_SIZE="50G" [ -z "$BOOT_SIZE" ] && BOOT_SIZE="2G" [ -z "$SWAP_SIZE" ] && SWAP_SIZE="8G" # Ensure sizes are properly formatted (remove any extra decimals) ROOT_SIZE=$(echo "$ROOT_SIZE" | sed 's/\.[0-9]*G/G/') HOME_SIZE=$(echo "$HOME_SIZE" | sed 's/\.[0-9]*G/G/') BOOT_SIZE=$(echo "$BOOT_SIZE" | sed 's/\.[0-9]*G/G/') SWAP_SIZE=$(echo "$SWAP_SIZE" | sed 's/\.[0-9]*G/G/') echo echo "System analysis summary:" echo " EFI partition: ${INTERNAL_PARTITIONS[efi]:-'not found'}" echo " Root partition: ${INTERNAL_PARTITIONS[root]:-'not found'}" echo " Home partition: ${INTERNAL_PARTITIONS[home]:-'integrated in root'}" echo " Boot partition: ${INTERNAL_PARTITIONS[boot]:-'integrated in root/efi'}" echo " Swap partition: ${INTERNAL_PARTITIONS[swap]:-'not found'}" success "System 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. Please ensure you're running from live USB!" confirm_action "Continue anyway? (Not recommended for production systems)" fi # Check if tools are available and install if missing local missing_tools=() for tool in lvm cryptsetup rsync parted pv grub-install mkfs.ext4 mkfs.fat bc wipefs; 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 "Attempting to install missing tools automatically..." # Try to run preparation script if it exists local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -f "$script_dir/prepare_live_system.sh" ]; then log "Running prepare_live_system.sh to install missing tools..." bash "$script_dir/prepare_live_system.sh" || { error "Failed to prepare live system. Please run: sudo ./prepare_live_system.sh" } else # Fallback: try to install directly log "Attempting direct package installation..." if command -v apt >/dev/null 2>&1; then apt update && apt install -y lvm2 cryptsetup rsync parted pv grub-efi-amd64 grub-common e2fsprogs dosfstools bc util-linux initramfs-tools || { error "Failed to install required packages. Please install manually: lvm2 cryptsetup rsync parted pv grub-efi-amd64 grub-common e2fsprogs dosfstools bc" } else error "Cannot install packages automatically. Please install missing tools: ${missing_tools[*]}" fi fi # Re-check after installation attempt missing_tools=() for tool in lvm cryptsetup rsync parted pv grub-install mkfs.ext4 mkfs.fat bc wipefs; do if ! command -v $tool >/dev/null 2>&1; then missing_tools+=("$tool") fi done if [ ${#missing_tools[@]} -gt 0 ]; then error "Still missing required tools after installation attempt: ${missing_tools[*]}" fi fi success "Prerequisites check passed" } setup_work_directories() { log "Setting up work directories..." mkdir -p "$WORK_DIR"/{internal_root,internal_home,internal_boot,external_root,external_home,external_boot} success "Work directories created" } backup_existing_external_data() { log "Backing up any existing data on external drive..." # Check if external LVM volumes are already mounted if [ -d "/dev/external-vg" ]; then warning "Found existing external-vg volume group" confirm_action "This will DESTROY all data on the external M.2 drive!" # Deactivate existing LVM volumes vgchange -an external-vg || true vgremove -f external-vg || true pvremove -f ${EXTERNAL_DRIVE}2 || true fi success "External drive prepared for migration" } create_lvm_layout() { log "Creating new LVM layout on external drive..." # Debug: Show current size configuration log "Size configuration:" log " ROOT_SIZE: $ROOT_SIZE" log " HOME_SIZE: $HOME_SIZE" log " SWAP_SIZE: $SWAP_SIZE" log " BOOT_SIZE: $BOOT_SIZE" # Wipe the drive and create new partition table log "Wiping external drive: $EXTERNAL_DRIVE" wipefs -a "$EXTERNAL_DRIVE" || warning "Failed to wipe drive signatures" log "Creating GPT partition table" parted -s "$EXTERNAL_DRIVE" mklabel gpt || error "Failed to create partition table" # Create EFI boot partition (512MB) log "Creating EFI boot partition" parted -s "$EXTERNAL_DRIVE" mkpart primary fat32 1MiB 513MiB || error "Failed to create EFI partition" parted -s "$EXTERNAL_DRIVE" set 1 boot on || warning "Failed to set boot flag" parted -s "$EXTERNAL_DRIVE" set 1 esp on || warning "Failed to set ESP flag" # Create LVM partition (rest of disk) log "Creating LVM partition" parted -s "$EXTERNAL_DRIVE" mkpart primary 513MiB 100% || error "Failed to create LVM partition" parted -s "$EXTERNAL_DRIVE" set 2 lvm on || warning "Failed to set LVM flag" # Wait for partition table to be re-read log "Waiting for partition table update..." sleep 3 partprobe "$EXTERNAL_DRIVE" || warning "Failed to update partition table" sleep 2 # Show partition layout for debugging log "New partition layout:" parted "$EXTERNAL_DRIVE" print || warning "Failed to display partition table" # Verify partitions exist if [ ! -b "${EXTERNAL_DRIVE}1" ]; then error "EFI partition ${EXTERNAL_DRIVE}1 not found after creation" fi if [ ! -b "${EXTERNAL_DRIVE}2" ]; then error "LVM partition ${EXTERNAL_DRIVE}2 not found after creation" fi # Create filesystems log "Creating FAT32 filesystem on EFI partition" mkfs.fat -F32 "${EXTERNAL_DRIVE}1" || error "Failed to create EFI filesystem" # Setup LVM log "Creating LVM physical volume on ${EXTERNAL_DRIVE}2" pvcreate "${EXTERNAL_DRIVE}2" || error "Failed to create physical volume" log "Creating volume group: $VG_NAME" vgcreate "$VG_NAME" "${EXTERNAL_DRIVE}2" || error "Failed to create volume group" # Validate sizes before creating logical volumes log "Validating LVM sizes..." # Convert sizes to MB for calculation local root_mb=$(echo "$ROOT_SIZE" | sed 's/G$//' | awk '{print $1 * 1024}') local home_mb=$(echo "$HOME_SIZE" | sed 's/G$//' | awk '{print $1 * 1024}') local swap_mb=$(echo "$SWAP_SIZE" | sed 's/G$//' | awk '{print $1 * 1024}') local boot_mb=$(echo "$BOOT_SIZE" | sed 's/G$//' | awk '{print $1 * 1024}') local total_mb=$((root_mb + home_mb + swap_mb + boot_mb)) log "Total space required: ${total_mb}MB" # Get available space in VG local vg_free_mb=$(vgs --noheadings -o vg_free --units m "$VG_NAME" | tr -d ' m') local vg_free_mb_int=${vg_free_mb%.*} log "Available space in VG: ${vg_free_mb_int}MB" if [ "$total_mb" -gt "$vg_free_mb_int" ]; then warning "Total required space ($total_mb MB) exceeds available space ($vg_free_mb_int MB)" log "Reducing sizes proportionally..." # Reduce all sizes by 10% to leave space for snapshots ROOT_SIZE=$((root_mb * 85 / 100 / 1024))"G" HOME_SIZE=$((home_mb * 85 / 100 / 1024))"G" SWAP_SIZE=$((swap_mb * 85 / 100 / 1024))"G" BOOT_SIZE=$((boot_mb * 85 / 100 / 1024))"G" log "Adjusted sizes:" log " ROOT_SIZE: $ROOT_SIZE" log " HOME_SIZE: $HOME_SIZE" log " SWAP_SIZE: $SWAP_SIZE" log " BOOT_SIZE: $BOOT_SIZE" fi # Create logical volumes with space for snapshots log "Creating logical volume: root ($ROOT_SIZE)" lvcreate -L "$ROOT_SIZE" -n "$ROOT_LV" "$VG_NAME" || error "Failed to create root LV" log "Creating logical volume: home ($HOME_SIZE)" lvcreate -L "$HOME_SIZE" -n "$HOME_LV" "$VG_NAME" || error "Failed to create home LV" log "Creating logical volume: swap ($SWAP_SIZE)" lvcreate -L "$SWAP_SIZE" -n "$SWAP_LV" "$VG_NAME" || error "Failed to create swap LV" log "Creating logical volume: boot ($BOOT_SIZE)" lvcreate -L "$BOOT_SIZE" -n "$BOOT_LV" "$VG_NAME" || error "Failed to create boot LV" # Show LVM layout log "LVM layout created:" lvs "$VG_NAME" || warning "Failed to display LV layout" # Create filesystems on LVM volumes log "Creating ext4 filesystem on root LV" mkfs.ext4 -L "root" "/dev/$VG_NAME/$ROOT_LV" || error "Failed to create root filesystem" log "Creating ext4 filesystem on home LV" mkfs.ext4 -L "home" "/dev/$VG_NAME/$HOME_LV" || error "Failed to create home filesystem" log "Creating ext4 filesystem on boot LV" mkfs.ext4 -L "boot" "/dev/$VG_NAME/$BOOT_LV" || error "Failed to create boot filesystem" log "Creating swap on swap LV" 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..." local found_encrypted=false # Check each partition for encryption 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 found_encrypted=true log "Found encrypted partition: $part_device ($part_name)" local crypt_name="internal_${part_name}_luks" if ! cryptsetup status "$crypt_name" >/dev/null 2>&1; then echo "Please enter the password for encrypted $part_name partition ($part_device):" if cryptsetup open "$part_device" "$crypt_name"; then success "Unlocked $part_device as /dev/mapper/$crypt_name" # Update the partition reference to the decrypted device INTERNAL_PARTITIONS["$part_name"]="/dev/mapper/$crypt_name" # Update filesystem type of decrypted partition 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 $part_device" fi else log "Encrypted partition $part_device already unlocked" INTERNAL_PARTITIONS["$part_name"]="/dev/mapper/$crypt_name" fi fi done if [ "$found_encrypted" = false ]; then log "No encrypted partitions found" fi success "Encrypted partition handling completed" } mount_filesystems() { log "Mounting filesystems..." # Mount internal filesystems for part_name in "${!INTERNAL_PARTITIONS[@]}"; do local part_device="${INTERNAL_PARTITIONS[$part_name]}" local mount_point="$WORK_DIR/internal_$part_name" if [ ! -d "$mount_point" ]; then mkdir -p "$mount_point" fi log "Mounting $part_device to $mount_point" if mount "$part_device" "$mount_point"; then success "Mounted $part_name partition" PARTITION_MOUNTS["$part_name"]="$mount_point" else error "Failed to mount $part_device" fi done # Mount external LVM filesystems mount "/dev/$VG_NAME/$ROOT_LV" "$EXTERNAL_ROOT_MOUNT" mount "/dev/$VG_NAME/$HOME_LV" "$EXTERNAL_HOME_MOUNT" mount "/dev/$VG_NAME/$BOOT_LV" "$EXTERNAL_BOOT_MOUNT" # Mount EFI partition mkdir -p "$EXTERNAL_ROOT_MOUNT/boot/efi" mount "${EXTERNAL_DRIVE}1" "$EXTERNAL_ROOT_MOUNT/boot/efi" success "All filesystems mounted" } copy_system_data() { log "Copying system data (this will take a while)..." # Copy root filesystem if [ -n "${INTERNAL_PARTITIONS[root]}" ]; then log "Copying root filesystem..." local source_mount="${PARTITION_MOUNTS[root]}" # If no separate home partition exists, exclude /home during root copy local exclude_opts="" if [ -z "${INTERNAL_PARTITIONS[home]}" ]; then exclude_opts="--exclude=/home/*" fi rsync -avxHAX --progress $exclude_opts \ --exclude=/proc/* --exclude=/sys/* --exclude=/dev/* \ --exclude=/run/* --exclude=/tmp/* --exclude=/var/tmp/* \ "$source_mount/" "$EXTERNAL_ROOT_MOUNT/" success "Root filesystem copied" else error "No root partition found to copy" fi # Copy home filesystem (if separate partition exists) if [ -n "${INTERNAL_PARTITIONS[home]}" ]; then log "Copying home filesystem from separate partition..." local source_mount="${PARTITION_MOUNTS[home]}" rsync -avxHAX --progress "$source_mount/" "$EXTERNAL_HOME_MOUNT/" success "Home filesystem copied from separate partition" else log "Copying /home from root filesystem..." # Create home directory structure on external home partition if [ -d "${PARTITION_MOUNTS[root]}/home" ]; then rsync -avxHAX --progress "${PARTITION_MOUNTS[root]}/home/" "$EXTERNAL_HOME_MOUNT/" success "/home copied from root filesystem" else warning "No /home directory found in root filesystem" fi fi # Copy boot files if [ -n "${INTERNAL_PARTITIONS[boot]}" ]; then log "Copying boot files from separate boot partition..." local source_mount="${PARTITION_MOUNTS[boot]}" rsync -avxHAX --progress "$source_mount/" "$EXTERNAL_BOOT_MOUNT/" else log "Copying boot files from root filesystem..." if [ -d "${PARTITION_MOUNTS[root]}/boot" ]; then rsync -avxHAX --progress "${PARTITION_MOUNTS[root]}/boot/" "$EXTERNAL_BOOT_MOUNT/" else warning "No /boot directory found" fi fi success "System data copied successfully" } update_system_configuration() { log "Updating system configuration..." # Update fstab 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") cat > "$EXTERNAL_ROOT_MOUNT/etc/fstab" << EOF # /etc/fstab: static file system information. # # Use 'blkid' to print the universally unique identifier for a device; this may # be used with UUID= as a more robust way to name devices that works even if # disks are added and removed. See fstab(5). # # 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 # Update crypttab (remove old encrypted home entry since we're using LVM now) echo "# No encrypted partitions in LVM setup" > "$EXTERNAL_ROOT_MOUNT/etc/crypttab" success "System configuration updated" } install_bootloader() { log "Installing bootloader..." # Bind mount necessary filesystems for chroot mount --bind /dev "$EXTERNAL_ROOT_MOUNT/dev" mount --bind /proc "$EXTERNAL_ROOT_MOUNT/proc" mount --bind /sys "$EXTERNAL_ROOT_MOUNT/sys" mount --bind /run "$EXTERNAL_ROOT_MOUNT/run" # Update initramfs to include LVM support chroot "$EXTERNAL_ROOT_MOUNT" /bin/bash -c " echo 'GRUB_ENABLE_CRYPTODISK=y' >> /etc/default/grub update-initramfs -u -k all grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=debian --recheck update-grub " # Unmount bind mounts umount "$EXTERNAL_ROOT_MOUNT/dev" umount "$EXTERNAL_ROOT_MOUNT/proc" umount "$EXTERNAL_ROOT_MOUNT/sys" umount "$EXTERNAL_ROOT_MOUNT/run" success "Bootloader installed successfully" } create_lvm_snapshot_script() { log "Creating LVM snapshot backup script..." cat > "$EXTERNAL_ROOT_MOUNT/usr/local/bin/lvm-snapshot-backup.sh" << 'EOF' #!/bin/bash # LVM Snapshot Backup Script # Creates snapshots of LVM volumes for backup purposes set -e VG_NAME="system-vg" SNAPSHOT_SIZE="10G" BACKUP_DIR="/mnt/backup" create_snapshots() { echo "Creating LVM snapshots..." # Create snapshots lvcreate -L "$SNAPSHOT_SIZE" -s -n root-snapshot "/dev/$VG_NAME/root" lvcreate -L "$SNAPSHOT_SIZE" -s -n home-snapshot "/dev/$VG_NAME/home" echo "Snapshots created successfully" echo "root-snapshot: /dev/$VG_NAME/root-snapshot" echo "home-snapshot: /dev/$VG_NAME/home-snapshot" } mount_snapshots() { echo "Mounting snapshots..." mkdir -p "$BACKUP_DIR"/{root,home} mount "/dev/$VG_NAME/root-snapshot" "$BACKUP_DIR/root" mount "/dev/$VG_NAME/home-snapshot" "$BACKUP_DIR/home" echo "Snapshots mounted at $BACKUP_DIR" } remove_snapshots() { echo "Cleaning up snapshots..." umount "$BACKUP_DIR/root" 2>/dev/null || true umount "$BACKUP_DIR/home" 2>/dev/null || true lvremove -f "/dev/$VG_NAME/root-snapshot" 2>/dev/null || true lvremove -f "/dev/$VG_NAME/home-snapshot" 2>/dev/null || true echo "Snapshots removed" } case "$1" in create) create_snapshots ;; mount) mount_snapshots ;; remove) remove_snapshots ;; backup) create_snapshots mount_snapshots echo "Snapshots ready for backup at $BACKUP_DIR" echo "Run 'lvm-snapshot-backup.sh remove' when backup is complete" ;; *) echo "Usage: $0 {create|mount|remove|backup}" echo " create - Create snapshots only" echo " mount - Mount existing snapshots" echo " remove - Remove snapshots and unmount" echo " backup - Create and mount snapshots ready for backup" exit 1 ;; esac EOF chmod +x "$EXTERNAL_ROOT_MOUNT/usr/local/bin/lvm-snapshot-backup.sh" success "LVM snapshot backup script created" } repair_grub_bootloader() { log "Performing final GRUB repair to ensure bootability..." # This is based on the working simple_grub_repair.sh local temp_boot="/tmp/grub-repair-boot" local temp_efi="/tmp/grub-repair-efi" mkdir -p "$temp_boot" "$temp_efi" log "Mounting boot partitions for GRUB repair..." mount "/dev/$VG_NAME/boot" "$temp_boot" || { warning "Could not mount boot partition for GRUB repair" return 0 } mount "${EXTERNAL_DRIVE}1" "$temp_efi" || { warning "Could not mount EFI partition for GRUB repair" umount "$temp_boot" 2>/dev/null || true return 0 } log "Installing GRUB bootloader directly to external drive..." if grub-install --target=x86_64-efi \ --efi-directory="$temp_efi" \ --bootloader-id=debian \ --boot-directory="$temp_boot" \ --recheck \ "$EXTERNAL_DRIVE"; then success "GRUB bootloader installed successfully" # Verify installation if [ -f "$temp_efi/EFI/debian/grubx64.efi" ]; then success "GRUB EFI bootloader verified" else warning "GRUB EFI bootloader not found after installation" fi else warning "GRUB installation encountered issues" fi # Cleanup umount "$temp_boot" 2>/dev/null || true umount "$temp_efi" 2>/dev/null || true rmdir "$temp_boot" "$temp_efi" 2>/dev/null || true success "GRUB repair completed" } cleanup() { log "Cleaning up..." # Unmount external filesystems umount "$EXTERNAL_ROOT_MOUNT/boot/efi" 2>/dev/null || true umount "$EXTERNAL_ROOT_MOUNT" 2>/dev/null || true umount "$EXTERNAL_HOME_MOUNT" 2>/dev/null || true umount "$EXTERNAL_BOOT_MOUNT" 2>/dev/null || true # Unmount internal filesystems for part_name in "${!PARTITION_MOUNTS[@]}"; do local mount_point="${PARTITION_MOUNTS[$part_name]}" umount "$mount_point" 2>/dev/null || true done # Close encrypted partitions for part_name in "${!INTERNAL_PARTITIONS[@]}"; do local part_device="${INTERNAL_PARTITIONS[$part_name]}" if [[ "$part_device" == *"/dev/mapper/"* ]]; then local crypt_name=$(basename "$part_device") cryptsetup close "$crypt_name" 2>/dev/null || true fi done success "Cleanup completed" } main() { echo -e "${GREEN}=== LVM Migration Script ===${NC}" echo "This script will migrate your non-LVM system to LVM on an external M.2 drive" echo "Run this from a live USB system for best results" echo check_prerequisites detect_drives analyze_internal_system echo echo "Migration Summary:" echo " Source: $INTERNAL_DRIVE (non-LVM system)" echo " Target: $EXTERNAL_DRIVE (will become LVM system)" echo " Root size: $ROOT_SIZE" echo " Home size: $HOME_SIZE" echo " Swap size: $SWAP_SIZE" echo " Boot size: $BOOT_SIZE" echo confirm_action "WARNING: This will DESTROY all data on $EXTERNAL_DRIVE!" setup_work_directories backup_existing_external_data create_lvm_layout handle_encrypted_partitions mount_filesystems copy_system_data update_system_configuration install_bootloader create_lvm_snapshot_script repair_grub_bootloader cleanup success "Complete LVM migration with GRUB repair finished successfully!" echo echo -e "${GREEN}=== MIGRATION COMPLETE ===${NC}" echo "✅ System migrated to LVM on external M.2 drive" echo "✅ GRUB bootloader installed and repaired" echo "✅ Boot configuration updated" echo "✅ LVM snapshot backup system ready" echo echo -e "${BLUE}Next steps:${NC}" echo "1. Reboot your system" echo "2. Enter BIOS/UEFI settings and configure:" echo " • Set external M.2 as first boot device" echo " • Disable Secure Boot (if enabled)" echo " • Ensure UEFI mode is enabled" echo "3. Look for 'debian' entry in boot menu" echo "4. Boot from external drive - should work without reset loops!" echo echo -e "${GREEN}Your new LVM system features:${NC}" echo "• Instant snapshots: sudo /usr/local/bin/lvm-snapshot-backup.sh backup" echo "• Dynamic volume resizing" echo "• Advanced backup strategies with rollback" echo "• Original internal drive unchanged as fallback" echo echo -e "${YELLOW}🎉 One-button migration completed successfully!${NC}" echo "Everything is configured and ready to boot!" } # Trap to ensure cleanup on exit trap cleanup EXIT main "$@"