bokov.me
This is my website, which is a place for my hobby projects. Feel free to look around.
Article

Using the Ubuntu Base image

Linux distributions usually ship with an installer. This installer is then flashed onto a pendrive / optical medium, and the system boots from this device to install the operating system. A linux installer will generally perform the following tasks:

  • Partition your disk
  • Copy root filesystem contents
  • chroot into the system, and run scripts to install packages, and setup the system
  • Prepare kernel, initrd/initramfs, and install the bootloader

Depending on the requirements, a system may support multiple types of booting, such as booting from the master boot record (aka legacy boot), or booting via the Unified Extensible Firmware Interface (aka UEFI). EFI is a special beast on its own, so I won't go into details about how to prepare a system for EFI installation.

What I will show in this article is how to perform the tasks an operating system installer does, without an installer. This allows you to create bootable virtual machine disks (that can be flashed for physical servers as well). I will also show how to create a container image from a root filesystem archive.

The Ubuntu Base archive

The Ubuntu Base archive contains a root filesystem for a complete functional Ubuntu system. It is meant to be a base for building container images, or cloud virtual machines. If you have a Linux OS running, you can download the base archive, extract it, and chroot into it (after a bit of a setup) to start playing around with Ubuntu:

#!/bin/bash
wget https://cdimage.ubuntu.com/ubuntu-base/releases/22.04/release/ubuntu-base-22.04-base-amd64.tar.gz
sudo mkdir -p /mnt/ubuntu.22.04
sudo tar -xvf ubuntu-base-22.04-base-amd64.tar.gz -C /mnt/ubuntu.22.04
sudo mkdir -p /mnt/ubuntu.22.04/etc
sudo cp /etc/resolv.conf /mnt/ubuntu.22.04/etc/resolv.conf
sudo mkdir -p /mnt/ubuntu.22.04/proc
sudo mkdir -p /mnt/ubuntu.22.04/sys
sudo mkdir -p /mnt/ubuntu.22.04/dev/pts
sudo mkdir -p /mnt/ubuntu.22.04/run
sudo mount -t proc none /mnt/ubuntu.22.04/proc
sudo mount -t sysfs none /mnt/ubuntu.22.04/sys
sudo mount --bind /dev /mnt/ubuntu.22.04/dev
sudo mount --bind /dev/pts /mnt/ubuntu.22.04/dev/pts
sudo mount --bind /run /mnt/ubuntu.22.04/run
sudo chroot /mnt/ubuntu.22.04

You can exit the chroot by pressing Ctrl+C, or running the exit command inside the chroot. To cleanup the chroot, use the following command:

#!/bin/bash
sudo umount -R /mnt/ubuntu.22.04/*
sudo rm -rf /mnt/ubuntu.22.04/

Creating a virtual machine disk

The rootfs in itself is not very useful, as in order to be able to boot into the operating system, we need a few other pieces as well:

  • We need the Linux kernel,
  • We need the initramfs,
  • And we need a bootloader

A physical disk also needs to be partitioned, before it can be consumed by a BIOS. I will show you how to create a bootable image, which uses the legacy boot procedure.

  • First, we will create an empty disk file
  • We will then create a loop device for the disk file
  • We will create a partition table in the drive image
  • We will then create loop devices for each partition
  • Then we will create the filesystems in each partition
  • Then we will mount the filesystems on the partitions to our host Linux
  • We will then copy files to the filesystems
  • Finally, we will install the MBR of our bootloader onto the disk image

Creating an empty disk image

You can use the dd command to create an image. I chose to create a 4GiB (4 * 1024 * 1024 * 1024 bytes) image with the name ubuntu.img in the current working directory.

#!/bin/bash
dd if=/dev/zero of=ubuntu.img bs=1M count=4096

Using loop devices

Loop devices allow us to access files as if they were block devices. This gives a transparent layer for partitioning, and filesystem management tools to access not only physically connected storage devices, but also allow virtual disks to be manipulated. The heart and soul of loop devices is the losetup command, which can be ran to create a loop device for a file.

#!/bin/bash
sudo losetup --find --show ubuntu.img

This command will output the name of your loop device, such as /dev/loop0. Now, instead of doing /dev/sda or /dev/sdaX for commands such as parted, we will instead use /dev/loop0 and /dev/loop0pX.

Creating a partition table

I have used this process in the context of embedded devices, where resources are constrained. A partition table for a server in a data center, or a personal computer will differ from what I will show you, and I also have to point out, that this method does not apply any encryption either, meaning it is not the most secure approach possible. Nevertheless, I will create the following partition table:

  • 16 MiB of empty space at the beginning of the disk
  • 512 MiB FAT32 partition for the bootloader, and kernel, and the initramfs
  • Rest is partitioned (~3.5GiB) for the root filesystem, as an ext4 filesystem

You can use the parted tool to create the partition table. The -s flag is used to run parted in script mode, allowing me to specify the partition layout in a single command.

#!/bin/bash
sudo parted -s /dev/loop0 mklabel gpt \
    mkpart primary fat32 16MiB 512MiB \
    mkpart primary ext4 512MiB 100% \
    set 1 boot on \
    set 1 legacy_boot on

Creating the filesystems

We will now create the filesystems in our partitions. Since we are using a GPT partition table, Linux will pick up the partitions, and create matching loop devices. For this, we have to first detach the current loop device:

#!/bin/bash
sudo losetup -d /dev/loop0

Now, we can use the -P flag on losetup to execute the partition scan when setting up the loop device:

#!/bin/bash
sudo losetup --find --show -P ubuntu.img

This command should have created the /dev/loopXp1 and /dev/loopXp2 devices. You can check this by running ls -la /dev | grep loop, and inspecting the output. In my example, the /dev/loop0p1 is my FAT32 partition, and /dev/loop0p2 is my ext4 partition.

$ ls -la /dev | grep loop
crw-------  1 root root  10, 237 Aug 19 07:29 loop-control
brw-------  1 root root   7,   0 Aug 19 08:27 loop0
brw-------  1 root root 259,   2 Aug 19 08:29 loop0p1
brw-------  1 root root 259,   3 Aug 19 08:29 loop0p2
brw-------  1 root root   7,   1 Aug 19 07:29 loop1
brw-------  1 root root   7,   2 Aug 19 07:29 loop2
brw-------  1 root root   7,   3 Aug 19 07:29 loop3
brw-------  1 root root   7,   4 Aug 19 07:29 loop4
brw-------  1 root root   7,   5 Aug 19 07:29 loop5
brw-------  1 root root   7,   6 Aug 19 07:29 loop6
brw-------  1 root root   7,   7 Aug 19 07:29 loop7

You can run the mkfs command to create the filesystems in these loop devices:

#!/bin/bash
sudo mkfs -t vfat /dev/loop0p1
sudo mkfs -t ext4 /dev/loop0p2

Mounting the filesystems

After the filesystems have been created, we can mount them into our running Linux host. I will create a single directory for mounting the root filesystem, and I will mount the boot partition inside this directory. This is required so that when installing a kernel in chroot, it is placed on the correct partition.

After the directories are created, it is the matter of a simple mount command to mount these filesystems.

#!/bin/bash
sudo mkdir -p /mnt/ubuntu.22.04.root
sudo mkdir -p /mnt/ubuntu.22.04.root/boot
sudo mount -t ext4 /dev/loop0p2 /mnt/ubuntu.22.04.root
sudo mount -t vfat /dev/loop0p1 /mnt/ubuntu.22.04.root/boot

You are now ready to copy files into your drive image!

Copying files

Using a simple tar command, we can extract the contents of the root filesystem into /mnt/ubuntu.22.04.root.

#!/bin/bash
sudo tar -xvf rootfs.tar -C /mnt/ubuntu.22.04.root

A proper operating system installer would at this point chroot into the root filesystem, and use scripts, and the package manager to setup the system. We will also do the same, in order to acquire a kernel for our image. This chrooting will also need a few preparation commands as well.

#!/bin/bash
sudo mkdir -p /mnt/ubuntu.22.04.root/etc
sudo cp /etc/resolv.conf /mnt/ubuntu.22.04.root/etc/resolv.conf
sudo mkdir -p /mnt/ubuntu.22.04.root/proc
sudo mkdir -p /mnt/ubuntu.22.04.root/sys
sudo mkdir -p /mnt/ubuntu.22.04.root/dev/pts
sudo mkdir -p /mnt/ubuntu.22.04.root/run
sudo mount -t proc none /mnt/ubuntu.22.04.root/proc
sudo mount -t sysfs none /mnt/ubuntu.22.04.root/sys
sudo mount --bind /dev /mnt/ubuntu.22.04.root/dev
sudo mount --bind /dev/pts /mnt/ubuntu.22.04.root/dev/pts
sudo mount --bind /run /mnt/ubuntu.22.04.root/run
sudo chroot /mnt/ubuntu.22.04.root

Now, inside the chroot, use apt to update the package catalog, and install a linux kernel. I will also install a few other packages, that will make life easier, and will be required for bootloader installation as well.

#!/bin/bash
apt -y update
apt -y install linux-headers-generic linux-image-generic nano whois extlinux syslinux-common

Installing the bootloader

There are many bootloaders you could use to boot Linux, for simple use-cases I prefer to use SysLinux. Specifically, I will use extlinux, which is a boot loader, that can load the configuration, and the kernel from a FAT32/ext2/ext3/ext4 filesystem. Since we are using the GPT partitioning scheme, SysLinux will automatically find the partition marked with the legacy_boot flag, and assume that our config resides on this partition. I will perform the bootloader installation inside a chroot on the drive image itself.

If you have not yet cleaned up the previous chroot, you can use it again. The following commands will create a bootloader config, install the bootloader, and install the MBR of the bootloader, all inside a chroot.

#!/bin/bash
# Write a setup script for running inside the chroot
cat > /mnt/ubuntu.22.04.root/setup.sh <<EOF
mkdir -p /boot/extlinux
cat > /boot/extlinux/extlinux.conf <<EOF2
DEFAULT mylinux
LABEL mylinux
  KERNEL /boot/vmlinuz
  INITRD /boot/initrd.img
  APPEND root=/dev/sda2 console=ttyS0,9600 console=tty0 ro splash
EOF2
# Install extlinux
extlinux --install /boot/extlinux
# Install extlinux MBR
dd bs=440 count=1 conv=notrunc /usr/lib/syslinux/mbr/gptmbr.bin of=/dev/loop0
EOF
# Make setup script runnable
chmod 777 /mnt/ubuntu.22.04.root/setup.sh
# Run setup script in chroot
chroot /mnt/ubuntu.22.04.root /bin/bash /setup.sh
# Delete setup script
rm /setup.sh

At this point, your drive image should be ready. You should now cleanup your environment, and detach all loop devices. I like to do the following:

#!/bin/bash
sync
umount -R /mnt/ubuntu.22.04.root
losetup -D

Trying out the image

You can use qemu to boot up your new operating system image. For example, you can use the following command to boot up a virtual machine:

qemu-system-x86_64 -device virtio-scsi-pci,id=scsi \
    -drive file=ubuntu.img,id=drive,if=none,format=raw \
    -device scsi-hd,drive=drive \
    -m 1G \
    -display curses
About me and this site