Booting an ISO image on your hard drive

Indroduction

Every once in a while, disaster strikes, and you need to recover from it, or maybe even, Murphy help you, re-install your operating system. Got backups?

You may already have appropriate ISO images on CD-ROM, DVD, or on a USB thumb drive. But you don't always have those to hand.

What we will do here is show you how to create a small partition, gather one or more ISO images, put them on that small partition, and create the stanzas so you can use GRUB to boot to those ISO images. We also provide a script, mk.iso.part.sh, to pull all this together. The hope is that, Murphy willing, a small partition, not mounted, and enough of GRUB to boot it will survive whatever catastrophe hits so that you can then use them to rescue yourself.

I use three different ISOs:

  • A recent Debian netinst installer, which you can also use as a rescue system.

  • Finnix, an excellent command line rescue distribution.

  • The GParted live CD, so I can have a graphical partition editor.

How To

The Partition

The first thing is to create a small partition on your hard drive. This should be large enough to hold the ISO images you want to use, plus a few smaller files. More on those shortly. The partition should have the label CDPART. The script will use this to detect the correct partition. If you don't like that, change it, and the script. I currently use a partition of roughly 2500 megabytes (2500M for the Debian installer). I lay down an ext2 (not ext4) file system, but GRUB can work with other file systems, not least FAT.

Gathering the ISOs

Put the ISO images in a directory where the installer script can find them. This is the value for the variable isoSource in the script.

How the Script Works

The first thing the script does is locate the desired partition. It searches for a partition with the label cdpart. You can override this with the command line option -n.

There are a few command line options. -d to install Debian, -f for Finnix, and -g for GPated. Or -a for all three.

-z will remove existing files related to a given image. So, for example, -zd will zap existing files for the Debian image, and install new ones. Without this option the script installs files not already on the target partition, or updates newer versions.

-n to specify a partition number. Use this to over-ride the search for a partition, or in case you want to use a partition with some other label. Give only the partition number, not the device name. E.g.: -n 23 Note: There is minimal error checking here.

-s to specify the file system. The script supports only FAT, specified with -s msdos, or the default, ext2. The script does not format the partition, but it does need to know the file system in order to specify the file system for /etc/fstab and the file system module GRUB should load. Note: there is currently no error checking here.

If the ISOs partition does not already have an entry in /etc/fstab, the script adds one and makes a suitable mount point under /media. Under qemu, virtual disks show up as device vdX, rather than the usual sdX. We adjust the fstab entry appropriately for virtual machines.

Then the code handles the three ISO images.

The Debian image comes with some extra goodies. In addition to the ISO image, we also copy in two checksum files, for sha256 and sha512 checksums.

We also copy in two preseed files. Preseeding is described in the Debian Installation Manual Appendix B. I use two preseed files. One, a.cfg, contains information common to all my installations, e.g. my user name and log-in password (suitably hashed, of course). The other, ${HOSTNAME}.cfg, contains information specific to a given machine, such as partitioning information, local NTP servers (if any), WiFi information for those machines that use WiFi, etc..

The preseed files are optional, but if you don't use them you might want to take the first menu entry out of the GRUB initialization file, 50_netinst.

The GRUB file for the Debian netinst image is 50_netinst. It creates a submenu for GRUB with three entries. One is for a preseeded installation, one with the standard netinst menu so you can install as you see fit, and one that boots directly into the rescue option.

All three use the GUI. If the graphical setup fails, take out the /gtk in the linux and initrd lines in 50_netinst, and re-run update-grub.

The Finnix and GParted sections of the code are similar, but without the preseed and checksum files.

Almost the last thing we do is to update GRUB. Check the output for any errors!

The Results

GRUB Menu

Theme and Variations

You don't have to use a separate partition. You could put them in /root. But that runs the risk that the ISOs will get clobbered along with the rest of the system.

