Blame assemblers/org.osbuild.qemu

Packit Service 863627
#!/usr/bin/python3
Packit Service 863627
"""
Packit Service 863627
Assemble a bootable partitioned disk image with qemu-img
Packit Service 863627
Packit Service 863627
Assemble a bootable partitioned disk image using `qemu-img`.
Packit Service 863627
Packit Service 863627
Creates a sparse partitioned disk image of type `pttype` of a given `size`,
Packit Service 863627
with a partition table according to `partitions` or a MBR partitioned disk
Packit Service 863627
having a single bootable partition containing the root filesystem if the
Packit Service 863627
`pttype` property is absent.
Packit Service 863627
Packit Service 863627
If the partition type is MBR it installs GRUB2 (using the buildhost's
Packit Service 863627
`/usr/lib/grub/i386-pc/boot.img` etc.) as the bootloader.
Packit Service 863627
Packit Service 863627
Copies the tree contents into the root filesystem and then converts the raw
Packit Service 863627
sparse image into the format requested with the `fmt` option.
Packit Service 863627
Packit Service 863627
Buildhost commands used: `truncate`, `mount`, `umount`, `sfdisk`,
Packit Service 863627
`grub2-mkimage`, `mkfs.ext4` or `mkfs.xfs`, `qemu-img`.
Packit Service 863627
"""
Packit Service 863627
Packit Service 863627
Packit Service 863627
import contextlib
Packit Service 863627
import json
Packit Service 863627
import os
Packit Service 863627
import shutil
Packit Service 863627
import struct
Packit Service 863627
import subprocess
Packit Service 863627
import sys
Packit Service 863627
import tempfile
Packit Service 863627
from typing import List, BinaryIO
Packit Service 863627
import osbuild.remoteloop as remoteloop
Packit Service 863627
Packit Service 863627
SCHEMA = """
Packit Service 863627
"additionalProperties": false,
Packit Service 863627
"required": ["format", "filename", "ptuuid", "size"],
Packit Service 863627
"oneOf": [{
Packit Service 863627
  "required": ["root_fs_uuid"]
Packit Service 863627
},{
Packit Service 863627
  "required": ["pttype", "partitions"]
Packit Service 863627
}],
Packit Service 863627
"properties": {
Packit Service 863627
  "bootloader": {
Packit Service 863627
    "description": "Options specific to the bootloader",
Packit Service 863627
    "type": "object",
Packit Service 863627
    "properties": {
Packit Service 863627
      "type": {
Packit Service 863627
        "description": "What bootloader to install",
Packit Service 863627
        "type": "string",
Packit Service 863627
        "enum": ["grub2", "zipl"]
Packit Service 863627
      }
Packit Service 863627
    }
Packit Service 863627
  },
Packit Service 863627
  "format": {
Packit Service 863627
    "description": "Image file format to use",
Packit Service 863627
    "type": "string",
Packit Service 863627
    "enum": ["raw", "raw.xz", "qcow2", "vdi", "vmdk", "vpc", "vhdx"]
Packit Service 863627
  },
Packit Service 863627
  "filename": {
Packit Service 863627
    "description": "Image filename",
Packit Service 863627
    "type": "string"
Packit Service 863627
  },
Packit Service 863627
  "partitions": {
Packit Service 863627
    "description": "Partition layout ",
Packit Service 863627
    "type": "array",
Packit Service 863627
    "items": {
Packit Service 863627
      "description": "Description of one partition",
Packit Service 863627
      "type": "object",
Packit Service 863627
      "properties": {
Packit Service 863627
        "bootable": {
Packit Service 863627
          "description": "Mark the partition as bootable (dos)",
Packit Service 863627
          "type": "boolean"
Packit Service 863627
        },
Packit Service 863627
        "name": {
Packit Service 863627
          "description": "The partition name (GPT)",
Packit Service 863627
          "type": "string"
Packit Service 863627
        },
Packit Service 863627
        "size": {
Packit Service 863627
          "description": "The size of this partition",
Packit Service 863627
          "type": "integer"
Packit Service 863627
        },
Packit Service 863627
        "start": {
Packit Service 863627
          "description": "The start offset of this partition",
Packit Service 863627
          "type": "integer"
Packit Service 863627
        },
Packit Service 863627
        "type": {
Packit Service 863627
          "description": "The partition type (UUID or identifier)",
Packit Service 863627
          "type": "string"
Packit Service 863627
        },
Packit Service 863627
        "uuid": {
Packit Service 863627
           "description": "UUID of the partition (GPT)",
Packit Service 863627
           "type": "string"
Packit Service 863627
        },
Packit Service 863627
        "filesystem": {
Packit Service 863627
          "description": "Description of the filesystem",
Packit Service 863627
          "type": "object",
Packit Service 863627
          "required": ["mountpoint", "type", "uuid"],
Packit Service 863627
          "properties": {
Packit Service 863627
            "label": {
Packit Service 863627
              "description": "Label for the filesystem",
Packit Service 863627
              "type": "string"
Packit Service 863627
            },
Packit Service 863627
            "mountpoint": {
Packit Service 863627
              "description": "Where to mount the partition",
Packit Service 863627
              "type": "string"
Packit Service 863627
            },
Packit Service 863627
            "type": {
Packit Service 863627
              "description": "Type of the filesystem",
Packit Service 863627
              "type": "string",
Packit Service 863627
              "enum": ["ext4", "xfs", "vfat"]
Packit Service 863627
            },
Packit Service 863627
            "uuid": {
Packit Service 863627
              "description": "UUID for the filesystem",
Packit Service 863627
              "type": "string"
Packit Service 863627
            }
Packit Service 863627
          }
Packit Service 863627
        }
Packit Service 863627
      }
Packit Service 863627
    }
Packit Service 863627
  },
Packit Service 863627
  "ptuuid": {
Packit Service 863627
    "description": "UUID for the disk image's partition table",
Packit Service 863627
    "type": "string"
Packit Service 863627
  },
Packit Service 863627
  "pttype": {
Packit Service 863627
    "description": "The type of the partition table",
Packit Service 863627
    "type": "string",
Packit Service 863627
    "enum": ["mbr", "dos", "gpt"]
Packit Service 863627
  },
Packit Service 863627
  "root_fs_uuid": {
Packit Service 863627
    "description": "UUID for the root filesystem",
Packit Service 863627
    "type": "string"
Packit Service 863627
  },
Packit Service 863627
  "size": {
Packit Service 863627
    "description": "Virtual disk size",
Packit Service 863627
    "type": "integer"
Packit Service 863627
  },
Packit Service 863627
  "root_fs_type": {
Packit Service 863627
    "description": "Type of the root filesystem",
Packit Service 863627
    "type": "string",
Packit Service 863627
    "enum": ["ext4", "xfs"],
Packit Service 863627
    "default": "ext4"
Packit Service 863627
  }
Packit Service 863627
}
Packit Service 863627
"""
Packit Service 863627
Packit Service 863627
@contextlib.contextmanager
Packit Service 863627
def mount(source, dest):
Packit Service 863627
    subprocess.run(["mount", source, dest], check=True)
