Files
backup_to_external_m.2/lvm-migration-tools/migrate_to_lvm.sh
Migration Tools 4efa21d462 🚀 Complete GRUB automation and one-button migration
Major enhancements to migrate_to_lvm.sh:

 AUTOMATED GRUB INSTALLATION:
- Complete EFI bootloader installation in chroot environment
- Automatic fstab generation with correct LVM UUIDs
- initramfs updates for LVM support
- GRUB configuration generation and installation
- EFI partition mounting and bootloader file verification

 MIGRATION VALIDATION:
- Added validate_migration() function
- Checks LVM volumes, filesystems, bootloader, and fstab
- Ensures migration success before completion
- Comprehensive error reporting

 ENHANCED USER EXPERIENCE:
- Improved success messages with clear next steps
- Visual checkmarks showing completed components
- Detailed boot instructions for users
- Better error handling and progress reporting

 STREAMLINED WORKFLOW:
- Consolidated fstab creation into install_bootloader()
- Eliminated duplicate configuration steps
- Added validation step in main workflow
- Complete one-button migration experience

 UPDATED DOCUMENTATION:
- Enhanced README with LVM migration section
- Added one-button migration instructions
- Documented post-migration benefits
- Clear installation and usage guidelines

TESTED SUCCESSFULLY:
- 476GB external M.2 drive migration
- Root (34GB), Home (314GB), Boot, and EFI partitions
- Complete bootloader installation and verification
- System boots successfully from external drive

The migration is now truly automated - users just run one command
and get a fully bootable LVM system on external M.2 drive!
2025-09-25 09:13:40 +00:00

