elemental

Building a Linux Virtual Machine Image with Elemental

This section provides an overview of how you build a Linux image that can include additional extensions using Elemental and the elemental3ctl command-line interface. The image can be used to boot a virtual machine and run a Linux operating system, such as openSUSE Tumbleweed, with custom configurations and extensions.

Prerequisites

Prepare the installation target

  1. Create a qcow2 disk with a size of 20GB:

     qemu-img create -f qcow2 example.qcow2 20G
    
  2. Associate the created virtual disk with a block device:

     sudo modprobe nbd && sudo qemu-nbd -c /dev/nbd0 example.qcow2
    
  3. Check for the block device:

     sudo lsblk /dev/nbd0
    

Prepare basic configuration

elemental3ctl can apply basic configuration and extensions at deployment time in the following ways:

Configuring through a system extension image

While we recommend reading through the complete document to understand system extensions in the context of elemental project, please refer to the “Create system extension images” section for specific steps. After that continue further with the “Configuring through a configuration script” section below.

Preparing the system extension image as an overlay

Overlay is the way of merging contents of a system extension onto the host system such that it all looks like a part of the host system.

  1. On the same level as example-extension/, create an overlays/var/lib/extensions directory:
     mkdir -p overlays/var/lib/extensions
    
  2. Based on the section you follow above, copy either the kubectl.x86-64.raw or the tools-1.0_1.0_x86-64.raw file to this directory.
  3. Create an archive from the overlay directory:
     tar -cavzf overlays.tar.gz -C overlays .
    

You have now prepared an archive containing a system extension image for use during the installation process. This adds the kubectl binary or strace package to the operating system after boot.

Configuring through a configuration script

The OS installation supports configurations through a script that will run in a chroot on the unpacked operating system after expanding the provided overlays archives.

Example configuration script

This configuration script applies the following set of configurations on the built image:

  1. Configures the password for the root user to linux.
  2. Sets up a oneshot type systemd.service that will list the contents of the /var/lib/extensions/ directory.

Steps:

  1. Create configuration script:

     cat <<- EOF > config.sh
     #!/bin/bash
    
     set -e
    
     # Set root user password
     echo "linux" | passwd root --stdin
    
     # Configure example systemd service
     cat <<- END > /etc/systemd/system/example-oneshot.service
     [Unit]
     Description=Example One-Shot Service
    
     [Service]
     Type=oneshot
     ExecStart=/bin/ls -alh /var/lib/extensions/
    
     [Install]
     WantedBy=multi-user.target
     END
    
     systemctl enable example-oneshot.service
     EOF
    
  2. Make config.sh executable:

     chmod +x config.sh
    

Install operating system on a target device

Once you run the below command, the virtual disk created as part of the Prepare the Installation Target section now holds a ready to boot image that will run openSUSE Tumbleweed and will be configured as described in the Prepare Basic Configuration section.

sudo elemental3ctl install \
  --overlay tar://overlays.tar.gz \
  --config config.sh \
  --os-image registry.opensuse.org/devel/unifiedcore/tumbleweed/containers/uc-base-os-kernel-default:latest \
  --target /dev/nbd0 \
  --cmdline "root=LABEL=SYSTEM console=ttyS0"

Note that:

NOTE: elemental3ctl also supports a --local flag that can be used in combination with the DOCKER_HOST=unix:///run/podman/podman.sock environment variable to allow for referring to locally pulled OS images.

In case you encounter issues with the process, make sure to enable the --debug flag for more information. If the issue persists and you are not aware of the problem, feel free to raise a GitHub Issue.

Mandatory cleanup before booting the image

Since you attached a block device to the virtual disk created in the Prepare the Installation Target section, detach the block device before booting the image:

sudo qemu-nbd -d /dev/nbd0

Starting the virtual machine image

To boot the image in a virtual machine, you can use either QEMU or libvirt utilities for creating the VM.

Using QEMU:

NOTE: Make sure you have qemu installed on your system. If not, you can install it using zypper install qemu-x86.

NOTE: If you are using a different architecture, make sure to adjust the qemu-system-x86_64 command accordingly.

NOTE: If you haven’t configured your user to be in the kvm group, you can run the command with sudo to allow QEMU to access the KVM acceleration.

qemu-system-x86_64 -m 8G \
         -accel kvm \
         -cpu host \
         -hda example.qcow2 \
         -bios /usr/share/qemu/ovmf-x86_64.bin \
         -nographic

You should see the bootloader prompting you to start openSUSE Tumbleweed.