Packit Service 863627
    try:
Packit Service 863627
        yield dest
Packit Service 863627
    finally:
Packit Service 863627
        subprocess.run(["umount", "-R", dest], check=True)
Packit Service 863627
Packit Service 863627
Packit Service 863627
def mkfs_ext4(device, uuid, label):
Packit Service 863627
    opts = []
Packit Service 863627
    if label:
Packit Service 863627
        opts = ["-L", label]
Packit Service 863627
    subprocess.run(["mkfs.ext4", "-U", uuid] + opts + [device],
Packit Service 863627
                   input="y", encoding='utf-8', check=True)
Packit Service 863627
Packit Service 863627
Packit Service 863627
def mkfs_xfs(device, uuid, label):
Packit Service 863627
    opts = []
Packit Service 863627
    if label:
Packit Service 863627
        opts = ["-L", label]
Packit Service 863627
    subprocess.run(["mkfs.xfs", "-m", f"uuid={uuid}"] + opts + [device],
Packit Service 863627
                   encoding='utf-8', check=True)
Packit Service 863627
Packit Service 863627
Packit Service 863627
def mkfs_vfat(device, uuid, label):
Packit Service 863627
    volid = uuid.replace('-', '')
Packit Service 863627
    opts = []
Packit Service 863627
    if label:
Packit Service 863627
        opts = ["-n", label]
Packit Service 863627
    subprocess.run(["mkfs.vfat", "-i", volid] + opts + [device], encoding='utf-8', check=True)
Packit Service 863627
Packit Service 863627
Packit Service 863627
class Filesystem:
Packit Service 863627
    def __init__(self,
Packit Service 863627
                 fstype: str,
Packit Service 863627
                 uuid: str,
Packit Service 863627
                 mountpoint: str,
Packit Service 863627
                 label: str = None):
Packit Service 863627
        self.type = fstype
Packit Service 863627
        self.uuid = uuid
Packit Service 863627
        self.mountpoint = mountpoint
Packit Service 863627
        self.label = label
Packit Service 863627
Packit Service 863627
    def make_at(self, device: str):
Packit Service 863627
        fs_type = self.type
