#!/usr/bin/python3

import argparse
import os
import shutil
import subprocess
import sys
import tempfile
import yaml


KERNELS = {
    "amd64": "linux-image-amd64",
    "arm64": "linux-image-arm64",
    "armhf": "linux-image-armmp-lpae",
    "i386": "linux-image-686",
    "ppc64el": "linux-image-powerpc64le",
}

QEMUS = {
    "amd64": "qemu-system-x86_64",
}


class Config:
    def __init__(self):
        p = argparse.ArgumentParser()
        p.add_argument("--dump", action="store_true")
        p.add_argument("--vmdb", action="store")
        p.add_argument("--tarball-directory", action="store", default=".")
        p.add_argument("--debian-release", action="store")
        p.add_argument("--grub", action="store", choices=["bios", "uefi", "ieee1275"])
        p.add_argument("--mklabel", action="store", choices=["msdos", "gpt"])
        p.add_argument("--arch", action="store")
        p.add_argument("--boot", action="store_true")
        p.add_argument("--maybe-boot", action="store_true")
        p.add_argument("--verbose", action="store_true")
        args = p.parse_args()

        self.dump = args.dump
        self.tarball_directory = args.tarball_directory
        self.vmdb_filename = args.vmdb
        self.vmdb = yaml.safe_load(open(self.vmdb_filename))
        self.boot = args.boot
        self.maybe_boot = args.maybe_boot
        self.verbose = args.verbose

        if args.arch:
            arch = args.arch
        else:
            arch = self.default_arch()
        self.qemu = QEMUS.get(arch)
        if self.boot and not self.qemu:
            sys.exit(
                f"Can't boot architecture {arch}: don't know of an emulator for it"
            )

        mklabel = self._step("mklabel")
        debootstrap = self._step("debootstrap")
        grub = self._step("grub")

        if args.debian_release is not None:
            debootstrap["debootstrap"] = args.debian_release
        if args.arch is not None:
            debootstrap["arch"] = args.arch
            debootstrap["include"] = debootstrap.get("include", []) + [
                KERNELS[args.arch]
            ]
        if args.mklabel is not None:
            mklabel["mklabel"] = args.mklabel
        if args.grub is not None:
            grub["grub"] = args.grub

    def _step(self, wanted):
        for item in self.vmdb["steps"]:
            if wanted in item:
                return item
        assert 0

    def debootstrap_release(self):
        for item in self.vmdb["steps"]:
            if "debootstrap" in item:
                return item["debootstrap"]
        assert 0

    def arch(self):
        for item in self.vmdb["steps"]:
            if "debootstrap" in item:
                return item.get("arch", self.default_arch())
        assert 0

    def default_arch(self):
        p = subprocess.run(
            ["dpkg", "--print-architecture"], check=True, capture_output=True
        )
        return p.stdout.decode().strip()

    def log(self):
        log = f"{self.debootstrap_release()}_{self.arch()}.log"
        return os.path.join(self.tarball_directory, log)

    def image(self):
        image = f"{self.debootstrap_release()}_{self.arch()}.img"
        return os.path.join(self.tarball_directory, image)

    def tarball(self):
        tarball = f"{self.debootstrap_release()}_{self.arch()}.tar.gz"
        return os.path.join(self.tarball_directory, tarball)

    def write_vmdb(self, filename):
        with open(filename, "w") as f:
            yaml.safe_dump(self.vmdb, stream=f, indent=4)


def run_vmdb2(config):
    print(f"building image {config.image()}")
    fd, vmdb = tempfile.mkstemp(dir=config.tarball_directory, suffix=".vmdb")
    config.write_vmdb(vmdb)
    os.close(fd)

    argv = [
        "./vmdb2",
        "--rootfs-tarball",
        config.tarball(),
        "--verbose",
        "--log",
        config.log(),
        "--output",
        config.image(),
        vmdb,
    ]
    subprocess.run(argv, check=True, capture_output=not config.verbose)

    os.remove(vmdb)
    print(f"built image {config.image()} OK")


def smoke_test(config):
    print(f"booting image {config.image()}")

    assert config.qemu
    tmp = tempfile.mkdtemp()
    qemu_sh = os.path.join(tmp, "run.sh")
    expect_txt = os.path.join(tmp, "expect.txt")

    qemu_script = f"""\
#!/bin/bash

set -euo pipefail
cd {tmp}
cp /usr/share/OVMF/OVMF_VARS.fd .
qemu-system-x86_64 \
  -m 1024 \
  -drive if=pflash,format=raw,unit=0,file=/usr/share/ovmf/OVMF.fd,readonly=on \
  -drive if=pflash,format=raw,unit=1,file=OVMF_VARS.fd \
  -drive format=raw,file="{config.image()}" \
  -nographic
"""

    expect_script = f"""\
set timeout 300
proc abort {{}} {{
    puts "ERROR ERROR\n"
    exit 1
}}
spawn {qemu_sh}
expect "login: "
send "root\n"
expect "# "
send "poweroff\r"
set timeout 5
expect {{
    "reboot: Power down" {{puts poweroffing\n}}
    eof abort
    timeout abort
}}
expect eof
"""

    with open(qemu_sh, "w") as f:
        f.write(qemu_script)
    os.chmod(qemu_sh, 0o755)

    with open(expect_txt, "w") as f:
        f.write(expect_script)

    p = subprocess.run(
        ["expect", "-d", expect_txt], check=False, capture_output=not config.verbose
    )
    shutil.rmtree(tmp)
    if p.returncode != 0:
        sys.stderr.write(p.stderr.decode())
        sys.exit(f"{config.image()} failed to boot")
    print(f"verified that {config.image()} boots OK")


def main():
    config = Config()
    if config.dump:
        config.write_vmdb("/dev/stdout")
    else:
        run_vmdb2(config)
        if config.boot:
            smoke_test(config)
        elif config.maybe_boot and config.qemu:
            smoke_test(config)


main()