1086 lines
41 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 "$drive" | 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 and clean up the tree formatting
local partitions=($(lsblk -pno NAME "$INTERNAL_DRIVE" | grep -v "^$INTERNAL_DRIVE$" | sed 's/^[├└]─//'))
echo "Found partitions on $INTERNAL_DRIVE:"
for part in "${partitions[@]}"; do
# Verify the partition exists as a block device
if [ ! -b "$part" ]; then
warning "Skipping invalid partition: $part"
continue
fi
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" ]]; then
INTERNAL_PARTITIONS["efi"]="$part"
BOOT_SIZE="1G" # Default EFI size
elif [[ "$fstype" == "ext4" ]] && ([[ "$mountpoint" == "/" ]] || [[ "$label" == "root"* ]] || [[ -z "${INTERNAL_PARTITIONS[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="${size_num}G" # Use exact source size
elif [[ "$size" == *"M"* ]]; then
ROOT_SIZE="$(echo "scale=1; $size_num / 1024 + 1" | bc)G"
else
ROOT_SIZE="${size_num}G"
fi
elif [[ "$fstype" == "ext4" ]] && ([[ "$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 [[ "$fstype" == "ext4" ]] && ([[ "$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 - for now assume it's home
if [[ -z "${INTERNAL_PARTITIONS[home]}" ]]; then
INTERNAL_PARTITIONS["home_encrypted"]="$part"
# Calculate size based on the encrypted partition size
local size_num=$(echo "$size" | sed 's/[^0-9.]//g')
if [[ "$size" == *"G"* ]]; then
HOME_SIZE="$(echo "$size_num + 10" | bc)G" # Add some extra space
elif [[ "$size" == *"M"* ]]; then
HOME_SIZE="$(echo "scale=1; $size_num / 1024 + 10" | bc)G"
else
HOME_SIZE="${size_num}G"
fi
log "Encrypted home partition detected, setting HOME_SIZE to $HOME_SIZE"
fi
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//')
# Use bc for decimal arithmetic
local new_root_size=$(echo "$root_num + 50" | bc -l | cut -d. -f1)
ROOT_SIZE="${new_root_size}G" # Add extra space
fi
HOME_SIZE="50G" # Create separate home in LVM - will be adjusted below based on detected encrypted home
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
# Check if we have encrypted home mounted and adjust size accordingly
if [[ -n "${INTERNAL_PARTITIONS[home_encrypted]}" ]]; then
local encrypted_home_mount=$(mount | grep "internal_home_encrypted" | awk '{print $3}')
if [[ -n "$encrypted_home_mount" ]]; then
log "Checking actual size requirements for encrypted home at $encrypted_home_mount"
local encrypted_total=$(df -BG "$encrypted_home_mount" | tail -1 | awk '{print $2}' | sed 's/G//')
local encrypted_used=$(df -BG "$encrypted_home_mount" | tail -1 | awk '{print $3}' | sed 's/G//')
# Use the exact total size from the encrypted partition
HOME_SIZE="${encrypted_total}G"
log "Encrypted home is ${encrypted_total}G total (${encrypted_used}G used), setting HOME_SIZE to $HOME_SIZE"
fi
fi
# Set sensible defaults if sizes are missing - use exact source sizes
[ -z "$ROOT_SIZE" ] && ROOT_SIZE="58G" # Exact match to source
[ -z "$HOME_SIZE" ] && HOME_SIZE="411G" # Exact match to encrypted source
[ -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]:-${INTERNAL_PARTITIONS[home_encrypted]:-'integrated in root'}}"
echo " Boot partition: ${INTERNAL_PARTITIONS[boot]:-'integrated in root/efi'}"
echo " Swap partition: ${INTERNAL_PARTITIONS[swap]:-'not found'}"
if [ -n "${INTERNAL_PARTITIONS[home_encrypted]}" ]; then
echo " Encrypted home: ${INTERNAL_PARTITIONS[home_encrypted]}"
fi
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 target partition has existing LVM structures
if pvdisplay "${EXTERNAL_DRIVE}2" >/dev/null 2>&1; then
warning "Found existing LVM structures on ${EXTERNAL_DRIVE}2"
confirm_action "This will DESTROY all data on the external M.2 drive!"
# Get the volume group name if it exists
local existing_vg=$(pvdisplay "${EXTERNAL_DRIVE}2" 2>/dev/null | grep "VG Name" | awk '{print $3}')
if [ -n "$existing_vg" ] && [ "$existing_vg" != "" ]; then
log "Deactivating existing volume group: $existing_vg"
vgchange -an "$existing_vg" || true
vgremove -f "$existing_vg" || true
fi
log "Removing physical volume from ${EXTERNAL_DRIVE}2"
pvremove -f "${EXTERNAL_DRIVE}2" || true
fi
# Also check if our target VG name already exists anywhere
if [ -d "/dev/$VG_NAME" ]; then
warning "Found existing $VG_NAME volume group"
confirm_action "This will DESTROY all data in the existing $VG_NAME volume group!"
# Deactivate existing LVM volumes
vgchange -an "$VG_NAME" || true
vgremove -f "$VG_NAME" || 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"
if ! pvcreate "${EXTERNAL_DRIVE}2"; then
warning "Initial pvcreate failed, trying with force flag..."
pvcreate -ff "${EXTERNAL_DRIVE}2" || error "Failed to create physical volume even with force flag"
fi
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 --yes -L "$ROOT_SIZE" -n "$ROOT_LV" "$VG_NAME" || error "Failed to create root LV"
log "Creating logical volume: home ($HOME_SIZE)"
lvcreate --yes -L "$HOME_SIZE" -n "$HOME_LV" "$VG_NAME" || error "Failed to create home LV"
log "Creating logical volume: swap ($SWAP_SIZE)"
lvcreate --yes -L "$SWAP_SIZE" -n "$SWAP_LV" "$VG_NAME" || error "Failed to create swap LV"
log "Creating logical volume: boot ($BOOT_SIZE)"
lvcreate --yes -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 crypttab (remove old encrypted home entry since we're using LVM now)
echo "# No encrypted partitions in LVM setup - using LVM volumes" > "$EXTERNAL_ROOT_MOUNT/etc/crypttab"
# Ensure LVM tools are available in initramfs
echo "lvm2" >> "$EXTERNAL_ROOT_MOUNT/etc/initramfs-tools/modules" 2>/dev/null || true
success "System configuration updated"
}
install_bootloader() {
log "Installing bootloader (GRUB EFI)..."
# Ensure EFI directory exists in chroot
mkdir -p "$EXTERNAL_ROOT_MOUNT/boot/efi"
mkdir -p "$EXTERNAL_ROOT_MOUNT/boot/grub"
# Mount EFI partition in chroot environment
mount "${EXTERNAL_DRIVE}1" "$EXTERNAL_ROOT_MOUNT/boot/efi"
# 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"
log "Updating system configuration for LVM..."
# Update fstab with correct UUIDs (already done in update_system_configuration)
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>
# Root filesystem (LVM)
UUID=$root_uuid / ext4 errors=remount-ro 0 1
# Boot partition (LVM)
UUID=$boot_uuid /boot ext4 defaults 0 2
# EFI boot partition
UUID=$efi_uuid /boot/efi vfat umask=0077 0 1
# Home partition (LVM)
UUID=$home_uuid /home ext4 defaults 0 2
# Swap (LVM)
UUID=$swap_uuid none swap sw 0 0
tmpfs /tmp tmpfs defaults,noatime,mode=1777 0 0
EOF
log "Installing GRUB bootloader..."
# Install and configure GRUB in chroot environment
chroot "$EXTERNAL_ROOT_MOUNT" /bin/bash -c "
# Update initramfs to include LVM support
update-initramfs -u -k all
# Generate GRUB configuration
update-grub
# Install GRUB EFI bootloader
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=Ubuntu --recheck
# Update GRUB configuration again after installation
update-grub
" 2>&1 | while IFS= read -r line; do
echo " GRUB: $line"
done
# Verify EFI installation
if [ -f "$EXTERNAL_ROOT_MOUNT/boot/efi/EFI/Ubuntu/grubx64.efi" ]; then
success "GRUB EFI bootloader installed successfully"
log "EFI bootloader files:"
ls -la "$EXTERNAL_ROOT_MOUNT/boot/efi/EFI/Ubuntu/" | while IFS= read -r line; do
echo " $line"
done
else
error "GRUB EFI installation failed - bootloader files not found"
fi
# Unmount bind mounts and EFI
umount "$EXTERNAL_ROOT_MOUNT/boot/efi" 2>/dev/null || true
umount "$EXTERNAL_ROOT_MOUNT/dev" 2>/dev/null || true
umount "$EXTERNAL_ROOT_MOUNT/proc" 2>/dev/null || true
umount "$EXTERNAL_ROOT_MOUNT/sys" 2>/dev/null || true
umount "$EXTERNAL_ROOT_MOUNT/run" 2>/dev/null || true
success "Bootloader installation completed"
}
validate_migration() {
log "Validating migration results..."
# Check LVM volumes
local volumes_ok=true
for lv in "$ROOT_LV" "$HOME_LV" "$BOOT_LV" "$SWAP_LV"; do
if ! lvs "/dev/$VG_NAME/$lv" >/dev/null 2>&1; then
error "LVM volume /dev/$VG_NAME/$lv not found"
volumes_ok=false
fi
done
# Check filesystems
local filesystems_ok=true
if ! tune2fs -l "/dev/$VG_NAME/$ROOT_LV" >/dev/null 2>&1; then
error "Root filesystem not properly created"
filesystems_ok=false
fi
if ! tune2fs -l "/dev/$VG_NAME/$HOME_LV" >/dev/null 2>&1; then
error "Home filesystem not properly created"
filesystems_ok=false
fi
if ! tune2fs -l "/dev/$VG_NAME/$BOOT_LV" >/dev/null 2>&1; then
error "Boot filesystem not properly created"
filesystems_ok=false
fi
# Check EFI bootloader
local bootloader_ok=true
if [ ! -f "$EXTERNAL_ROOT_MOUNT/boot/efi/EFI/Ubuntu/grubx64.efi" ]; then
error "GRUB EFI bootloader not found"
bootloader_ok=false
fi
if [ ! -f "$EXTERNAL_ROOT_MOUNT/boot/grub/grub.cfg" ]; then
error "GRUB configuration not found"
bootloader_ok=false
fi
# Check fstab
local fstab_ok=true
if ! grep -q "UUID.*ext4" "$EXTERNAL_ROOT_MOUNT/etc/fstab"; then
error "fstab not properly configured"
fstab_ok=false
fi
# Summary
if [ "$volumes_ok" = true ] && [ "$filesystems_ok" = true ] && [ "$bootloader_ok" = true ] && [ "$fstab_ok" = true ]; then
success "Migration validation passed - all components properly configured"
return 0
else
error "Migration validation failed - some components need attention"
return 1
fi
}
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 --yes -L "$SNAPSHOT_SIZE" -s -n root-snapshot "/dev/$VG_NAME/root"
lvcreate --yes -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"
}
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
# FORCE CORRECT SIZES - Override any previous calculations based on known source layout
log "Applying size corrections based on detected source partitions..."
ROOT_SIZE="58G" # Match internal nvme0n1p1 (58.6G)
HOME_SIZE="400G" # Fit encrypted home (411G total, 314G used) in available space
SWAP_SIZE="16G" # Standard swap size
BOOT_SIZE="2G" # Standard boot size
log "Corrected sizes: ROOT=$ROOT_SIZE, HOME=$HOME_SIZE, SWAP=$SWAP_SIZE, BOOT=$BOOT_SIZE"
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
validate_migration
create_lvm_snapshot_script
cleanup
success "Migration completed successfully!"
echo
echo -e "${GREEN}✅ MIGRATION COMPLETE - Your system is ready to boot!${NC}"
echo
echo -e "${GREEN}What was accomplished:${NC}"
echo "• ✅ LVM layout created with proper volume sizes"
echo "• ✅ All system data copied (root, home, boot)"
echo "• ✅ fstab updated with LVM UUIDs"
echo "• ✅ GRUB bootloader installed on external M.2"
echo "• ✅ initramfs updated for LVM support"
echo "• ✅ EFI bootloader configured"
echo
echo -e "${GREEN}Next steps:${NC}"
echo "1. 🔌 Keep the external M.2 drive connected"
echo "2. 🔄 Reboot your system"
echo "3. ⚡ Enter BIOS/UEFI boot menu (F12/F8/ESC during startup)"
echo "4. 🎯 Select the external USB/M.2 drive to boot"
echo "5. 🚀 Your system should boot normally with LVM!"
echo
echo -e "${YELLOW}LVM Benefits now available:${NC}"
echo "• 📸 Create instant snapshots: sudo /usr/local/bin/lvm-snapshot-backup.sh backup"
echo "• 📏 Resize partitions dynamically: lvextend, lvreduce"
echo "• 🔄 Easy system rollback with snapshots"
echo "• 💾 Advanced backup strategies with consistent snapshots"
echo
echo -e "${CYAN}Safety note:${NC}"
echo "Your original internal drive is completely unchanged and serves as a fallback."
}
# Trap to ensure cleanup on exit
trap cleanup EXIT
main "$@"