T7453: Enhance raw/qcow2 image creation

Description
This pull request introduces improvements to the raw_image.py script responsible for building raw disk images in the VyOS build process.

Main Changes
Added use of kpartx to reliably map EFI and root partitions from the loop device.
Introduced disk_details as an attribute on the BuildContext object to pass partition metadata through the image build steps.
Improved the __exit__ method for BuildContext to unmount all mount points and clean up kpartx mappings and loop devices, even in failure cases.
Fixed a crash in mount_image() when con.disk_details was not set.
Added useful debug logs for loop device usage and partition mapping.
Motivation
The previous implementation assumed partitions like /dev/loopXp3 would appear automatically, which is unreliable across some environments (especially containers or newer systems).

This PR makes the process more reliable by explicitly mapping partitions with kpartx, a tool designed for this purpose.

It also ensures proper resource cleanup by unmounting and detaching everything cleanly, preventing leaked loop devices or stale mount points.

Test Instructions

Flavor : cloud-init.toml
packages = [
  "cloud-init",
  "qemu-guest-agent"
]

image_format = ["qcow2"]
disk_size = 10

[boot_settings]
console_type = "ttyS0"

Run:

sudo ./build-vyos-image --architecture amd64 \
  --build-by "you@example.com" \
  --reuse-iso vyos-1.5-rolling-*.iso \
  cloud-init
Expected behavior:

The build completes without errors.
The .qcow2 image file is generated and bootable (e.g., in KVM or Proxmox).
Partitions are mounted correctly via /dev/mapper/loopXp*.

Signed-off-by: Gabin-CC <gabin.laurent@rte-international.com>
This commit is contained in:
Gabin-CC 2025-06-05 03:37:56 +02:00
parent 8350580ac5
commit 1cda2d42bb

View File

@ -63,22 +63,38 @@ class BuildContext:
return self
def __exit__(self, exc_type, exc_value, exc_tb):
def __exit__(self, exc_type, exc_value, traceback):
print(f"I: Tearing down the raw image build environment in {self.work_dir}")
cmd(f"""umount {self.squash_dir}/dev/""")
cmd(f"""umount {self.squash_dir}/proc/""")
cmd(f"""umount {self.squash_dir}/sys/""")
cmd(f"umount {self.squash_dir}/boot/efi")
cmd(f"umount {self.squash_dir}/boot")
cmd(f"""umount {self.squash_dir}""")
cmd(f"""umount {self.iso_dir}""")
cmd(f"""umount {self.raw_dir}""")
cmd(f"""umount {self.efi_dir}""")
for mount in [
f"{self.squash_dir}/dev/",
f"{self.squash_dir}/proc/",
f"{self.squash_dir}/sys/",
f"{self.squash_dir}/boot/efi",
f"{self.squash_dir}/boot",
f"{self.squash_dir}",
f"{self.iso_dir}",
f"{self.raw_dir}",
f"{self.efi_dir}"
]:
if os.path.ismount(mount):
try:
cmd(f"umount {mount}")
except Exception as e:
print(f"W: Failed to umount {mount}: {e}")
# Remove kpartx mappings
if self.loop_device:
cmd(f"""losetup -d {self.loop_device}""")
mapper_base = os.path.basename(self.loop_device)
try:
cmd(f"kpartx -d {self.loop_device}")
except Exception as e:
print(f"W: Failed to remove kpartx mappings for {mapper_base}: {e}")
try:
cmd(f"losetup -d {self.loop_device}")
except Exception as e:
print(f"W: Failed to detach loop device {self.loop_device}: {e}")
def create_disk(path, size):
cmd(f"""qemu-img create -f raw "{path}" {size}G""")
@ -106,14 +122,23 @@ def setup_loop_device(con, raw_file):
def mount_image(con):
import vyos.system.disk
from subprocess import Popen, PIPE, STDOUT
from re import match
try:
root = con.disk_details.partition['root']
efi = con.disk_details.partition['efi']
except (AttributeError, KeyError):
raise RuntimeError("E: No valid root or EFI partition found in disk details")
vyos.system.disk.filesystem_create(con.disk_details.partition['efi'], 'efi')
vyos.system.disk.filesystem_create(con.disk_details.partition['root'], 'ext4')
vyos.system.disk.filesystem_create(efi, 'efi')
vyos.system.disk.filesystem_create(root, 'ext4')
cmd(f"mount -t ext4 {con.disk_details.partition['root']} {con.raw_dir}")
cmd(f"mount -t vfat {con.disk_details.partition['efi']} {con.efi_dir}")
print(f"I: Mounting root: {root} to {con.raw_dir}")
cmd(f"mount -t ext4 {root} {con.raw_dir}")
cmd(f"mount -t vfat {efi} {con.efi_dir}")
if not os.path.ismount(con.efi_dir):
cmd(f"mount -t vfat {con.disk_details.partition['efi']} {con.efi_dir}")
else:
print(f"I: {con.disk_details.partition['efi']} already mounted on {con.efi_dir}")
def install_image(con, version):
from glob import glob
@ -205,6 +230,22 @@ def create_raw_image(build_config, iso_file, work_dir):
create_disk(raw_file, build_config["disk_size"])
setup_loop_device(con, raw_file)
disk_details = parttable_create(con.loop_device, (int(build_config["disk_size"]) - 1) * 1024 * 1024)
# Map partitions using kpartx
cmd(f"kpartx -av {con.loop_device}")
cmd("udevadm settle")
# Resolve mapper names (example: /dev/mapper/loop0p2)
from glob import glob
mapper_base = os.path.basename(con.loop_device).replace("/dev/", "")
mapped_parts = sorted(glob(f"/dev/mapper/{mapper_base}p*"))
if len(mapped_parts) < 3:
raise RuntimeError("E: Expected at least 3 partitions created by kpartx")
disk_details.partition['efi'] = mapped_parts[1]
disk_details.partition['root'] = mapped_parts[2]
con.disk_details = disk_details
mount_image(con)
install_image(con, version)