The checksum and preseed files for the Debian image are optional; feel free to comment out or delete relevant code from the script and the GRUB configuration file.

GRUB has limited support for LVM. I did not investigate LVM for this purpose because I believe it would make the partition more fragile, more easily corrupted.

The Files

mk.iso.part.sh

The script.

#! /bin/bash

# A shell script to set up iso(s) on their own partition for rescue
# work.

# https://www.linuxjournal.com/content/grub-boot-iso See also:
# https://help.ubuntu.com/community/Grub2/ISOBoot/Examples
# https://www.linuxbabe.com/desktop-linux/boot-from-iso-files-using-grub2-boot-loader
# https://gparted.org/livehd.php
# https://gist.github.com/Pysis868/27203177bdef15fbb70c

doDebian=0
doFinnix=0
doGparted=0
zap=0

# Now find the partition for the CD images. This assumes that it has
# the label CDPART. The user can over-ride this with option -n.
partNo=$(blkid | grep CDPART | cut -d':' -f1 | cut -b9-)

# The file system on that partition. Your choices are msdos or ext2.
fileSys=ext2

help () {
    # shellcheck disable=SC2046
    printf "%s: Install isos and updated grub for them.\n" $(basename "$0");
    printf "Options: -a: all; -d: Debian netinst; -f: Finnix;\n";
    printf "         -g: Gparted; -n: Partition number; s: file System.\n";
    printf "         -z: zap existing files first.\n";
    exit 0;
}

if [ -z "$*" ] ; then
    help;
fi

while getopts "adfgn:s:z" opt; do
    case $opt in
        a ) doDebian=1;doFinnix=1;doGparted=1;;
        d ) doDebian=1;;
        f ) doFinnix=1;;
        g ) doGparted=1;;
        n ) partNo=$OPTARG;;
        s ) fileSys=$OPTARG;;
        z ) zap=1;;
        * ) help;;
    esac
done

if [ "${doDebian}" = 0 ] && [ "${doFinnix}" = 0 ] && [ "${doGparted}" = 0 ] ; then
    help
fi

if [ -z "${partNo}" ] ; then
    printf 'CD partition not detected!\n';
    help
fi

isos=/media/isos
grubd=/etc/grub.d

# If we haven't already, set up fstab to use the iso partition.
if ! grep "${isos}" /etc/fstab >> /dev/null ; then
    echo updating fstab.

    if [ "${fileSys}" != 'msdos' ] ; then
        cat >> /etc/fstab << FSTAB

# CDROM images
/dev/sda5   /media/isos     ext4       user,noauto   0       0
FSTAB

    else
        cat >> /etc/fstab << FSTAB

# CDROM images
/dev/sda5   /media/isos     vfat       user,noauto   0       0
FSTAB
    fi

    mkdir -p "${isos}" || { printf "Quiting! Can't make directory %s.\n" "${isos}" ; exit 1 ; }

    case $HOSTNAME in

        # virtual machines, e.g. debian.11
        debian.* | cdtest | testing )
            sed -i -e "s/\/dev\/sda5/\/dev\/vda${partNo}/g" /etc/fstab;;

        *)
            # N.B.: untested!
            sed -i -e "s/\/dev\/sda5/\/dev\/sda${partNo}/g" /etc/fstab;;

    esac
    systemctl daemon-reload
fi

if ! mount "${isos}" ; then
    printf "Couldn't mount %s! Exiting.\n" "${isos}"
    exit 2;
fi

mtpt=$(pwd)

isoSource=/home/charles/samba/debian.installations/isos

if [ ! -d "${isoSource}" ] ; then
    printf "IsoSource directory %s not mounted.\n" ${isoSource};
    exit 1;
fi

cd "${isoSource}" || { printf "Could not cd to directory %s.\n" "${isoSource}"; exit 1;}