Packit Service 863627
        if fs_type == "ext4":
Packit Service 863627
            maker = mkfs_ext4
Packit Service 863627
        elif fs_type == "xfs":
Packit Service 863627
            maker = mkfs_xfs
Packit Service 863627
        elif fs_type == "vfat":
Packit Service 863627
            maker = mkfs_vfat
Packit Service 863627
        else:
Packit Service 863627
            raise ValueError(f"Unknown filesystem type '{fs_type}'")
Packit Service 863627
        maker(device, self.uuid, self.label)
Packit Service 863627
Packit Service 863627
Packit Service 863627
# pylint: disable=too-many-instance-attributes
Packit Service 863627
class Partition:
Packit Service 863627
    def __init__(self,
Packit Service 863627
                 pttype: str = None,
Packit Service 863627
                 start: int = None,
Packit Service 863627
                 size: int = None,
Packit Service 863627
                 bootable: bool = False,
Packit Service 863627
                 name: str = None,
Packit Service 863627
                 uuid: str = None,
Packit Service 863627
                 filesystem: Filesystem = None):
Packit Service 863627
        self.type = pttype
Packit Service 863627
        self.start = start
Packit Service 863627
        self.size = size
Packit Service 863627
        self.bootable = bootable
Packit Service 863627
        self.name = name
Packit Service 863627
        self.uuid = uuid
Packit Service 863627
        self.filesystem = filesystem
Packit Service 863627
        self.index = None
Packit Service 863627
Packit Service 863627
    @property
Packit Service 863627
    def start_in_bytes(self):
Packit Service 863627
        return (self.start or 0) * 512
Packit Service 863627
Packit Service 863627
    @property
Packit Service 863627
    def size_in_bytes(self):
Packit Service 863627
        return (self.size or 0) * 512
Packit Service 863627
Packit Service 863627
    @property
Packit Service 863627
    def mountpoint(self):
Packit Service 863627
        if self.filesystem is None:
Packit Service 863627
            return None
Packit Service 863627
        return self.filesystem.mountpoint
Packit Service 863627
Packit Service 863627
    @property
Packit Service 863627
    def fs_type(self):
Packit Service 863627
        if self.filesystem is None:
Packit Service 863627
            return None
Packit Service 863627
        return self.filesystem.type
Packit Service 863627
Packit Service 863627
    @property
Packit Service 863627
    def fs_uuid(self):
Packit Service 863627
        if self.filesystem is None:
Packit Service 863627
            return None
Packit Service 863627
        return self.filesystem.uuid
Packit Service 863627
Packit Service 863627
Packit Service 863627
class PartitionTable:
Packit Service 863627
    def __init__(self, label, uuid, partitions):
Packit Service 863627
        self.label = label
Packit Service 863627
        self.uuid = uuid
Packit Service 863627
        self.partitions = partitions or []
Packit Service 863627
Packit Service 863627
    def __getitem__(self, key) -> Partition:
Packit Service 863627
        return self.partitions[key]
Packit Service 863627
Packit Service 863627
    def partitions_with_filesystems(self) -> List[Partition]:
Packit Service 863627
        """Return partitions with filesystems sorted by hierarchy"""
Packit Service 863627
        def mountpoint_len(p):
Packit Service 863627
            return len(p.mountpoint)
Packit Service 863627
        parts_fs = filter(lambda p: p.filesystem is not None, self.partitions)
Packit Service 863627
        return sorted(parts_fs, key=mountpoint_len)
Packit Service 863627
Packit Service 863627
    def partition_containing_root(self) -> Partition:
Packit Service 863627
        """Return the partition containing the root filesystem"""
Packit Service 863627
        for p in self.partitions:
Packit Service 863627
            if p.mountpoint and p.mountpoint == "/":
Packit Service 863627
                return p
Packit Service 863627
        return None
Packit Service 863627
Packit Service 863627
    def partition_containing_boot(self) -> Partition:
Packit Service 863627
        """Return the partition containing /boot"""
Packit Service 863627
        for p in self.partitions_with_filesystems():
Packit Service 863627
            if p.mountpoint == "/boot":
Packit Service 863627
                return p
Packit Service 863627
        # fallback to the root partition
Packit Service 863627
        return self.partition_containing_root()
Packit Service 863627
Packit Service 863627
    def find_prep_partition(self) -> Partition:
Packit Service 863627
        """Find the PReP partition'"""
Packit Service 863627
        if self.label == "dos":
Packit Service 863627
            prep_type = "41"
Packit Service 863627
        elif self.label == "gpt":
Packit Service 863627
            prep_type = "9E1A2D38-C612-4316-AA26-8B49521E5A8B"
