|
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)
|