Explore virtual machine

  1. Login with the root user and password as configured in the config.sh script.

  2. Check you are running the expected operating system:

     cat /etc/os-release
    
  3. Check that example-oneshot.service has run successfully:

    • View service status:

        systemctl status example-oneshot.service
      
    • View service logs:

        journalctl -u example-oneshot.service
      
  4. Check that elemental3ctl binary is available and working:

    • Check logs for the systemd-sysext.service:

        journalctl -u systemd-sysext.service
      
    • Try calling the command:

        elemental3ctl version
      

Create an Installer Media

Elemental supports creating installation media in the form of live ISOs or RAW disk images. Content-wise they both are almost the same. The difference is that ISO installs to a target disk device and the RAW disk resets to factory from a recovery partition.

The ISO image includes EFI binaries and bootloader setup, the OS image (as a squashfs image) and the installation assets (configuration script and drop-in files added over the OS).

The RAW image includes the ESP partition with the EFI binaries, the bootloader setup and a recovery partition including the OS image (again as a squashfs image) together with the installation assets.

Regardless of whether the artifact is an ISO or a RAW disk, the respective image boots like a live OS system based on tmpfs overlayfs. The boot process relies on the dmsquash-live dracut module for live booting.

To create a self installer image, you should prepare and include a specific set of configuration assets. These include:

  1. A configuration script
  2. Extensions to the installer media

Configure the Live Installer

The installer media supports configurations through a script which will run in late initramfs in a writeable system root.

There are some of relevant kernel parameters used by the installer to define the boot context. Those are not configurable and included by elemental3ctl when setting the bootloader.

These kernel parameters can be easily used to handle automated actions at boot like in the example below.

Example live configuration script

In this example, you prepare a configuration script that sets four aspects:

Create the script and make it executable:

cat <<- END > config-live.sh
#!/bin/bash

# Set autologin for the Live ISO
mkdir -p /etc/systemd/system/serial-getty@ttyS0.service.d

cat > /etc/systemd/system/serial-getty@ttyS0.service.d/override.conf << EOF
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear %I $TERM
EOF

mkdir -p /etc/systemd/system/getty@tty1.service.d

cat > /etc/systemd/system/getty@tty1.service.d/override.conf << EOF
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear %I $TERM
EOF

# Ensure extensions included in ISO's /extensions folder are loaded at boot
# ISO filesystem is mounted at /run/initramfs/live folder
rm -rf /run/extensions
ln -s /run/initramfs/live/extensions /run/extensions

# Set the elemental-autoinstall.service
cat > /etc/systemd/system/elemental-autoinstall.service << EOF
[Unit]
Description=Elemental Autoinstall
Wants=network-online.target
After=network-online.target
ConditionPathExists=/run/initramfs/live/Install/install.yaml
ConditionFileIsExecutable=/usr/local/bin/elemental3ctl

[Service]
Type=oneshot
ExecStart=/usr/local/bin/elemental3ctl --debug install
ExecStartPost=reboot

[Install]
WantedBy=multi-user.target
EOF

systemctl enable elemental-autoinstall.service

# Set the elemental-reset service
cat > /etc/systemd/system/elemental-reset.service << EOF
[Unit]
Description=Elemental Reset
After=multi-user.target
ConditionPathExists=/run/initramfs/live/Install/install.yaml
ConditionFileIsExecutable=/usr/local/bin/elemental3ctl
ConditionKernelCommandLine=elm.recovery
ConditionKernelCommandLine=elm.reset
OnSuccess=reboot.target
StartLimitIntervalSec=600
StartLimitBurst=3

[Service]
Type=oneshot
ExecStart=/usr/local/bin/elemental3ctl --debug reset
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl enable elemental-reset.service
END

chmod +x config-live.sh

Include Extensions in the Installer Media

The provided OS does not include the elemental3ctl required to run the installation to the target disk. The elemental3ctl is delivered through a systemd extension image. To ensure it is available at ISO boot, it has to be included in the ISO filesystem and either copied or linked to /run/extensions.

This example shows how to prepare the ISO overlay directory tree and the configuration script to ensure the elemental3ctl extensions are available and loaded at boot.

  1. Create an iso-overlay/extensions directory:

     mkdir -p iso-overlay/extensions
    
  2. Create the elemental3ctl extension image and move it to this directory:

     mv example-extension/mkosi.output/elemental3ctl-3.0.x86-64.raw iso-overlay/extensions
    
  3. Make sure the live configuration script links the extensions folder at /run/extensions

