diff --git a/assemblers/io.weldr.qcow2 b/assemblers/io.weldr.qcow2 new file mode 100755 index 00000000..2938a950 --- /dev/null +++ b/assemblers/io.weldr.qcow2 @@ -0,0 +1,82 @@ +#!/usr/bin/python3 + +import contextlib +import json +import os +import shutil +import subprocess +import sys +import tempfile + +def tree_size(tree): + size = 0 + for root, dirs, files in os.walk(tree): + for entry in files + dirs: + path = os.path.join(root, entry) + size += os.stat(path, follow_symlinks=False).st_size + return size + +@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) + +@contextlib.contextmanager +def mount_api(dest): + with mount("/dev", f"{dest}/dev", "-o", "rbind"), \ + mount("/proc", f"{dest}/proc", "-o", "rbind"), \ + mount("/sys", f"{dest}/sys", "-o", "rbind"), \ + mount("none", f"{dest}/run", "-t", "tmpfs"): + yield + +@contextlib.contextmanager +def loop_device(image): + r = subprocess.run(["losetup", "-f"], stdout=subprocess.PIPE, encoding="utf-8", check=True) + loop = r.stdout.strip() + r = subprocess.run(["losetup", loop, "-P", image], check=True) + try: + yield loop + finally: + subprocess.run(["losetup", "-d", loop], check=True) + +def main(tree, input_dir, options): + filename = options["filename"] + partition_table_id = options["partition_table_id"] + root_fs_uuid = options["root_fs_uuid"] + + # Create a working directory on a tmpfs, maybe we should implicitly + # always do this. + with tempfile.TemporaryDirectory() as workdir: + image = f"{workdir}/image.raw" + mountpoint = f"{workdir}/mnt" + + # Create an empty image file of the right size + size = int(tree_size(tree) * 1.2) + subprocess.run(["truncate", "--size", str(size), image], check=True) + + # Set up the partition table of the image + partition_table = "label: mbr\nlabel-id: {partition_table_id}\nbootable, type=83" + subprocess.run(["sfdisk", "-q", image], input=partition_table, encoding='utf-8', check=True) + + # Mount the created image as a loopback device + with loop_device(image) as loop: + # Populate the first partition of the image with an ext4 fs and fill it with the contents of the + # tree we are operating on. + subprocess.run(["mkfs.ext4", "-d", tree, "-U", root_fs_uuid, f"{loop}p1"], check=True) + + # Install grub2 into the boot sector of the image + # It is unclear why chrooting into the image is required. + with mount(f"{loop}p1", mountpoint): + with mount_api(mountpoint): + subprocess.run(["chroot", mountpoint, "grub2-install", "--no-floppy", "--target", "i386-pc", loop], check=True) + + subprocess.run(["qemu-img", "convert", "-O" "qcow2", "-c", image, filename], check=True) + +if __name__ == '__main__': + args = json.load(sys.stdin) + r = main(args["tree"], args["input_dir"], args["options"]) + sys.exit(r) diff --git a/stages/io.weldr.qcow2 b/stages/io.weldr.qcow2 deleted file mode 100755 index cde80f77..00000000 --- a/stages/io.weldr.qcow2 +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/python3 - -import contextlib -import json -import os -import subprocess -import sys -import tempfile - -def tree_size(tree): - size = 0 - for root, dirs, files in os.walk(tree): - for entry in files + dirs: - path = os.path.join(root, entry) - size += os.stat(path, follow_symlinks=False).st_size - return size - -@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", dest], check=True) - -@contextlib.contextmanager -def mount_image(device, dest): - with mount(device, dest), \ - mount("/dev", f"{dest}/dev", "--bind"), \ - mount("/proc", f"{dest}/proc", "--bind"), \ - mount("/sys", f"{dest}/sys", "--bind"): - yield - -@contextlib.contextmanager -def loop_device(image): - r = subprocess.run(["losetup", "--show", "-f", "-P", image], stdout=subprocess.PIPE, encoding="utf-8", check=True) - loop = r.stdout.strip() - try: - yield loop - finally: - subprocess.run(["losetup", "-d", loop], check=True) - -def main(tree, options): - target = options["target"]) - - size = tree_size(tree) - - with tempfile.TemporaryDirectory(dir=os.getcwd()) as workdir: - image = f"{workdir}/image.raw" - mountpoint = f"{workdir}/mnt" - - subprocess.run(["truncate", "--size", str(int(size * 1.1)), image], check=True) - - partition_table = "label: mbr\nbootable, type=83" - subprocess.run(["sfdisk", "-q", image], input=partition_table, encoding='utf-8', check=True) - - with loop_device(image) as loop: - subprocess.run(["mkfs.ext4", "-d", tree, f"{loop}p1"], check=True) - - # grub2-mkconfig and grub2-install only seem to work reliably when - # called from a chroot - with mount_image(f"{loop}p1", mountpoint): - subprocess.run(["chroot", mountpoint, "grub2-mkconfig", "-o", "/boot/grub2/grub.cfg"], check=True) - subprocess.run(["chroot", mountpoint, "grub2-install", "--no-floppy", "--target", "i386-pc", loop], check=True) - - subprocess.run(["qemu-img", "convert", "-O" "qcow2", "-c", image, target], check=True) - -if __name__ == '__main__': - args = json.load(sys.stdin) - - try: - r = main(args["tree"], args["options"]) - except CalledProcessError as e: - print(e, file=sys.stderr) - r = 1 - - sys.exit(r)