Packit Service 863627
Packit Service 863627
        for part in self.partitions:
Packit Service 863627
            if part.type.upper() == prep_type:
Packit Service 863627
                return part
Packit Service 863627
        return None
Packit Service 863627
Packit Service 863627
    def find_bios_boot_partition(self) -> Partition:
Packit Service 863627
        """Find the BIOS-boot Partition"""
Packit Service 863627
        bb_type = "21686148-6449-6E6F-744E-656564454649"
Packit Service 863627
        for part in self.partitions:
Packit Service 863627
            if part.type.upper() == bb_type:
Packit Service 863627
                return part
Packit Service 863627
        return None
Packit Service 863627
Packit Service 863627
    def write_to(self, target, sync=True):
Packit Service 863627
        """Write the partition table to disk"""
Packit Service 863627
        # generate the command for sfdisk to create the table
Packit Service 863627
        command = f"label: {self.label}\nlabel-id: {self.uuid}"
Packit Service 863627
        for partition in self.partitions:
Packit Service 863627
            fields = []
Packit Service 863627
            for field in ["start", "size", "type", "name", "uuid"]:
Packit Service 863627
                value = getattr(partition, field)
Packit Service 863627
                if value:
Packit Service 863627
                    fields += [f'{field}="{value}"']
Packit Service 863627
                if partition.bootable:
Packit Service 863627
                    fields += ["bootable"]
Packit Service 863627
            command += "\n" + ", ".join(fields)
Packit Service 863627
Packit Service 863627
        subprocess.run(["sfdisk", "-q", target],
Packit Service 863627
                       input=command,
Packit Service 863627
                       encoding='utf-8',
Packit Service 863627
                       check=True)
Packit Service 863627
Packit Service 863627
        if sync:
Packit Service 863627
            self.update_from(target)
Packit Service 863627
Packit Service 863627
    def update_from(self, target):
Packit Service 863627
        """Update and fill in missing information from disk"""
Packit Service 863627
        r = subprocess.run(["sfdisk", "--json", target],
Packit Service 863627
                           stdout=subprocess.PIPE,
Packit Service 863627
                           encoding='utf-8',
Packit Service 863627
                           check=True)
Packit Service 863627
        disk_table = json.loads(r.stdout)["partitiontable"]
Packit Service 863627
        disk_parts = disk_table["partitions"]
Packit Service 863627
Packit Service 863627
        assert len(disk_parts) == len(self.partitions)
Packit Service 863627
        for i, part in enumerate(self.partitions):
Packit Service 863627
            part.index = i
Packit Service 863627
            part.start = disk_parts[i]["start"]
Packit Service 863627
            part.size = disk_parts[i]["size"]
Packit Service 863627
            part.type = disk_parts[i].get("type")
Packit Service 863627
            part.name = disk_parts[i].get("name")
Packit Service 863627
Packit Service 863627
Packit Service 863627
def filesystem_from_json(js) -> Filesystem:
Packit Service 863627
    return Filesystem(js["type"], js["uuid"], js["mountpoint"], js.get("label"))
Packit Service 863627
Packit Service 863627
Packit Service 863627
def partition_from_json(js) -> Partition:
Packit Service 863627
    p = Partition(pttype=js.get("type"),
Packit Service 863627
                  start=js.get("start"),
Packit Service 863627
                  size=js.get("size"),
Packit Service 863627
                  bootable=js.get("bootable"),
Packit Service 863627
                  name=js.get("name"),
Packit Service 863627
                  uuid=js.get("uuid"))
Packit Service 863627
    fs = js.get("filesystem")
Packit Service 863627
    if fs:
Packit Service 863627
        p.filesystem = filesystem_from_json(fs)
Packit Service 863627
    return p
Packit Service 863627
Packit Service 863627
Packit Service 863627
def partition_table_from_options(options) -> PartitionTable:
Packit Service 863627
    ptuuid = options["ptuuid"]
Packit Service 863627
    pttype = options.get("pttype", "dos")
Packit Service 863627
    partitions = options.get("partitions")
Packit Service 863627
Packit Service 863627
    if pttype == "mbr":
Packit Service 863627
        pttype = "dos"
Packit Service 863627
Packit Service 863627
    if partitions is None:
Packit Service 863627
        # legacy mode, create a correct
Packit Service 863627
        root_fs_uuid = options["root_fs_uuid"]
Packit Service 863627
        root_fs_type = options.get("root_fs_type", "ext4")