# shellcheck disable=SC2046
finnixIso=$(basename $(ls finnix*.iso))
# shellcheck disable=SC2046
gpartedIso=$(basename $(ls gparted*.iso))
# shellcheck disable=SC2046
isoBase=$(basename $(ls debian*.iso) .iso)

# shellcheck disable=SC2210
if lsb_release -a 2>&1 | grep -i 'Debian GNU/Linux.*bookworm' >> /dev/null ; then
    release=bookworm
elif lsb_release -a 2>&1 | grep -i 'Debian GNU/Linux 11 (bullseye)' >> /dev/null ; then
    release=bullseye
elif lsb_release -a 2>&1 | grep -i 'Debian GNU/Linux .*trixie' >> /dev/null ; then
    release=trixie

else
    printf "We don't support this release.\n"
    exit
fi

machine=$(uname -m)

if [ "$machine" != "i686" ] && [ "$machine" != "x86_64" ] ; then
    printf "We don't support this machine type, %s.\n" "$machine"
    exit
fi

machineRelease="$machine$release"

printf "Release is %s. Machine is %s.\n" "$release" "$machine"
printf "isoBase is \"%s\".\nfinnixIso is \"%s\".\ngpartedIso is \"%s\".\n" \
       "${isoBase}" "${finnixIso}" "${gpartedIso}"

# select the netinst iso to install. Make sure these are current and
# present.
case "$machineRelease" in

    i686bullseye )
        # bullseye i386 machines.
        installer=install.386
        ;;

    x86_64bullseye )
        # bullseye AMD64
        installer=install.amd64
        ;;

    i686bookworm )
        # bookworm i386
        installer=install.386
        ;;

    x86_64bookworm )
        # bookworm AMD64
        installer=install.amd64
        ;;

    x86_64trixie )
        # trixie AMD64
        installer=install.amd
        ;;

    * )
        printf "We don't support this release or this machine type.\n"
        exit
esac

if [ "${doDebian}" -gt 0 ] ; then
    iso=${isoBase}.iso
    checksums256=${isoBase}.sha256sums
    checksums512=${isoBase}.sha512sums

    if [ ! -e "${iso}" ] ; then
        printf "File %s does not exist here!" "${iso}"
        exit
    fi

    if [ ! -e "${checksums256}" ] ; then
        printf "File %s does not exist here!" "${checksums256}"
        exit
    fi

    if [ ! -e "${checksums512}" ] ; then
        printf "File %s does not exist here!" "${checksums512}"
        exit
    fi

    if [ "${zap}" != 0 ] ; then
        if [ -e "${grubd}"/50_netinst ] ; then
            rm "${grubd}"/50_netinst
        fi

        if [ -e "${isos}/${iso}" ] ; then
            rm "${isos}/${iso}"
        fi

        if [ -e "${isos}/${checksums256}" ] ; then
            rm "${isos}/${checksums256}"
        fi

        if [ -e "${isos}/${checksums512}" ] ; then
            rm "${isos}/${checksums512}"
        fi
    fi

    # netinst grub stanza
    if [ "${mtpt}/isos/50_netinst" -nt "${grubd}"/50_netinst ] ; then
        printf "Copying in 50_netinst.\n"
        cp -rp "${mtpt}/isos/50_netinst" "${grubd}"/
        chown root:root "${grubd}"/50_netinst
        chmod u+x "${grubd}"/50_netinst
        sed -i -e"s/set isofile=.*/set isofile=\/${iso}/g" "${grubd}"/50_netinst
        sed -i -e"s/install.amd/${installer}/g" "${grubd}"/50_netinst
        sed -i -e"s/preseed.cfg/${HOSTNAME}.cfg/g" "${grubd}"/50_netinst

        # partition number
        if [ "${partNo}" != 5 ] ; then
            sed -i -e"s/set root='hd0,[[:digit:]]*'/set root='hd0,$partNo'/g" "${grubd}"/50_netinst
        fi

        # file system
        if [ "${fileSys}" != 'msdos' ] ; then
            sed -i -e"s/insmod part_msdos/insmod ext2/g" "${grubd}"/50_netinst
        fi

        # bullseye or bookworm?
        if lsb_release -a | grep -i 'Debian GNU/Linux bookworm' >> /dev/null ; then
            sed -i -e 's/Debian 13 OS/Debian 12 OS/g;' "${grubd}"/50_netinst
        fi
    fi

    if [ ! -e "${isos}/${iso}" ] || [ "${iso}" -ot "${isos}/${iso}" ] ; then
        printf "Copying in netinst iso.\n"
        cp -rp "${iso}" "${checksums256}" "${checksums512}" "${isos}"
        chown root:root "${isos}/${isoBase}".*
        if [ ! -e "${isos}/${HOSTNAME}.cfg" ] ||
               [ "${mtpt}/${HOSTNAME}.cfg" -ot "${isos}/${HOSTNAME}.cfg" ] ; then
            printf "Copying in preseed files.\n"
            cp -rp "${mtpt}/${HOSTNAME}.cfg" "${mtpt}/a.cfg" "${isos}"
        fi
    fi
