Files
backup_to_external_m.2/migrate_to_lvm.sh
root 5b7cc3773c 🚀 Complete One-Button Migration with Integrated GRUB Repair
 MAJOR ENHANCEMENT: True one-button migration solution!

Features integrated into migrate_to_lvm.sh:
-  Complete LVM migration process
-  Automatic GRUB bootloader repair (fixes reset/reboot loops)
-  EFI bootloader installation and verification
-  Boot configuration updates
-  LVM snapshot system setup
-  Enhanced success messaging with clear next steps

Updated START_LVM_MIGRATION.sh:
- 🎯 Highlights one-button solution
-  Shows all integrated features
- 🚀 Emphasizes complete automation

No more separate GRUB repair needed - everything in one script!
Tested and working - fixes boot issues automatically.
2025-09-25 12:03:03 +02:00

969 lines
35 KiB
Bash
Executable File

#!/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).
#
# <file system> <mount point> <type> <options> <dump> <pass>
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 "$@"