Packit Service 863627
        partitions = [{
Packit Service 863627
            "bootable": True,
Packit Service 863627
            "type": "83",
Packit Service 863627
            "filesystem": {
Packit Service 863627
                "type": root_fs_type,
Packit Service 863627
                "uuid": root_fs_uuid,
Packit Service 863627
                "mountpoint": "/"
Packit Service 863627
            }
Packit Service 863627
        }]
Packit Service 863627
    parts = [partition_from_json(p) for p in partitions]
Packit Service 863627
    return PartitionTable(pttype, ptuuid, parts)
Packit Service 863627
Packit Service 863627
Packit Service 863627
def grub2_write_boot_image(boot_f: BinaryIO,
Packit Service 863627
                           image_f: BinaryIO,
Packit Service 863627
                           core_location: int):
Packit Service 863627
    """Write the boot image (grub2 stage 1) to the MBR"""
Packit Service 863627
Packit Service 863627
    # The boot.img file is 512 bytes, but we must only copy the first 440
Packit Service 863627
    # bytes, as these contain the bootstrapping code. The rest of the
Packit Service 863627
    # first sector contains the partition table, and must not be
Packit Service 863627
    # overwritten.
Packit Service 863627
    image_f.seek(0)
Packit Service 863627
    image_f.write(boot_f.read(440))
Packit Service 863627
Packit Service 863627
    # Additionally, write the location (in sectors) of
Packit Service 863627
    # the grub core image, into the boot image, so the
Packit Service 863627
    # latter can find the former. To exact location is
Packit Service 863627
    # taken from grub2's "boot.S":
Packit Service 863627
    #  GRUB_BOOT_MACHINE_KERNEL_SECTOR 0x5c (= 92)
Packit Service 863627
    image_f.seek(0x5c)