fi

if [ "${doFinnix}" -gt 0 ] ; then

    if [ "${zap}" != 0 ] ; then
        if [ -e "${grubd}"/52_finnix ] ; then
            rm "${grubd}"/52_finnix
        fi

        if [ -e "${isos}/${finnixIso}" ] ; then
            rm "${isos}/${finnixIso}"
        fi
    fi

    # finnix grub stanza
    if [ "${mtpt}/isos/52_finnix" -nt "${grubd}"/52_finnix ] ; then
        printf "Copying in 52_finnix.\n"
        cp -rp "${mtpt}/isos/52_finnix" "${grubd}"/
        sed -i -e"s/set isofile=.*/set isofile=\/${finnixIso}/g" "${grubd}"/52_finnix
        chown root:root "${grubd}"/52_finnix
        chmod u+x "${grubd}"/52_finnix

        # partition number
        if [ "${partNo}" != 5 ] ; then
            sed -i -e"s/set root='hd0,[[:digit:]]*'/set root='hd0,$partNo'/g" "${grubd}"/52_finnix
        fi

        # file system
        if [ "${fileSys}" != 'msdos' ] ; then
            sed -i -e"s/insmod part_msdos/insmod ext2/g" "${grubd}"/52_finnix
        fi

    fi

    if [ ! -e "${isos}/${finnixIso}" ] || [ "${finnixIso}" -ot "${isos}/${finnixIso}" ] ; then
        printf "Copying in finnix iso.\n"
        cp -rp "${finnixIso}" "${isos}"
        chown root:root "${isos}/${finnixIso}"
    fi
fi

if [ "${doGparted}" -gt 0 ] ; then

    if [ "${zap}" != 0 ] ; then
        if [ -e "${grubd}"/51_gparted ] ; then
            rm "${grubd}"/51_gparted
        fi

        if [ -e "${isos}/${gpartedIso}" ] ; then
            rm "${isos}/${gpartedIso}"
        fi
    fi

    # gparted grub stanza
    if [ "${mtpt}/isos/51_gparted" -nt "${grubd}"/51_gparted ] ; then
        printf "Copying in 51_gparted.\n"
        cp -rp "${mtpt}/isos/51_gparted" "${grubd}"/
        sed -i -e"s/set isofile=.*/set isofile=\/${gpartedIso}/g" "${grubd}"/51_gparted
        chown root:root "${grubd}"/51_gparted
        chmod u+x "${grubd}"/51_gparted

        # partition number
        if [ "${partNo}" != 5 ] ; then
            sed -i -e"s/set root='hd0,[[:digit:]]*'/set root='hd0,$partNo'/g" "${grubd}"/51_gparted
        fi

        # file system
        if [ "${fileSys}" != 'msdos' ] ; then
            sed -i -e"s/insmod part_msdos/insmod ext2/g" "${grubd}"/51_gparted
        fi
    fi

    if [ ! -e "${isos}/${gpartedIso}" ] || [ "${gpartedIso}" -ot "${isos}/${gpartedIso}" ] ; then
        printf "Copying in gparted iso.\n"
        cp -rp "${gpartedIso}" "${isos}"
        chown root:root "${isos}/${gpartedIso}"
    fi
