boot-fix/grub-repair.sh
2025-08-28 12:11:37 -07:00

1241 lines
44 KiB
Bash
Executable file

#!/bin/bash
# GRUB and EFI Repair Script
# Designed to run from a live ISO to repair an installed system
# Author: System Administrator
# Version: 1.0
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables
SCRIPT_NAME=$(basename "$0")
MOUNT_POINT="/mnt"
EFI_MOUNT_POINT="/mnt/boot/efi"
GRUB_CFG_PATH="/mnt/boot/grub/grub.cfg"
BACKUP_DIR="/tmp/grub-backup-$(date +%Y%m%d-%H%M%S)"
LOG_FILE=$(mktemp "/tmp/grub-repair-$(date +%Y%m%d-%H%M%S).XXXXXX.log" 2>/dev/null || echo "/tmp/grub-repair-$(date +%Y%m%d-%H%M%S).log")
EFI_PARTITION_PATH="" # Store detected EFI partition for reuse
# Function to print colored output
print_status() {
local color=$1
local message=$2
echo -e "${color}[$(date '+%Y-%m-%d %H:%M:%S')] ${message}${NC}" | tee -a "$LOG_FILE"
}
# Function to print help
show_help() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] COMMAND
GRUB and EFI Repair Script for Live ISO Recovery
Features:
- Automatically detects UEFI vs BIOS boot mode
- Installs appropriate GRUB target (x86_64-efi or i386-pc)
- Supports modern partition layouts (Fedora-style: /boot/efi, /boot, /)
OPTIONS:
-h, --help Show this help message
-v, --verbose Enable verbose output
-d, --device DEV Specify target device (e.g., /dev/sda)
-p, --partition N Specify partition number (default: 1)
-m, --mount PATH Specify mount point (default: /mnt)
-b, --backup Create backup before operations
-f, --force Force operations without confirmation
-l, --log FILE Specify log file path
COMMANDS:
detect Detect available disks and partitions
mount Mount target system
unmount Unmount target system
backup Create backup of current GRUB configuration
install-grub Install/reinstall GRUB
update-grub Update GRUB configuration
check-efi Check and repair EFI partition and boot entries
fix-boot Complete boot repair (mount + install + update)
status Show current status and mounted systems
clean Clean up temporary files and unmount
EXAMPLES:
# Detect available systems
$SCRIPT_NAME detect
# Mount system on /dev/sda1
$SCRIPT_NAME -d /dev/sda -p 1 mount
# Complete boot repair with backup
$SCRIPT_NAME -d /dev/sda -p 1 -b fix-boot
# Note: The partition number (-p) is a starting point; the script will
# automatically detect and mount the correct root partition if needed.
# Check and repair EFI only
$SCRIPT_NAME -d /dev/sda check-efi
EOF
}
# Function to validate partition number
validate_partition() {
local partition="$1"
# Check if partition is numeric
if [[ ! "$partition" =~ ^[0-9]+$ ]]; then
print_status "$RED" "Error: Partition number must be a positive integer: $partition"
exit 1
fi
# Check if partition number is reasonable (1-128)
if [[ "$partition" -lt 1 || "$partition" -gt 128 ]]; then
print_status "$RED" "Error: Partition number must be between 1 and 128: $partition"
exit 1
fi
echo "$partition"
}
# Function to sanitize user input paths
sanitize_path() {
local path="$1"
local description="$2"
# Check if path is absolute
if [[ "$path" != /* ]]; then
print_status "$RED" "Error: $description must be an absolute path: $path"
exit 1
fi
# Check for dangerous path components
if [[ "$path" == *".."* ]]; then
print_status "$RED" "Error: $description contains dangerous path components: $path"
exit 1
fi
# Check for null bytes or other dangerous characters
if [[ "$path" == *$'\0'* ]]; then
print_status "$RED" "Error: $description contains null bytes: $path"
exit 1
fi
echo "$path"
}
# Function to check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
print_status "$RED" "Error: This script must be run as root (use sudo)"
exit 1
fi
}
# Function to check if running from live ISO
check_live_iso() {
if [[ "$FORCE" == "true" ]]; then
print_status "$YELLOW" "Force mode enabled, skipping live ISO check"
return 0
fi
if ! grep -q "boot=live" /proc/cmdline 2>/dev/null; then
print_status "$YELLOW" "Warning: This doesn't appear to be running from a live ISO"
print_status "$YELLOW" "Continue anyway? (y/N)"
read -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
exit 1
fi
fi
}
# Function to detect boot mode
detect_boot_mode() {
# Check if we're running in UEFI mode
if [[ -d "/sys/firmware/efi" ]]; then
echo "uefi"
return 0
else
echo "bios"
return 0
fi
}
# Function to detect available disks and partitions
detect_systems() {
print_status "$BLUE" "Detecting available disks and partitions..."
# Show boot mode
local boot_mode=$(detect_boot_mode)
echo "Boot mode: $boot_mode"
echo ""
echo "Available disks:"
lsblk -d -o NAME,SIZE,TYPE,MOUNTPOINT
echo -e "\nAvailable partitions:"
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE,LABEL
echo -e "\nEFI partitions:"
local efi_found=false
for partition in $(blkid | grep -i "vfat\|fat32" | cut -d: -f1); do
if [[ -b "$partition" ]]; then
echo " $partition - $(blkid -s LABEL -o value "$partition" 2>/dev/null || echo 'no label')"
efi_found=true
fi
done
if [[ "$efi_found" == "false" ]]; then
echo "No EFI partitions found"
fi
echo -e "\nLinux partitions:"
local linux_found=false
for partition in $(blkid | grep -i "ext4\|xfs\|btrfs" | cut -d: -f1); do
if [[ -b "$partition" ]]; then
local fstype=$(blkid -s TYPE -o value "$partition" 2>/dev/null)
local size=$(lsblk -rno SIZE "$partition" 2>/dev/null)
echo " $partition - $fstype ($size)"
linux_found=true
fi
done
if [[ "$linux_found" == "false" ]]; then
echo "No Linux partitions found"
fi
}
# Function to detect Linux distribution
detect_distribution() {
local mount_point="$1"
# Try to detect distribution from /etc/os-release
if [[ -f "$mount_point/etc/os-release" ]]; then
local distro_name=$(grep "^NAME=" "$mount_point/etc/os-release" | cut -d'"' -f2 | tr '[:upper:]' '[:lower:]')
echo "$distro_name"
return 0
fi
# Fallback: check for distribution-specific files
if [[ -f "$mount_point/etc/debian_version" ]]; then
echo "debian"
return 0
elif [[ -f "$mount_point/etc/redhat-release" ]]; then
echo "fedora"
return 0
elif [[ -f "$mount_point/etc/arch-release" ]]; then
echo "arch"
return 0
elif [[ -f "$mount_point/etc/lsb-release" ]]; then
local distro=$(grep "^DISTRIB_ID=" "$mount_point/etc/lsb-release" | cut -d'=' -f2 | tr '[:upper:]' '[:lower:]')
echo "$distro"
return 0
fi
# Default fallback - will use generic GRUB bootloader ID
echo "generic_grub"
return 1
}
# Function to detect root partition
detect_root_partition() {
local device="$1"
# Get the parent disk name more robustly
local disk
if command -v lsblk >/dev/null 2>&1; then
# Use lsblk to get parent disk (more reliable for NVMe, etc.)
disk=$(lsblk -rno PKNAME "$device" 2>/dev/null | head -1)
# If no parent found, the device might be a disk itself
if [[ -z "$disk" ]]; then
# Check if the device itself is a disk (not a partition)
local device_type=$(lsblk -rno TYPE "$device" 2>/dev/null)
if [[ "$device_type" == "disk" ]]; then
disk="$device"
else
# Fallback: try to extract disk from device path
disk="${device%[0-9]*}"
fi
fi
else
# Fallback: try to extract disk from device path
disk="${device%[0-9]*}"
fi
if [[ -z "$disk" ]]; then
print_status "$YELLOW" "Warning: Could not determine parent disk for $device"
return 1
fi
# Capture blkid output once for efficiency
local blkid_output=$(blkid 2>/dev/null)
# Look for Linux root partitions (ext4, xfs, btrfs)
local root_partitions=""
# Scan all partitions on the disk
if command -v lsblk >/dev/null 2>&1; then
local all_partitions=$(lsblk -rno NAME "$disk" | grep -E "[0-9]+$")
for partition in $all_partitions; do
local full_path="/dev/$partition"
if [[ -b "$full_path" ]]; then
local fstype=$(echo "$blkid_output" | grep "$full_path:" | grep -o 'TYPE="[^"]*"' | cut -d'"' -f2)
if [[ "$fstype" == "ext4" || "$fstype" == "xfs" || "$fstype" == "btrfs" || "$fstype" == "zfs" || "$fstype" == "f2fs" ]]; then
root_partitions="$root_partitions $full_path"
fi
fi
done
fi
# If we found multiple Linux partitions, try to identify the root
if [[ -n "$root_partitions" ]]; then
# Look for partitions that might be root based on size and mount history
for partition in $root_partitions; do
# Check if this partition has a root filesystem structure
if [[ -b "$partition" ]]; then
# Try to mount temporarily to check for root filesystem structure
local temp_mount="/tmp/root-check-$$"
mkdir -p "$temp_mount"
if mount "$partition" "$temp_mount" 2>/dev/null; then
# Check for common root filesystem indicators
if [[ -d "$temp_mount/etc" && -d "$temp_mount/boot" && -d "$temp_mount/var" ]]; then
umount "$temp_mount"
rmdir "$temp_mount"
echo "$partition"
return 0
fi
umount "$temp_mount"
rmdir "$temp_mount"
fi
fi
done
# If we couldn't identify the root, return the largest partition (common for root)
local largest_partition=""
local largest_size=0
for partition in $root_partitions; do
if [[ -b "$partition" ]]; then
local size=$(lsblk -rno SIZE "$partition" 2>/dev/null | sed 's/[^0-9]//g')
if [[ -n "$size" && "$size" -gt "$largest_size" ]]; then
largest_size="$size"
largest_partition="$partition"
fi
fi
done
if [[ -n "$largest_partition" ]]; then
echo "$largest_partition"
return 0
fi
fi
# Fallback: try common partition numbers (2, 3, 4)
for part_num in 2 3 4; do
local test_partition="${disk}${part_num}"
if [[ -b "$test_partition" ]]; then
local fstype=$(echo "$blkid_output" | grep "$test_partition:" | grep -o 'TYPE="[^"]*"' | cut -d'"' -f2)
if [[ "$fstype" == "ext4" || "$fstype" == "xfs" || "$fstype" == "btrfs" || "$fstype" == "zfs" || "$fstype" == "f2fs" ]]; then
echo "$test_partition"
return 0
fi
fi
done
# If still no root partition found, return empty
return 1
}
# Function to detect EFI partition
detect_efi_partition() {
local device="$1"
# Get the parent disk name more robustly
local disk
if command -v lsblk >/dev/null 2>&1; then
# Use lsblk to get parent disk (more reliable for NVMe, etc.)
disk=$(lsblk -rno PKNAME "$device" 2>/dev/null | head -1)
# If no parent found, the device might be a disk itself
if [[ -z "$disk" ]]; then
# Check if the device itself is a disk (not a partition)
local device_type=$(lsblk -rno TYPE "$device" 2>/dev/null)
if [[ "$device_type" == "disk" ]]; then
disk="$device"
else
# Fallback: try to extract disk from device path
disk="${device%[0-9]*}"
fi
fi
else
# Fallback: try to extract disk from device path
disk="${device%[0-9]*}"
fi
if [[ -z "$disk" ]]; then
print_status "$YELLOW" "Warning: Could not determine parent disk for $device"
return 1
fi
# Capture blkid output once for efficiency
local blkid_output=$(blkid 2>/dev/null)
# First try to find EFI partition by filesystem type and label
local efi_partition=$(echo "$blkid_output" | grep -i "vfat\|fat32" | grep -i "efi\|boot" | grep "$disk" | head -1 | cut -d: -f1)
if [[ -n "$efi_partition" ]]; then
echo "$efi_partition"
return 0
fi
# Fallback: try common partition numbers (1, 2, 12)
for part_num in 1 2 12; do
local test_partition="${disk}${part_num}"
if [[ -b "$test_partition" ]]; then
local fstype=$(echo "$blkid_output" | grep "$test_partition:" | grep -o 'TYPE="[^"]*"' | cut -d'"' -f2)
if [[ "$fstype" == "vfat" || "$fstype" == "fat32" ]]; then
echo "$test_partition"
return 0
fi
fi
done
# Enhanced fallback: scan all partitions on the disk
if command -v lsblk >/dev/null 2>&1; then
local all_partitions=$(lsblk -rno NAME "$disk" | grep -E "[0-9]+$")
for partition in $all_partitions; do
local full_path="/dev/$partition"
if [[ -b "$full_path" ]]; then
local fstype=$(echo "$blkid_output" | grep "$full_path:" | grep -o 'TYPE="[^"]*"' | cut -d'"' -f2)
if [[ "$fstype" == "vfat" || "$fstype" == "fat32" ]]; then
echo "$full_path"
return 0
fi
fi
done
fi
# If still no EFI partition found, return empty
return 1
}
# Function to mount target system
mount_system() {
local device="$1"
local partition="$2"
if [[ -z "$device" ]]; then
print_status "$RED" "Error: Device not specified. Use -d option."
exit 1
fi
local partition_device="${device}${partition}"
print_status "$BLUE" "Mounting system from $partition_device to $MOUNT_POINT..."
# Check if partition exists
if [[ ! -b "$partition_device" ]]; then
print_status "$RED" "Error: Partition $partition_device does not exist"
exit 1
fi
# Create mount point if it doesn't exist
mkdir -p "$MOUNT_POINT"
# Check if the specified partition is actually a root partition
# Note: Modern Linux distributions often use non-standard partition layouts
# (e.g., Fedora-style: EFI=1, Boot=2, Root=3), so we need to be smart
# about detecting which partition is actually the root filesystem
local fstype=$(blkid -s TYPE -o value "$partition_device" 2>/dev/null)
if [[ "$fstype" != "ext4" && "$fstype" != "xfs" && "$fstype" != "btrfs" && "$fstype" != "zfs" && "$fstype" != "f2fs" ]]; then
print_status "$YELLOW" "Warning: Partition $partition_device has filesystem type '$fstype', which may not be a root partition"
print_status "$BLUE" "Attempting to detect the actual root partition..."
local detected_root=$(detect_root_partition "$device")
if [[ -n "$detected_root" ]]; then
print_status "$BLUE" "Detected root partition: $detected_root"
partition_device="$detected_root"
else
print_status "$YELLOW" "Could not detect root partition, proceeding with specified partition"
fi
fi
# Mount root partition
print_status "$BLUE" "Mounting root partition $partition_device..."
if ! mount "$partition_device" "$MOUNT_POINT"; then
print_status "$RED" "Error: Failed to mount root partition $partition_device"
exit 1
fi
# Mount necessary filesystems
print_status "$BLUE" "Mounting necessary filesystems..."
# Mount /proc
# Note: We explicitly unmount on failure rather than relying solely on the trap cleanup
# This provides immediate, targeted cleanup and prevents partial mount states
mkdir -p "$MOUNT_POINT/proc"
if ! mount --bind /proc "$MOUNT_POINT/proc"; then
print_status "$RED" "Error: Failed to mount /proc"
umount "$MOUNT_POINT"
exit 1
fi
# Mount /sys
if ! mount --bind /sys "$MOUNT_POINT/sys"; then
print_status "$RED" "Error: Failed to mount /sys"
umount "$MOUNT_POINT/proc"
umount "$MOUNT_POINT"
exit 1
fi
# Mount /dev
if ! mount --bind /dev "$MOUNT_POINT/dev"; then
print_status "$RED" "Error: Failed to mount /dev"
umount "$MOUNT_POINT/sys"
umount "$MOUNT_POINT/proc"
umount "$MOUNT_POINT"
exit 1
fi
# Mount /dev/pts
if ! mount --bind /dev/pts "$MOUNT_POINT/dev/pts"; then
print_status "$RED" "Error: Failed to mount /dev/pts"
umount "$MOUNT_POINT/dev"
umount "$MOUNT_POINT/sys"
umount "$MOUNT_POINT/proc"
umount "$MOUNT_POINT"
exit 1
fi
# Mount /dev/shm for shared memory access
if ! mount --bind /dev/shm "$MOUNT_POINT/dev/shm"; then
print_status "$YELLOW" "Warning: Failed to mount /dev/shm, continuing without it"
# Note: /dev/shm is optional but recommended for full chroot functionality
fi
# Mount /run for runtime data
if ! mount --bind /run "$MOUNT_POINT/run"; then
print_status "$YELLOW" "Warning: Failed to mount /run, continuing without it"
# Note: /run is optional but recommended for full chroot functionality
fi
# Mount EFI partition if it exists
local efi_partition=$(detect_efi_partition "$device")
if [[ -n "$efi_partition" && -b "$efi_partition" ]]; then
print_status "$BLUE" "Mounting EFI partition $efi_partition..."
mkdir -p "$EFI_MOUNT_POINT"
if ! mount "$efi_partition" "$EFI_MOUNT_POINT"; then
print_status "$RED" "Error: Failed to mount EFI partition $efi_partition"
# Note: We explicitly unmount on failure rather than relying solely on the trap cleanup
# This provides immediate, targeted cleanup and prevents partial mount states
umount "$MOUNT_POINT/dev/pts" 2>/dev/null || true
umount "$MOUNT_POINT/dev" 2>/dev/null || true
umount "$MOUNT_POINT/sys" 2>/dev/null || true
umount "$MOUNT_POINT/proc" 2>/dev/null || true
umount "$MOUNT_POINT" 2>/dev/null || true
exit 1
fi
# Store EFI partition path for potential reuse by other functions
EFI_PARTITION_PATH="$efi_partition"
else
print_status "$YELLOW" "Warning: EFI partition not found, some operations may fail"
fi
# Mount boot partition if it exists (modern Fedora-style layout)
# Note: Many modern distributions separate /boot from the root filesystem
# for security and performance reasons. We detect this automatically.
local boot_partition=""
local disk="${device%[0-9]*}"
# Look for a separate boot partition
if command -v lsblk >/dev/null 2>&1; then
local all_partitions=$(lsblk -rno NAME "$disk" | grep -E "[0-9]+$")
for partition in $all_partitions; do
local full_path="/dev/$partition"
if [[ -b "$full_path" ]]; then
local fstype=$(blkid -s TYPE -o value "$full_path" 2>/dev/null)
# Look for ext4/xfs boot partitions that are smaller than root
if [[ "$fstype" == "ext4" || "$fstype" == "xfs" ]]; then
local size=$(lsblk -rno SIZE "$full_path" 2>/dev/null | sed 's/[^0-9]//g')
local root_size=$(lsblk -rno SIZE "$partition_device" 2>/dev/null | sed 's/[^0-9]//g')
if [[ -n "$size" && -n "$root_size" && "$size" -lt "$root_size" && "$size" -gt 1000 ]]; then
# Additional check: verify this looks like a boot partition
local temp_mount="/tmp/boot-check-$$"
mkdir -p "$temp_mount"
if mount "$full_path" "$temp_mount" 2>/dev/null; then
# Check for boot-specific directories or files
if [[ -d "$temp_mount/grub2" || -d "$temp_mount/grub" || -d "$temp_mount/extlinux" || -d "$temp_mount/loader" ]]; then
umount "$temp_mount"
rmdir "$temp_mount"
boot_partition="$full_path"
break
fi
umount "$temp_mount"
rmdir "$temp_mount"
fi
fi
fi
fi
done
fi
if [[ -n "$boot_partition" ]]; then
print_status "$BLUE" "Mounting boot partition $boot_partition..."
mkdir -p "$MOUNT_POINT/boot"
if ! mount "$boot_partition" "$MOUNT_POINT/boot"; then
print_status "$YELLOW" "Warning: Failed to mount boot partition, continuing without it"
# Note: We don't exit here because the boot partition is optional
# The system can still function with just the root and EFI partitions
fi
fi
print_status "$GREEN" "System mounted successfully at $MOUNT_POINT"
}
# Function to unmount system
unmount_system() {
print_status "$BLUE" "Unmounting system..."
# Unmount in reverse order
if mountpoint -q "$EFI_MOUNT_POINT"; then
umount "$EFI_MOUNT_POINT"
print_status "$GREEN" "EFI partition unmounted"
fi
# Unmount boot partition if it was mounted separately
if mountpoint -q "$MOUNT_POINT/boot"; then
umount "$MOUNT_POINT/boot"
print_status "$GREEN" "Boot partition unmounted"
fi
if mountpoint -q "$MOUNT_POINT/run"; then
umount "$MOUNT_POINT/run"
fi
if mountpoint -q "$MOUNT_POINT/dev/shm"; then
umount "$MOUNT_POINT/dev/shm"
fi
if mountpoint -q "$MOUNT_POINT/dev/pts"; then
umount "$MOUNT_POINT/dev/pts"
fi
if mountpoint -q "$MOUNT_POINT/dev"; then
umount "$MOUNT_POINT/dev"
fi
if mountpoint -q "$MOUNT_POINT/sys"; then
umount "$MOUNT_POINT/sys"
fi
if mountpoint -q "$MOUNT_POINT/proc"; then
umount "$MOUNT_POINT/proc"
fi
if mountpoint -q "$MOUNT_POINT"; then
umount "$MOUNT_POINT"
print_status "$GREEN" "Root partition unmounted"
fi
print_status "$GREEN" "System unmounted successfully"
}
# Function to create backup
create_backup() {
print_status "$BLUE" "Creating backup of current GRUB configuration..."
mkdir -p "$BACKUP_DIR"
if [[ -f "$GRUB_CFG_PATH" ]]; then
cp "$GRUB_CFG_PATH" "$BACKUP_DIR/"
print_status "$GREEN" "GRUB config backed up to $BACKUP_DIR"
fi
if [[ -d "$EFI_MOUNT_POINT" ]]; then
cp -r "$EFI_MOUNT_POINT" "$BACKUP_DIR/"
print_status "$GREEN" "EFI directory backed up to $BACKUP_DIR"
fi
print_status "$GREEN" "Backup completed at $BACKUP_DIR"
}
# Function to install GRUB
install_grub() {
print_status "$BLUE" "Installing GRUB..."
# Note: We use mountpoint -q instead of just checking directory existence
# because mountpoint -q verifies the directory is actually mounted, not just exists
if [[ ! -d "$MOUNT_POINT" ]] || ! mountpoint -q "$MOUNT_POINT"; then
print_status "$RED" "Error: System not mounted. Mount first with 'mount' command."
exit 1
fi
# Detect boot mode
local boot_mode=$(detect_boot_mode)
print_status "$BLUE" "Detected boot mode: $boot_mode"
# Detect distribution for appropriate bootloader ID
local bootloader_id="grub"
local bootloader_label="GRUB"
if [[ -d "$MOUNT_POINT/etc" ]]; then
local detected_distro=$(detect_distribution "$MOUNT_POINT")
case "$detected_distro" in
*ubuntu*)
bootloader_id="ubuntu"
bootloader_label="Ubuntu"
;;
*fedora*|*redhat*|*centos*)
bootloader_id="fedora"
bootloader_label="Fedora/RHEL"
;;
*arch*)
bootloader_id="arch"
bootloader_label="Arch Linux"
;;
*debian*)
bootloader_id="debian"
bootloader_label="Debian"
;;
*)
# Use generic GRUB ID for other distributions
bootloader_id="grub"
bootloader_label="GRUB"
;;
esac
print_status "$BLUE" "Detected distribution: $detected_distro, using bootloader ID: $bootloader_id"
fi
# Chroot into the system
print_status "$BLUE" "Chrooting into system to install GRUB..."
# Install GRUB based on boot mode
if [[ "$boot_mode" == "uefi" ]]; then
# Check if EFI partition is mounted
if [[ ! -d "$EFI_MOUNT_POINT" ]] || ! mountpoint -q "$EFI_MOUNT_POINT"; then
print_status "$RED" "Error: EFI partition not mounted. Required for UEFI GRUB installation."
exit 1
fi
print_status "$BLUE" "Installing GRUB for UEFI mode with bootloader ID: $bootloader_id"
if chroot "$MOUNT_POINT" grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id="$bootloader_id" --recheck 2>&1; then
print_status "$GREEN" "GRUB installed successfully for UEFI mode"
else
print_status "$RED" "Error: UEFI GRUB installation failed"
print_status "$YELLOW" "Run 'chroot $MOUNT_POINT grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=$bootloader_id --recheck' manually for detailed error output"
exit 1
fi
else
# BIOS mode - install to MBR
print_status "$BLUE" "Installing GRUB for BIOS mode..."
# Get the disk device (remove partition number)
local disk_device=""
if command -v lsblk >/dev/null 2>&1; then
# Find the root partition and get its parent disk
local root_partition=$(findmnt -n -o SOURCE "$MOUNT_POINT" 2>/dev/null | head -1)
if [[ -n "$root_partition" ]]; then
disk_device=$(lsblk -rno PKNAME "$root_partition" 2>/dev/null | head -1)
if [[ -n "$disk_device" ]]; then
disk_device="/dev/$disk_device"
fi
fi
fi
# Fallback: extract disk from partition path string
if [[ -z "$disk_device" ]]; then
local root_partition=$(findmnt -n -o SOURCE "$MOUNT_POINT" 2>/dev/null | head -1)
if [[ -n "$root_partition" ]]; then
disk_device="${root_partition%[0-9]*}"
fi
fi
if [[ -z "$disk_device" ]]; then
print_status "$RED" "Error: Could not determine disk device for BIOS GRUB installation"
exit 1
fi
print_status "$BLUE" "Installing GRUB to disk: $disk_device"
if chroot "$MOUNT_POINT" grub-install --target=i386-pc --recheck "$disk_device" 2>&1; then
print_status "$GREEN" "GRUB installed successfully for BIOS mode"
else
print_status "$RED" "Error: BIOS GRUB installation failed"
print_status "$YELLOW" "Run 'chroot $MOUNT_POINT grub-install --target=i386-pc --recheck $disk_device' manually for detailed error output"
exit 1
fi
fi
}
# Function to update GRUB configuration
update_grub() {
print_status "$BLUE" "Updating GRUB configuration..."
# Note: We use mountpoint -q instead of just checking directory existence
# because mountpoint -q verifies the directory is actually mounted, not just exists
if [[ ! -d "$MOUNT_POINT" ]] || ! mountpoint -q "$MOUNT_POINT"; then
print_status "$RED" "Error: System not mounted. Mount first with 'mount' command."
exit 1
fi
# Update GRUB configuration
if chroot "$MOUNT_POINT" grub-mkconfig -o /boot/grub/grub.cfg 2>&1; then
print_status "$GREEN" "GRUB configuration updated successfully"
else
print_status "$RED" "Error: GRUB configuration update failed"
print_status "$YELLOW" "Run 'chroot $MOUNT_POINT grub-mkconfig -o /boot/grub/grub.cfg' manually for detailed error output"
exit 1
fi
}
# Function to check EFI partition
check_efi() {
local device="$1"
local temp_mount="" # Initialize temp_mount variable
print_status "$BLUE" "Checking EFI partition..."
if [[ -z "$device" ]]; then
print_status "$RED" "Error: Device not specified. Use -d option."
exit 1
fi
# Check boot mode first
local boot_mode=$(detect_boot_mode)
if [[ "$boot_mode" == "bios" ]]; then
print_status "$YELLOW" "System is running in BIOS mode - EFI operations not applicable"
print_status "$BLUE" "Skipping EFI partition check for BIOS system"
return 0
fi
# Check if EFI partition is already mounted (from mount_system)
if [[ -d "$EFI_MOUNT_POINT" ]] && mountpoint -q "$EFI_MOUNT_POINT"; then
print_status "$BLUE" "Using already mounted EFI partition at: $EFI_MOUNT_POINT"
# Check EFI contents
print_status "$BLUE" "EFI partition contents:"
ls -la "$EFI_MOUNT_POINT"
# Check for existing boot entries
if [[ -d "$EFI_MOUNT_POINT/EFI" ]]; then
print_status "$BLUE" "EFI directory contents:"
find "$EFI_MOUNT_POINT/EFI" -type f -name "*.efi" 2>/dev/null || echo "No EFI files found"
fi
else
# EFI partition not mounted, detect and mount temporarily
local efi_partition=$(detect_efi_partition "$device")
if [[ -z "$efi_partition" ]]; then
print_status "$RED" "Error: No EFI partition found on device $device"
exit 1
fi
print_status "$BLUE" "Mounting EFI partition temporarily for inspection..."
temp_mount="/tmp/efi-temp"
mkdir -p "$temp_mount"
mount "$efi_partition" "$temp_mount"
# Check EFI contents
print_status "$BLUE" "EFI partition contents:"
ls -la "$temp_mount"
# Check for existing boot entries
if [[ -d "$temp_mount/EFI" ]]; then
print_status "$BLUE" "EFI directory contents:"
find "$temp_mount/EFI" -type f -name "*.efi" 2>/dev/null || echo "No EFI files found"
fi
fi
# Attempt to repair boot entries if efibootmgr is available
if command -v efibootmgr >/dev/null 2>&1; then
print_status "$BLUE" "Attempting to repair EFI boot entries..."
# Get current boot entries
local current_entries=$(efibootmgr | grep -E "^Boot[0-9]+" | wc -l)
print_status "$BLUE" "Current EFI boot entries: $current_entries"
# Try to create a new boot entry for GRUB
local grub_efi_path=""
local grub_efi_name=""
local efi_base_path=""
# Determine the base path for EFI operations
if [[ -d "$EFI_MOUNT_POINT" ]] && mountpoint -q "$EFI_MOUNT_POINT"; then
efi_base_path="$EFI_MOUNT_POINT"
else
efi_base_path="$temp_mount"
fi
# Search for GRUB EFI binaries in common locations
for grub_path in "EFI/grub/grubx64.efi" "EFI/grub/grub.efi" "EFI/ubuntu/grubx64.efi" "EFI/fedora/grubx64.efi" "EFI/arch/grubx64.efi"; do
if [[ -f "$efi_base_path/$grub_path" ]]; then
grub_efi_path="$grub_path"
grub_efi_name=$(basename "$grub_path")
break
fi
done
if [[ -n "$grub_efi_path" ]]; then
print_status "$BLUE" "GRUB EFI file found: $grub_efi_path, attempting to create boot entry..."
# Get the partition number dynamically
local partition_number=$(echo "$efi_partition" | grep -o '[0-9]\+$')
if [[ -n "$partition_number" ]]; then
# Convert path to EFI format (backslashes)
local efi_path=$(echo "$grub_efi_path" | sed 's/\//\\/g')
# Try to detect distribution for better bootloader ID
local bootloader_id="grub"
local bootloader_label="GRUB"
# First try to detect from EFI path
case "$grub_efi_path" in
"EFI/ubuntu/"*)
bootloader_id="ubuntu"
bootloader_label="Ubuntu"
;;
"EFI/fedora/"*)
bootloader_id="fedora"
bootloader_label="Fedora"
;;
"EFI/arch/"*)
bootloader_id="arch"
bootloader_label="Arch Linux"
;;
"EFI/debian/"*)
bootloader_id="debian"
bootloader_label="Debian"
;;
*)
# Try to detect distribution from mounted system if available
if [[ -d "$MOUNT_POINT" ]] && mountpoint -q "$MOUNT_POINT"; then
local detected_distro=$(detect_distribution "$MOUNT_POINT")
case "$detected_distro" in
*ubuntu*)
bootloader_id="ubuntu"
bootloader_label="Ubuntu"
;;
*fedora*|*redhat*|*centos*)
bootloader_id="fedora"
bootloader_label="Fedora/RHEL"
;;
*arch*)
bootloader_id="arch"
bootloader_label="Arch Linux"
;;
*debian*)
bootloader_id="debian"
bootloader_label="Debian"
;;
*)
# Use generic GRUB ID for other distributions
bootloader_id="grub"
bootloader_label="GRUB"
;;
esac
else
# Use generic GRUB ID for other distributions
bootloader_id="grub"
bootloader_label="GRUB"
fi
;;
esac
print_status "$BLUE" "Creating boot entry with ID: $bootloader_id, Label: $bootloader_label"
efibootmgr -c -d "$efi_partition" -p "$partition_number" -l "$efi_path" -L "$bootloader_label" 2>/dev/null || print_status "$YELLOW" "Could not create boot entry for $bootloader_label"
else
print_status "$YELLOW" "Could not determine partition number for $efi_partition"
fi
else
print_status "$YELLOW" "No GRUB EFI binary found in common locations"
fi
else
print_status "$YELLOW" "efibootmgr not available, skipping boot entry repair"
fi
# Unmount temporary mount if we created one
if [[ -n "$temp_mount" ]] && mountpoint -q "$temp_mount"; then
umount "$temp_mount"
rmdir "$temp_mount"
fi
print_status "$GREEN" "EFI partition check and repair completed"
}
# Function to show status
show_status() {
print_status "$BLUE" "Current system status:"
echo "Mounted systems:"
# Use direct mountpoint checks for better performance instead of findmnt
# mountpoint -q is more efficient than spawning a new process with findmnt
if mountpoint -q "$MOUNT_POINT"; then
echo "Root system mounted at: $MOUNT_POINT"
fi
if mountpoint -q "$EFI_MOUNT_POINT"; then
echo "EFI system mounted at: $EFI_MOUNT_POINT"
fi
if ! mountpoint -q "$MOUNT_POINT" && ! mountpoint -q "$EFI_MOUNT_POINT"; then
echo "No systems mounted"
fi
echo -e "\nAvailable GRUB configurations:"
find /mnt -name "grub.cfg" 2>/dev/null || echo "No GRUB configs found"
echo -e "\nEFI partition status:"
if mountpoint -q "$EFI_MOUNT_POINT"; then
echo "EFI mounted at: $EFI_MOUNT_POINT"
ls -la "$EFI_MOUNT_POINT" 2>/dev/null || echo "Cannot list EFI contents"
else
echo "EFI not mounted"
fi
echo -e "\nBackup directory:"
if [[ -d "$BACKUP_DIR" ]]; then
echo "Backup available at: $BACKUP_DIR"
ls -la "$BACKUP_DIR"
else
echo "No backup created"
fi
}
# Function to clean up
cleanup() {
print_status "$BLUE" "Cleaning up..."
# Unmount if mounted
if mountpoint -q "$MOUNT_POINT" || mountpoint -q "$EFI_MOUNT_POINT"; then
unmount_system
fi
# Remove temporary mount points
rmdir "$MOUNT_POINT" 2>/dev/null || true
rmdir "$EFI_MOINT_POINT" 2>/dev/null || true
# Remove temporary mount directories created during detection
find /tmp -maxdepth 1 -name "root-check-*" -type d 2>/dev/null | xargs -r rm -rf
find /tmp -maxdepth 1 -name "boot-check-*" -type d 2>/dev/null | xargs -r rm -rf
find /tmp -maxdepth 1 -name "efi-temp" -type d 2>/dev/null | xargs -r rm -rf
# Remove backup directory if it exists
if [[ -d "$BACKUP_DIR" ]]; then
rm -rf "$BACKUP_DIR"
print_status "$BLUE" "Removed backup directory: $BACKUP_DIR"
fi
print_status "$GREEN" "Cleanup completed"
}
# Function to perform comprehensive cleanup (clean command)
comprehensive_clean() {
print_status "$BLUE" "Performing comprehensive cleanup..."
# First, run the standard cleanup
cleanup
# Additional cleanup operations for the clean command
# Remove old log files (older than 7 days)
print_status "$BLUE" "Removing old log files..."
find /tmp -maxdepth 1 -name "grub-repair-*.log" -mtime +7 2>/dev/null | xargs -r rm -f
# Remove old backup directories (older than 7 days)
print_status "$BLUE" "Removing old backup directories..."
find /tmp -maxdepth 1 -name "grub-backup-*" -mtime +7 -type d 2>/dev/null | xargs -r rm -rf
# Clean up any remaining temporary files
print_status "$BLUE" "Removing remaining temporary files..."
find /tmp -maxdepth 1 -name "grub-repair-*" -type f 2>/dev/null | xargs -r rm -f
# Reset global variables to clean state
EFI_PARTITION_PATH=""
print_status "$GREEN" "Comprehensive cleanup completed"
}
# Function to complete boot repair
fix_boot() {
local device="$1"
local partition="$2"
print_status "$BLUE" "Starting complete boot repair..."
# Show boot mode
local boot_mode=$(detect_boot_mode)
print_status "$BLUE" "Detected boot mode: $boot_mode"
# Show detected partitions for confirmation
print_status "$BLUE" "Detected partitions:"
local efi_partition=$(detect_efi_partition "$device")
if [[ -n "$efi_partition" ]]; then
print_status "$BLUE" " EFI partition: $efi_partition"
fi
local root_partition=$(detect_root_partition "$device")
if [[ -n "$root_partition" ]]; then
print_status "$BLUE" " Root partition: $root_partition"
fi
# Final confirmation before proceeding
print_status "$YELLOW" "About to perform the following operations:"
print_status "$BLUE" " 1. Mount system partitions"
print_status "$BLUE" " 2. Install GRUB for $boot_mode mode"
print_status "$BLUE" " 3. Update GRUB configuration"
if [[ "$boot_mode" == "uefi" ]]; then
print_status "$BLUE" " 4. Check and repair EFI boot entries"
fi
print_status "$BLUE" " 5. Unmount all partitions"
print_status "$YELLOW" "Continue with boot repair? (y/N)"
read -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
print_status "$BLUE" "Boot repair cancelled by user"
exit 0
fi
# Create backup if requested
if [[ "$CREATE_BACKUP" == "true" ]]; then
create_backup
fi
# Mount system
mount_system "$device" "$partition"
# Install GRUB
install_grub
# Update GRUB configuration
update_grub
# Check and repair EFI (only for UEFI systems)
if [[ "$boot_mode" == "uefi" ]]; then
check_efi "$device"
else
print_status "$BLUE" "Skipping EFI operations for BIOS system"
fi
print_status "$GREEN" "Boot repair completed successfully!"
print_status "$BLUE" "You can now reboot your system."
}
# Main script logic
main() {
# Initialize variables
local device=""
local partition="1"
local command=""
local CREATE_BACKUP="false"
local FORCE="false"
local VERBOSE="false"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
-v|--verbose)
VERBOSE="true"
shift
;;
-d|--device)
device=$(sanitize_path "$2" "Device path")
shift 2
;;
-p|--partition)
partition=$(validate_partition "$2")
shift 2
;;
-m|--mount)
MOUNT_POINT=$(sanitize_path "$2" "Mount point")
EFI_MOUNT_POINT="$MOUNT_POINT/boot/efi"
shift 2
;;
-b|--backup)
CREATE_BACKUP="true"
shift
;;
-f|--force)
FORCE="true"
shift
;;
-l|--log)
LOG_FILE=$(sanitize_path "$2" "Log file path")
shift 2
;;
-*)
print_status "$RED" "Unknown option: $1"
show_help
exit 1
;;
*)
command="$1"
shift
;;
esac
done
# Check if command is provided
if [[ -z "$command" ]]; then
print_status "$RED" "Error: No command specified"
print_status "$RED" "Use '$SCRIPT_NAME --help' to see available commands"
show_help
exit 1
fi
# Check if running as root
check_root
# Check if running from live ISO
check_live_iso
# Set verbose mode
if [[ "$VERBOSE" == "true" ]]; then
set -x
fi
# Validate and execute command
case "$command" in
detect)
detect_systems
;;
mount)
mount_system "$device" "$partition"
;;
unmount)
unmount_system
;;
backup)
create_backup
;;
install-grub)
install_grub
;;
update-grub)
update_grub
;;
check-efi)
check_efi "$device"
;;
fix-boot)
fix_boot "$device" "$partition"
;;
status)
show_status
;;
clean)
comprehensive_clean
;;
*)
print_status "$RED" "Error: Unknown command: $command"
show_help
exit 1
;;
esac
}
# Trap to cleanup on exit
trap cleanup EXIT
# Run main function with all arguments
main "$@"