Build the Installer Image

If you do not have mcopy command on your system, install it using:

zypper in mtools

The command below creates an ISO image inside the build output directory. It will be using an openSUSE Tumbleweed image and will be configured to automatically self install to the target device (e.g. /dev/sda) at boot.

sudo elemental3ctl --debug build-installer \
    --type iso \
    --output build \
    --os-image registry.opensuse.org/devel/unifiedcore/tumbleweed/containers/uc-base-os-kernel-default:latest \
    --overlay dir://iso-overlay \
    --cmdline "console=ttyS0" \
    --config config-live.sh \
    --install-target /dev/sda \
    --install-overlay tar://overlays.tar.gz \
    --install-config config.sh \
    --install-cmdline "console=ttyS0"

In order to build a RAW disk image just use the same command as above but switching to RAW type (--type raw flag).

The RAW disk image only includes the ESP partition and a recovery partition. The recovery partition includes a squashfs OS image to boot from like a live ISO would.

Note that:

Booting an ISO Installer Image

NOTE: Make sure you have qemu installed on your system. If not, you can install it using zypper -n install qemu-x86. If you are using a different architecture, ensure the package name and respective command below are adjusted accordingly.

Launch a virtual machine to boot the installer ISO and verify the automated installation:

cp /usr/share/qemu/ovmf-x86_64-vars.bin .
qemu-system-x86_64 -m 8G \
         -accel kvm \
         -cpu host \
         -hda disk.img \
         -cdrom build/installer.iso \
         -drive if=pflash,format=raw,readonly,file=/usr/share/qemu/ovmf-x86_64-code.bin \
         -drive if=pflash,format=raw,file=ovmf-x86_64-vars.bin \
         -nographic

Note that:

Booting a RAW Installer Image

NOTE: Make sure you have qemu installed on your system. If not, you can install it using zypper -n install qemu-x86. If you are using a different architecture, ensure the package name and respective command below are adjusted accordingly.

To test the RAW installer image with QEMU, you need to either dump the image to a bigger image or expand the generated image.

cp /usr/share/qemu/ovmf-x86_64-vars.bin .
qemu-img resize build/installer.raw 16G

Launch a virtual machine to boot the installer RAW and verify, at boot, it self expands the partition table to fulfill the disk geometry and creates additional partitions.

qemu-system-x86_64 -m 8G \
         -accel kvm \
         -cpu host \
         -hda build/installer.raw \
         -drive if=pflash,format=raw,readonly,file=/usr/share/qemu/ovmf-x86_64-code.bin \
         -drive if=pflash,format=raw,file=ovmf-x86_64-vars.bin \
         -nographic

Note that:

Upgrading the OS of a Booted Image

Suppose the image that you created as part of the previous sections has been running for a while and now you want to upgrade its operating system to include the latest available package versions.

You can do this through the elemental3ctl command line tool, by executing the following command:

elemental3ctl upgrade --os-image registry.opensuse.org/devel/unifiedcore/tumbleweed/containers/uc-base-os-kernel-default:latest

After command completion, a new snapshot will be created:

localhost:~ # snapper list
 # | Type   | Pre # | Date                     | User | Used Space | Cleanup | Description                             | Userdata
---+--------+-------+--------------------------+------+------------+---------+-----------------------------------------+---------
0  | single |       |                          | root |            |         | current                                 |
1- | single |       | Wed Jul 16 12:57:23 2025 | root |  12.28 MiB |         | first root filesystem, snapshot 1       |
2+ | single |       | Wed Jul 16 13:00:13 2025 | root |  12.28 MiB | number  | snapshot created from parent snapshot 1 |

What’s left is to reboot the OS and select the latest snapshot from the grub menu. After the reboot, your snapshots should look similar to this:

localhost:~ # snapper list
 # | Type   | Pre # | Date                     | User | Used Space | Cleanup | Description                             | Userdata
---+--------+-------+--------------------------+------+------------+---------+-----------------------------------------+---------
0  | single |       |                          | root |            |         | current                                 |
1  | single |       | Wed Jul 16 12:57:23 2025 | root |  12.28 MiB |         | first root filesystem, snapshot 1       |
2* | single |       | Wed Jul 16 13:00:13 2025 | root |  12.28 MiB | number  | snapshot created from parent snapshot 1 |

The latest snapshot will be running on the latest version of the registry.opensuse.org/devel/unifiedcore/tumbleweed/containers/uc-base-os-kernel-default image and will still hold any previously defined configurations and/or extensions.