Packit Service 863627
    image_f.write(struct.pack("
Packit Service 863627
Packit Service 863627
Packit Service 863627
def grub2_write_core_mbrgap(core_f: BinaryIO,
Packit Service 863627
                            image_f: BinaryIO,
Packit Service 863627
                            pt: PartitionTable):
Packit Service 863627
    """Write the core into the MBR gap"""
Packit Service 863627
    # For historic and performance reasons the first partition
Packit Service 863627
    # is aligned to a specific sector number (used to be 64,
Packit Service 863627
    # now it is 2048), which leaves a gap between it and the MBR,
Packit Service 863627
    # where the core image can be embedded in; also check it fits
Packit Service 863627
    core_size = os.fstat(core_f.fileno()).st_size
Packit Service 863627
    partition_offset = pt[0].start_in_bytes
Packit Service 863627
    assert core_size < partition_offset - 512
Packit Service 863627
    image_f.seek(512)
Packit Service 863627
    shutil.copyfileobj(core_f, image_f)
Packit Service 863627
Packit Service 863627
    return 1  # the location of the core image in sectors
Packit Service 863627
Packit Service 863627
Packit Service 863627
def grub2_write_core_prep_part(core_f: BinaryIO,
Packit Service 863627
                               image_f: BinaryIO,
Packit Service 863627
                               pt: PartitionTable):
Packit Service 863627
    """Write the core to the prep partition"""
Packit Service 863627
    # On ppc64le with Open Firmware a special partition called
Packit Service 863627
    # 'PrEP partition' is used the store the grub2 core; the
Packit Service 863627
    # firmware looks for this partition and directly loads and
Packit Service 863627
    # executes the core form it.
Packit Service 863627
    prep_part = pt.find_prep_partition()
Packit Service 863627
    if prep_part is None:
Packit Service 863627
        raise ValueError("PrEP partition missing")
Packit Service 863627
Packit Service 863627
    core_size = os.fstat(core_f.fileno()).st_size
Packit Service 863627
    assert core_size < prep_part.size_in_bytes - 512
Packit Service 863627
    image_f.seek(prep_part.start_in_bytes)
Packit Service 863627
    shutil.copyfileobj(core_f, image_f)
Packit Service 863627
Packit Service 863627
    return prep_part.start
Packit Service 863627
Packit Service 863627
Packit Service 863627
def grub2_write_core_bios_boot(core_f: BinaryIO,
Packit Service 863627
                               image_f: BinaryIO,
Packit Service 863627
                               pt: PartitionTable):
Packit Service 863627
    """Write the core to the bios boot partition"""
Packit Service 863627
    bb = pt.find_bios_boot_partition()
Packit Service 863627
    if bb is None:
Packit Service 863627
        raise ValueError("BIOS-boot partition missing")
Packit Service 863627
    core_size = os.fstat(core_f.fileno()).st_size
Packit Service 863627
    if bb.size_in_bytes < core_size:
Packit Service 863627
        raise ValueError("BIOS-boot partition too small")
Packit Service 863627
Packit Service 863627
    image_f.seek(bb.start_in_bytes)
Packit Service 863627
    shutil.copyfileobj(core_f, image_f)
Packit Service 863627
Packit Service 863627
    # The core image needs to know from where to load its
Packit Service 863627
    # second sector so that information needs to be embedded
Packit Service 863627
    # into the image itself at the right location, i.e.
Packit Service 863627
    # the "sector start parameter" ("size .long 2, 0"):
Packit Service 863627
    # 0x200 - GRUB_BOOT_MACHINE_LIST_SIZE (12) = 0x1F4 = 500
Packit Service 863627
    image_f.seek(bb.start_in_bytes + 500)
Packit Service 863627
    image_f.write(struct.pack("
Packit Service 863627
Packit Service 863627
    return bb.start
Packit Service 863627
Packit Service 863627
Packit Service 863627
def grub2_partition_id(pt: PartitionTable):
Packit Service 863627
    """grub2 partition identifier for the partition table"""
Packit Service 863627
Packit Service 863627
    label2grub = {
Packit Service 863627
        "dos": "msdos",
Packit Service 863627
        "gpt": "gpt"
Packit Service 863627
    }
Packit Service 863627
Packit Service 863627
    if pt.label not in label2grub:
Packit Service 863627
        raise ValueError(f"Unknown partition type: {pt.label}")
Packit Service 863627
Packit Service 863627
    return label2grub[pt.label]
Packit Service 863627
Packit Service 863627
Packit Service 863627
def install_grub2(image: str, pt: PartitionTable, options):
Packit Service 863627
    """Install grub2 to image"""
Packit Service 863627
    platform = options.get("platform", "i386-pc")
Packit Service 863627
Packit Service 863627
    boot_path = f"/usr/lib/grub/{platform}/boot.img"
Packit Service 863627
    core_path = "/var/tmp/grub2-core.img"
Packit Service 863627
Packit Service 863627
    # Create the level-2 & 3 stages of the bootloader, aka the core
Packit Service 863627
    # it consists of the kernel plus the core modules required to
Packit Service 863627
    # to locate and load the rest of the grub modules, specifically
Packit Service 863627
    # the "normal.mod" (Stage 4) module.
Packit Service 863627
    # The exact list of modules required to be built into the core
Packit Service 863627
    # depends on the system: it is the minimal set needed to find
Packit Service 863627
    # read the partition and its filesystem containing said modules
Packit Service 863627
    # and the grub configuration [NB: efi systems work differently]
Packit Service 863627
Packit Service 863627
    # find the partition containing /boot/grub2
Packit Service 863627
    boot_part = pt.partition_containing_boot()
Packit Service 863627
Packit Service 863627
    # modules: access the disk and read the partition table:
Packit Service 863627
    # on x86 'biosdisk' is used to access the disk, on ppc64le
Packit Service 863627
    # with "Open Firmware" the latter is directly loading core
Packit Service 863627
    if platform == "i386-pc":
Packit Service 863627
        modules = ["biosdisk"]
Packit Service 863627
    else:
Packit Service 863627
        modules = []
Packit Service 863627
Packit Service 863627
    if pt.label == "dos":
Packit Service 863627
        modules += ["part_msdos"]
Packit Service 863627
    elif pt.label == "gpt":
Packit Service 863627
        modules += ["part_gpt"]
Packit Service 863627
Packit Service 863627
    # modules: grubs needs to access the filesystems of /boot/grub2
Packit Service 863627
    fs_type = boot_part.fs_type or "unknown"
Packit Service 863627
Packit Service 863627
    if fs_type == "ext4":
Packit Service 863627
        modules += ["ext2"]
Packit Service 863627
    elif fs_type == "xfs":
Packit Service 863627
        modules += ["xfs"]
Packit Service 863627
    else:
Packit Service 863627
        raise ValueError(f"unknown boot filesystem type: '{fs_type}'")
Packit Service 863627
Packit Service 863627
    # identify the partition containing boot for grub2
Packit Service 863627
    partid = grub2_partition_id(pt) + str(boot_part.index + 1)
Packit Service 863627
    print(f"grub2 prefix {partid}")
Packit Service 863627
Packit Service 863627
    # the path containing the grub files relative partition
Packit Service 863627
    grub_path = os.path.relpath("/boot/grub2", boot_part.mountpoint)
Packit Service 863627
Packit Service 863627
    # now created the core image
Packit Service 863627
    subprocess.run(["grub2-mkimage",
Packit Service 863627
                    "--verbose",
Packit Service 863627
                    "--directory", f"/usr/lib/grub/{platform}",
Packit Service 863627
                    "--prefix", f"(,{partid})/{grub_path}",
Packit Service 863627
                    "--format", platform,
Packit Service 863627
                    "--compression", "auto",
Packit Service 863627
                    "--output", core_path] +
Packit Service 863627
                   modules,
Packit Service 863627
                   check=True)
Packit Service 863627
Packit Service 863627
    with open(image, "rb+") as image_f:
Packit Service 863627
        # Write the newly created grub2 core to the image
Packit Service 863627
        with open(core_path, "rb") as core_f:
Packit Service 863627
            if platform == "powerpc-ieee1275":
Packit Service 863627
                # write the core to the PrEP partition
Packit Service 863627
                core_loc = grub2_write_core_prep_part(core_f, image_f, pt)
Packit Service 863627
            elif pt.label == "gpt":
Packit Service 863627
                # gpt requires a bios-boot partition
Packit Service 863627
                core_loc = grub2_write_core_bios_boot(core_f, image_f, pt)
Packit Service 863627
            else:
Packit Service 863627
                # embed the core in the MBR gap
Packit Service 863627
                core_loc = grub2_write_core_mbrgap(core_f, image_f, pt)
Packit Service 863627
Packit Service 863627
        # On certain platforms (x86) a level 1 boot loader is required
Packit Service 863627
        # to load to the core image (on ppc64le & Open Firmware this is
Packit Service 863627
        # done by the firmware itself)
Packit Service 863627
        if platform == "i386-pc":
Packit Service 863627
            # On x86, the boot image just jumps to core image
Packit Service 863627
            with open(boot_path, "rb") as boot_f:
Packit Service 863627
                grub2_write_boot_image(boot_f, image_f, core_loc)
Packit Service 863627
Packit Service 863627
Packit Service 863627
def parse_blsfile(blsfile):
Packit Service 863627
    params = {}
Packit Service 863627
    with open(blsfile, "r") as bls:
Packit Service 863627
        for line in bls:
Packit Service 863627
            key, value = line.split(' ', 1)
Packit Service 863627
            params[key] = value.strip()
Packit Service 863627
    return params
Packit Service 863627
Packit Service 863627
Packit Service 863627
def find_kernel(root):
Packit Service 863627
    base = f"{root}/boot/loader/entries"
Packit Service 863627
    for dirent in os.scandir(base):
Packit Service 863627
        fn, ext = os.path.splitext(dirent.name)
Packit Service 863627
        if ext != ".conf" or fn.endswith("rescue"):
Packit Service 863627
            continue
Packit Service 863627
        blsfile = f"{base}/{dirent.name}"
Packit Service 863627
        params = parse_blsfile(blsfile)
Packit Service 863627
        linux = root + params["linux"]
Packit Service 863627
        initrd = root + params["initrd"]
Packit Service 863627
        options = params.get("options", "")
Packit Service 863627
        return linux, initrd, options
Packit Service 863627
Packit Service 863627
Packit Service 863627
def install_zipl(root: str, device: str, pt: PartitionTable):
Packit Service 863627
    """Install the bootloader on s390x via zipl"""
Packit Service 863627
    kernel, initrd, kopts = find_kernel(root)
Packit Service 863627
    part_with_boot = pt.partition_containing_boot()
Packit Service 863627
    subprocess.run(["/usr/sbin/zipl",
Packit Service 863627
                    "--verbose",
Packit Service 863627
                    "--target", f"{root}/boot",
Packit Service 863627
                    "--image", kernel,
Packit Service 863627
                    "--ramdisk", initrd,
Packit Service 863627
                    "--parameters", kopts,
Packit Service 863627
                    "--targetbase", device,
Packit Service 863627
                    "--targettype", "SCSI",
Packit Service 863627
                    "--targetblocksize", "512",
Packit Service 863627
                    "--targetoffset", str(part_with_boot.start)],
Packit Service 863627
                   check=True)
Packit Service 863627
Packit Service 863627
Packit Service 863627
def main(tree, output_dir, options, loop_client):
Packit Service 863627
    fmt = options["format"]
Packit Service 863627
    filename = options["filename"]
Packit Service 863627
    size = options["size"]
Packit Service 863627
    bootloader = options.get("bootloader", {"type": "none"})
Packit Service 863627
Packit Service 863627
    # sfdisk works on sectors of 512 bytes and ignores excess space - be explicit about this
Packit Service 863627
    if size % 512 != 0:
Packit Service 863627
        raise ValueError("`size` must be a multiple of sector size (512)")
Packit Service 863627
Packit Service 863627
    if fmt not in ["raw", "raw.xz", "qcow2", "vdi", "vmdk", "vpc", "vhdx"]:
Packit Service 863627
        raise ValueError("`format` must be one of raw, qcow, vdi, vmdk, vpc, vhdx")
Packit Service 863627
Packit Service 863627
    image = "/var/tmp/osbuild-image.raw"
Packit Service 863627
Packit Service 863627
    # Create an empty image file
Packit Service 863627
    subprocess.run(["truncate", "--size", str(size), image], check=True)
Packit Service 863627
Packit Service 863627
    # The partition table
Packit Service 863627
    pt = partition_table_from_options(options)
Packit Service 863627
    pt.write_to(image)
Packit Service 863627
Packit Service 863627
    # For backwards comparability assume that if bootloader is not
Packit Service 863627
    # set and partition scheme is dos (MBR) grub2 is being used
Packit Service 863627
    if bootloader["type"] == "none" and pt.label == "dos":
Packit Service 863627
        bootloader["type"] = "grub2"
Packit Service 863627
Packit Service 863627
    # Install the bootloader
Packit Service 863627
    if bootloader["type"] == "grub2":
Packit Service 863627
        install_grub2(image, pt, bootloader)
Packit Service 863627
Packit Service 863627
    # Now assemble the filesystem hierarchy and copy the tree into the image
Packit Service 863627
    with contextlib.ExitStack() as cm:
Packit Service 863627
        root = cm.enter_context(tempfile.TemporaryDirectory(dir=output_dir, prefix="osbuild-mnt-"))
Packit Service 863627
        disk = cm.enter_context(loop_client.device(image, 0, size))
Packit Service 863627
        # iterate the partition according to their position in the filesystem tree
Packit Service 863627
        for partition in pt.partitions_with_filesystems():
Packit Service 863627
            offset, size = partition.start_in_bytes, partition.size_in_bytes
Packit Service 863627
            loop = cm.enter_context(loop_client.device(image, offset, size))
Packit Service 863627
            # make the specified filesystem, if any
Packit Service 863627
            if partition.filesystem is None:
Packit Service 863627
                continue
Packit Service 863627
            partition.filesystem.make_at(loop)
Packit Service 863627
            # now mount it
Packit Service 863627
            mountpoint = os.path.normpath(f"{root}/{partition.mountpoint}")
Packit Service 863627
            os.makedirs(mountpoint, exist_ok=True)
Packit Service 863627
            cm.enter_context(mount(loop, mountpoint))
Packit Service 863627
        # the filesystem tree should now be properly setup,
Packit Service 863627
        # copy the tree into the target image
Packit Service 863627
        subprocess.run(["cp", "-a", f"{tree}/.", root], check=True)
Packit Service 863627
Packit Service 863627
        # zipl needs access to the /boot directory and the whole image
Packit Service 863627
        # via the loopback device node
Packit Service 863627
        if bootloader["type"] == "zipl":
Packit Service 863627
            install_zipl(root, disk, pt)
Packit Service 863627
Packit Service 863627
    if fmt == "raw":
Packit Service 863627
        subprocess.run(["cp", image, f"{output_dir}/{filename}"], check=True)
Packit Service 863627
    elif fmt == "raw.xz":
Packit Service 863627
        with open(f"{output_dir}/{filename}", "w") as f:
Packit Service 863627
            subprocess.run(
Packit Service 863627
                ["xz", "--keep", "--stdout", "-0", image],
Packit Service 863627
                stdout=f,
Packit Service 863627
                check=True,
Packit Service 863627
                env={
Packit Service 863627
                    "XZ_OPT": "--threads 0"
Packit Service 863627
                }
Packit Service 863627
            )
Packit Service 863627
    else:
Packit Service 863627
        extra_args = {
Packit Service 863627
            "qcow2": ["-c"],
Packit Service 863627
            "vdi": [],
Packit Service 863627
            "vmdk": ["-c"],
Packit Service 863627
            "vpc": ["-o", "subformat=fixed,force_size"],
Packit Service 863627
            "vhdx": []
Packit Service 863627
        }
Packit Service 863627
        subprocess.run([
Packit Service 863627
            "qemu-img",
Packit Service 863627
            "convert",
Packit Service 863627
            "-O", fmt,
Packit Service 863627
            *extra_args[fmt],
Packit Service 863627
            image,
Packit Service 863627
            f"{output_dir}/{filename}"
Packit Service 863627
        ], check=True)
Packit Service 863627
Packit Service 863627
Packit Service 863627
if __name__ == '__main__':
Packit Service 863627
    args = json.load(sys.stdin)
Packit Service 863627
    ret = main(args["tree"], args["output_dir"], args["options"], remoteloop.LoopClient("/run/osbuild/api/remoteloop"))
Packit Service 863627
    sys.exit(ret)