fi

printf "\nUpdating grub:\n"
update-grub

printf "\nBe sure to umount the ISO partition to protect it.\n";
printf 'Like so: "umount %s".\n' "${isos}"

50_netinst

The GRUB initialization file for Debian.

#!/bin/sh
exec tail -n +3 $0
# This file provides an easy way to add custom menu entries.  Simply type the
# menu entries you want to add after this comment.  Be careful not to change
# the 'exec tail' line above.

# If the graphical setup fails, take out the /gtk in the linux and
# initrd lines below.
submenu "Debian Netinst Recovery or Installer" --class linux --class gnu-linux --class gnu --class os {
    menuentry "Install Debian 13 OS (Preseeded)" {
        set root='hd0,5'
        set isofile="/iso.file.here"
        insmod part_msdos
        insmod loopback
        loopback loop (hd0,5)$isofile
        linux (loop)/install.amd/gtk/vmlinuz auto=true file=/media/preseed.cfg
        initrd (loop)/install.amd/gtk/initrd.gz
    }

    menuentry "Install Debian 13 OS (Expert)" {
        set root='hd0,5'
        set isofile="/iso.file.here"
        insmod part_msdos
        insmod loopback
        loopback loop (hd0,5)$isofile
        linux (loop)/install.amd/gtk/vmlinuz
        initrd (loop)/install.amd/gtk/initrd.gz
    }

    menuentry "Install Debian 13 OS (Rescue)" {
        set root='hd0,5'
        set isofile="/iso.file.here"
        insmod part_msdos
        insmod loopback
        loopback loop (hd0,5)$isofile
        linux (loop)/install.amd/gtk/vmlinuz rescue/enable=true
        initrd (loop)/install.amd/gtk/initrd.gz
    }
}

51_gparted

The GRUB initialization file for GParted.

#!/bin/sh
exec tail -n +3 $0
# This file provides an easy way to add custom menu entries.  Simply type the
# menu entries you want to add after this comment.  Be careful not to change
# the 'exec tail' line above.

menuentry "Gparted live 1.6" {
      set root='hd0,5'
      set isofile="/iso.file.here"
      insmod part_msdos
      insmod loopback
      loopback loop $isofile
      linux (loop)/live/vmlinuz boot=live config union=overlay username=user components noswap noeject vga=788 ip= net.ifnames=0 toram=filesystem.squashfs findiso=$isofile
      initrd (loop)/live/initrd.img
}

52_finnix

The GRUB initialization file for Finnix.

#!/bin/sh
exec tail -n +3 $0
# This file provides an easy way to add custom menu entries.  Simply type the
# menu entries you want to add after this comment.  Be careful not to change
# the 'exec tail' line above.

menuentry "finnix " {
    set root='hd0,5'
    set isofile="/iso.file.here"
    insmod part_msdos
    insmod loopback
    loopback loop (hd0,5)$isofile
    linux (loop)/live/vmlinuz findiso=$isofile vga=791 nomodeset --
    initrd (loop)/live/initrd.img
}

Credits

Among the web sites I looked at while working on this:

The kernel command line for the GParted GRUB stanza came from the GParted web site.

GRUB Boot from ISO, Linux Journal, March 14, 2017

Grub2/ISOBoot/Examples has some ideas for other ISO images you might want to use.

How to Boot ISO Files From GRUB2 Boot Loader

Pysis868/grub.cfg A sample grub.cfg: more ideas for customizing the GRUB menu

blogroll

social