Similar to the existing test, but uses qemu-nbd to mount the generated image. Using unittest.TestCase.subTest() for now, which means that the tests aren't very independent. I think this is fine in this case, because we're testing images independently from each other, reusing the base tree in the store.
117 lines
4.4 KiB
Python
Executable file
117 lines
4.4 KiB
Python
Executable file
#!/usr/bin/python3
|
|
|
|
import contextlib
|
|
import json
|
|
import os
|
|
import socket
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import osbuild.remoteloop as remoteloop
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def mount(source, dest, *options):
|
|
os.makedirs(dest, 0o755, True)
|
|
subprocess.run(["mount", *options, source, dest], check=True)
|
|
try:
|
|
yield
|
|
finally:
|
|
subprocess.run(["umount", "-R", dest], check=True)
|
|
|
|
|
|
def main(tree, output_dir, options, loop_client):
|
|
fmt = options["format"]
|
|
filename = options["filename"]
|
|
ptuuid = options["ptuuid"]
|
|
root_fs_uuid = options["root_fs_uuid"]
|
|
size = options["size"]
|
|
|
|
# sfdisk works on sectors of 512 bytes and ignores excess space - be explicit about this
|
|
if size % 512 != 0:
|
|
raise ValueError("`size` must be a multiple of sector size (512)")
|
|
|
|
if fmt not in ["raw", "qcow2", "vdi", "vmdk"]:
|
|
raise ValueError("`format` must be one of raw, qcow, vdi, vmdk")
|
|
|
|
image = f"/var/tmp/osbuild-image.raw"
|
|
grub2_core = "/var/tmp/grub2-core.img"
|
|
grub2_load = "/var/tmp/grub2-load.cfg"
|
|
mountpoint = f"/tmp/osbuild-mnt"
|
|
|
|
# Create an empty image file
|
|
subprocess.run(["truncate", "--size", str(size), image], check=True)
|
|
|
|
# Set up the partition table of the image
|
|
partition_table = f"label: mbr\nlabel-id: {ptuuid}\nbootable, type=83"
|
|
subprocess.run(["sfdisk", "-q", image], input=partition_table, encoding='utf-8', check=True)
|
|
|
|
r = subprocess.run(["sfdisk", "--json", image], stdout=subprocess.PIPE, encoding='utf-8', check=True)
|
|
partition_table = json.loads(r.stdout)
|
|
partition = partition_table["partitiontable"]["partitions"][0]
|
|
partition_offset = partition["start"] * 512
|
|
partition_size = partition["size"] * 512
|
|
|
|
with open(grub2_load, "w") as load:
|
|
load.write(f"search.fs_uuid {root_fs_uuid} root \n"
|
|
"set prefix=($root)'/boot/grub2'\n")
|
|
|
|
# Create the level-2 bootloader
|
|
# The purpose of this is to find the grub modules and configuration
|
|
# to be able to start the level-3 bootloader. It contains the modules
|
|
# necessary to do this, but nothing else.
|
|
subprocess.run(["grub2-mkimage",
|
|
"--verbose",
|
|
"--directory", "/usr/lib/grub/i386-pc",
|
|
"--prefix", "/boot/grub2",
|
|
"--format", "i386-pc",
|
|
"--compression", "auto",
|
|
"--config", grub2_load,
|
|
"--output", grub2_core,
|
|
"part_msdos", "ext2", "biosdisk", "search_fs_uuid"],
|
|
check=True)
|
|
|
|
assert os.path.getsize(grub2_core) < partition_offset - 512
|
|
|
|
with open(image, "rb+") as image_f:
|
|
# Install the level-1 bootloader into the start of the MBR
|
|
# The purpose of this is simply to jump into the level-2 bootloader.
|
|
with open("/usr/lib/grub/i386-pc/boot.img", "rb") as boot_f:
|
|
# The boot.img file is 512 bytes, but we must only copy the first 440
|
|
# bytes, as these contain the bootstrapping code. The rest of the
|
|
# first sector contains the partition table, and must not be
|
|
# overwritten.
|
|
image_f.write(boot_f.read(440))
|
|
|
|
# Install the level-2 bootloader into the space after the MBR, before
|
|
# the first partition.
|
|
with open(grub2_core, "rb") as core_f:
|
|
image_f.seek(512)
|
|
shutil.copyfileobj(core_f, image_f)
|
|
|
|
# Populate the first partition of the image with an ext4 fs
|
|
subprocess.run(["mkfs.ext4", "-U", root_fs_uuid, "-E", f"offset={partition_offset}", image,
|
|
f"{int(partition_size / 1024)}k"], input="y", encoding='utf-8', check=True)
|
|
|
|
# Copy the tree into the target image
|
|
with loop_client.device(image, partition_offset, partition_size) as loop, \
|
|
mount(loop, mountpoint):
|
|
subprocess.run(["cp", "-a", f"{tree}/.", mountpoint], check=True)
|
|
|
|
extra_args = []
|
|
|
|
# raw and vdi don't suppport compression
|
|
if fmt not in ("raw", "vdi"):
|
|
extra_args.append("-c")
|
|
|
|
subprocess.run(["qemu-img", "convert", "-O", fmt, *extra_args, image, f"{output_dir}/{filename}"], check=True)
|
|
|
|
if __name__ == '__main__':
|
|
args = json.load(sys.stdin)
|
|
|
|
with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as sock:
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
|
|
sock.connect("/run/osbuild/api/remoteloop")
|
|
ret = main(args["tree"], args["output_dir"], args["options"], remoteloop.LoopClient(sock))
|
|
|
|
sys.exit(ret)
|