#!/usr/bin/python3.6

# SPDX Licence identifier MIT
# Copyright (c) 2017-2018 Red Hat Inc.
# Authors: Merlin Mathesius <merlinm@redhat.com>
#          Andrei Stepanov <astepano@redhat.com>
#          Bruno Goncalves <bgoncalv@redhat.com>

import os
import fmf
import sys
import json
import yaml
import time
import errno
import shlex
import signal
import socket
import shutil
import random
import logging
import argparse
import tempfile
import platform
import functools
import subprocess
import distutils.util


IDENTITY = """
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA1DrTSXQRF8isQQfPfK3U+eFC4zBrjur+Iy15kbHUYUeSHf5S
jXPYbHYqD1lHj4GJajC9okle9rykKFYZMmJKXLI6987wZ8vfucXo9/kwS6BDAJto
ZpZSj5sWCQ1PI0Ce8CbkazlTp5NIkjRfhXGP8mkNKMEhdNjaYceO49ilnNCIxhpb
eH5dH5hybmQQNmnzf+CGCCLBFmc4g3sFbWhI1ldyJzES5ZX3ahjJZYRUfnndoUM/
TzdkHGqZhL1EeFAsv5iV65HuYbchch4vBAn8jDMmHh8G1ixUCL3uAlosfarZLLyo
3HrZ8U/llq7rXa93PXHyI/3NL/2YP3OMxE8baQIDAQABAoIBAQCxuOUwkKqzsQ9W
kdTWArfj3RhnKigYEX9qM+2m7TT9lbKtvUiiPc2R3k4QdmIvsXlCXLigyzJkCsqp
IJiPEbJV98bbuAan1Rlv92TFK36fBgC15G5D4kQXD/ce828/BSFT2C3WALamEPdn
v8Xx+Ixjokcrxrdeoy4VTcjB0q21J4C2wKP1wEPeMJnuTcySiWQBdAECCbeZ4Vsj
cmRdcvL6z8fedRPtDW7oec+IPkYoyXPktVt8WsQPYkwEVN4hZVBneJPCcuhikYkp
T3WGmPV0MxhUvCZ6hSG8D2mscZXRq3itXVlKJsUWfIHaAIgGomWrPuqC23rOYCdT
5oSZmTvFAoGBAPs1FbbxDDd1fx1hisfXHFasV/sycT6ggP/eUXpBYCqVdxPQvqcA
ktplm5j04dnaQJdHZ8TPlwtL+xlWhmhFhlCFPtVpU1HzIBkp6DkSmmu0gvA/i07Z
pzo5Z+HRZFzruTQx6NjDtvWwiXVLwmZn2oiLeM9xSqPu55OpITifEWNjAoGBANhH
XwV6IvnbUWojs7uiSGsXuJOdB1YCJ+UF6xu8CqdbimaVakemVO02+cgbE6jzpUpo
krbDKOle4fIbUYHPeyB0NMidpDxTAPCGmiJz7BCS1fCxkzRgC+TICjmk5zpaD2md
HCrtzIeHNVpTE26BAjOIbo4QqOHBXk/WPen1iC3DAoGBALsD3DSj46puCMJA2ebI
2EoWaDGUbgZny2GxiwrvHL7XIx1XbHg7zxhUSLBorrNW7nsxJ6m3ugUo/bjxV4LN
L59Gc27ByMvbqmvRbRcAKIJCkrB1Pirnkr2f+xx8nLEotGqNNYIawlzKnqr6SbGf
Y2wAGWKmPyEoPLMLWLYkhfdtAoGANsFa/Tf+wuMTqZuAVXCwhOxsfnKy+MNy9jiZ
XVwuFlDGqVIKpjkmJyhT9KVmRM/qePwgqMSgBvVOnszrxcGRmpXRBzlh6yPYiQyK
2U4f5dJG97j9W7U1TaaXcCCfqdZDMKnmB7hMn8NLbqK5uLBQrltMIgt1tjIOfofv
BNx0raECgYEApAvjwDJ75otKz/mvL3rUf/SNpieODBOLHFQqJmF+4hrSOniHC5jf
f5GS5IuYtBQ1gudBYlSs9fX6T39d2avPsZjfvvSbULXi3OlzWD8sbTtvQPuCaZGI
Df9PUWMYZ3HRwwdsYovSOkT53fG6guy+vElUEDkrpZYczROZ6GUcx70=
-----END RSA PRIVATE KEY-----
"""
AUTH_KEY = ("AAAAB3NzaC1yc2EAAAADAQABAAABAQDUOtNJdBEXyKxBB898rdT54ULjMGuO6v4jLX"
            "mRsdRhR5Id/lKNc9hsdioPWUePgYlqML2iSV72vKQoVhkyYkpcsjr3zvBny9+5xej3"
            "+TBLoEMAm2hmllKPmxYJDU8jQJ7wJuRrOVOnk0iSNF+FcY/yaQ0owSF02Nphx47j2K"
            "Wc0IjGGlt4fl0fmHJuZBA2afN/4IYIIsEWZziDewVtaEjWV3InMRLllfdqGMllhFR+"
            "ed2hQz9PN2QcapmEvUR4UCy/mJXrke5htyFyHi8ECfyMMyYeHwbWLFQIve4CWix9qt"
            "ksvKjcetnxT+WWrutdr3c9cfIj/c0v/Zg/c4zETxtp")
DEF_USER = "root"
DEF_PASSWD = "foobar"
DEF_HOST = "127.0.0.3"
# configure sshd to use UseDNS no to fix broken EL7 systems
BOOTCMD_SSHD_USEDNS_NO = """bootcmd:
  - |
    if grep -q '^UseDNS' /etc/ssh/sshd_config; then
      sed 's/^UseDNS.*$/UseDNS no/' -i /etc/ssh/sshd_config
    elif grep -q '^#UseDNS' /etc/ssh/sshd_config; then
      sed 's/^#UseDNS.*$/UseDNS no/' -i /etc/ssh/sshd_config
    else
      echo "UseDNS no" >> /etc/ssh/sshd_config
    fi
"""
USER_DATA = """#cloud-config
users:
  - default
  - name: {0}
    ssh_authorized_keys:
      - ssh-rsa {2} standard-test-qcow2
ssh_pwauth: True
chpasswd:
  list: |
    {0}:{1}
  expire: False
""".format(DEF_USER, DEF_PASSWD, AUTH_KEY)
EMPTY_INVENTORY = {}
LOG_FILE = "default_provisioners.log"


class AdditionalDrives(object):
    """Prepare additional drives options for qemu.  Based on FMF config creates
    temporary sparse files and returns corresponding qemu command options.
    cleanup() will be called eventually to close the files.
    """

    _tempfiles = list()

    @classmethod
    def generate(cls, supported_devices, skip_missing_device):
        """Generate sparse files and return drive qemu options
        Returns
        -------
        list of str
                qemu -drive options
        """
        result = []
        drives = fmf_get(['qemu', 'drive'], list())
        dev_id = 0
        scsi_device_exists = False
        for drive in drives:
            # create temporary sparse file
            size = int(drive.get('size', 2 * 1024 ** 3))  # default size: 2G
            path = drive.get('path', None)
            path = str(path) if path is not None else None
            interface = drive.get('interface', None)
            drive_file = tempfile.NamedTemporaryFile(dir=path, delete=False)
            drive_file.truncate(size)
            cls._tempfiles.append(drive_file)
            logger.info("Created temporary sparse file '%s'.", drive_file.name)
            # translate data into qemu command options

            if interface is None or interface.lower() == 'virtio':
                result += ["-drive", "file=%s,media=disk,if=virtio" % drive_file.name]
            elif interface.lower() == 'nvme':
                if 'nvme' in supported_devices:
                    result += ["-device", "nvme,drive=nvme%s,serial=def%s" % (dev_id, dev_id)]
                    result += ["-drive", "file=%s,media=disk,if=none,id=nvme%s" % (drive_file.name, dev_id)]
                    dev_id += 1
                elif not skip_missing_device:
                    raise Exception("Cannot use device of size '%d' - no NVMe support on this platform" % size)
                else:
                    logger.warn("NVMe drive of size '%d' will be skipped - no NVMe support on this platform", size)
            elif interface.lower() == 'scsi':
                if 'scsi' in supported_devices:
                    if not scsi_device_exists:
                        result += ["-device", "virtio-scsi-pci,id=scsi0"]
                        scsi_device_exists = True
                    result += ["-device", "scsi-hd,drive=drive%s,bus=scsi0.0" % dev_id]
                    result += ["-drive", "if=none,file=%s,id=drive%s" % (drive_file.name, dev_id)]
                    dev_id += 1
                elif not skip_missing_device:
                    raise Exception("Cannot use device of size '%d' - no SCSI support on this platform" % size)
                else:
                    logger.warn("SCSI drive of size '%d' will be skipped - no SCSI support on this platform", size)

        usb_drives = fmf_get(['qemu', 'usb_drive'], list())
        if usb_drives:
            # there is at least 1 usb device, enable usb hub
            result += ["-usb"]
        usb_id = 0
        for usb_drive in usb_drives:
            usb_id += 1
            # create temporary sparse file
            size = int(usb_drive.get('size', 1 * 1024 ** 3))  # default size: 1G
            path = usb_drive.get('path', None)
            path = str(path) if path is not None else None
            usb_drive_file = tempfile.NamedTemporaryFile(dir=path, suffix='.img', prefix='usb_storage', delete=False)
            usb_drive_file.truncate(size)
            cls._tempfiles.append(usb_drive_file)
            logger.info("Created temporary sparse file '%s'.", usb_drive_file.name)
            # translate data into qemu command options
            result += ["-drive", "id=str_usb_storage_%s,file=%s,if=none" % (usb_id, usb_drive_file.name),
                       "-device", "usb-storage,drive=str_usb_storage_%s" % usb_id]

        return result

    @classmethod
    def cleanup(cls):
        """Close all temporary files created by this class
        """
        for drive_file in cls._tempfiles:
            fullname = drive_file.name
            logger.info("Closing and removing temporary sparse file '%s'", fullname)
            if os.path.isfile(fullname):
                drive_file.close()
                # just close the file doesn't make sure it gets deleted
                try:
                    os.remove(fullname)
                except OSError:
                    pass


def print_bad_inventory():
    """Print bad inventory on any uncatched exception. This will prevent
    running playbook on localhost.
    """
    fake_host = "fake_host"
    fake_hostname = "standard-inventory-qcow2_failed_check_logs"
    hosts = [fake_host]
    bad_inv = {"localhost": {"hosts": hosts, "vars": {}},
               "subjects": {"hosts": hosts, "vars": {}},
               "_meta": {"hostvars": {fake_host: {"ansible_host": fake_hostname}}}}
    sys.stdout.write(json.dumps(bad_inv, indent=4, separators=(',', ': ')))


# See https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python/377028#377028
def which(executable, default=None):
    def is_exe(fpath):
        return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
    fpath, _ = os.path.split(executable)
    if fpath:
        if is_exe(executable):
            return executable
    else:
        for path in os.environ['PATH'].split(os.pathsep):
            exe_file = os.path.join(path, executable)
            if is_exe(exe_file):
                return exe_file
    return default


def get_artifact_path(path=""):
    """Return path to an artifact file in artifacts directory. If path == ""
    than return path artifacts dir.  Create artifacts dir if necessary.
    """
    artifacts = os.environ.get("TEST_ARTIFACTS", os.path.join(os.getcwd(), "artifacts"))
    try:
        os.makedirs(artifacts)
    except OSError as exc:
        if exc.errno != errno.EEXIST or not os.path.isdir(artifacts):
            raise
    return os.path.join(artifacts, path)


def inv_list(opts):
    hosts = []
    variables = {}
    for ii, subject in enumerate(opts.subjects):
        if len(opts.hostalias) > ii:
            hostalias = opts.hostalias[ii]
        else:
            hostalias = None
        hostalias = image_to_alias(subject, hostalias, opts.use_basename)
        logger.info(f"image {subject} alias {hostalias}")
        host_vars = inv_host(opts, subject, hostalias)
        if host_vars:
            hosts.append(hostalias)
            variables[hostalias] = host_vars
    if not hosts:
        return EMPTY_INVENTORY
    return {"localhost": {"hosts": hosts, "vars": {}},
            "subjects": {"hosts": hosts, "vars": {}},
            "_meta": {"hostvars": variables}}


def get_qemu_smp_arg():
    """Determine the number of CPUs that should be visible in the guest.
    See e.g. https://www.redhat.com/archives/libvirt-users/2017-June/msg00025.html
    We want to match the number of host physical cores, or to go with the value
    provided explicitly via the STR_CPU_LIMIT environment variable.
    """
    if os.environ.get('STR_CPU_LIMIT'):
        sockets = os.environ.get('STR_CPU_LIMIT')
    else:
        # We may be run in a cgroup with fewer cores available than physical.
        available_cpu = int(subprocess.check_output(['nproc']).strip())
        # https://stackoverflow.com/questions/6481005/how-to-obtain-the-number-of-cpus-cores-in-linux-from-the-command-line
        core_sockets = set()
        for line in subprocess.check_output(['lscpu', '-b', '-p=Core,Socket'], universal_newlines=True).split("\n"):
            if line.startswith('#'):
                continue
            core_sockets.add(line.strip())
        sockets = min(available_cpu, len(core_sockets))
    return '{},sockets={},cores=1,threads=1'.format(sockets, sockets)


def write_debug_inventory(file_, host_vars):
    raw_inventory = {"all": {"children": {"localhost": {"hosts": host_vars},
                                          "subjects": {"hosts": host_vars},
                                          }
                             }
                     }
    with open(file_, "w") as ofile:
        inventory = yaml.dump(raw_inventory, default_flow_style=False)
        ofile.write(inventory)
    return inventory


class FmfMetadataTree(object):
    """This is aux class to hold one copy FMF tree. fmf.Tree(path) could be
    very resource consuming when walking through big project with many
    directories.

    Returns
    -------
    fmf.Tree() object or False.

    """
    tree = None
    """fmf.Tree() object."""
    path = None
    """Metadata tree is created for this path."""
    def get(self, path="."):
        if self.path != path or self.tree is None:
            FmfMetadataTree.path = path
            try:
                FmfMetadataTree.tree = fmf.Tree(path)
            except Exception:
                """Fmf initialization failed. Do not try initialize further for this path.
                """
                FmfMetadataTree.tree = False
        return self.tree


def fmf_get(path, default=None):
    """Return parameter from FMF at desired path or default.

    Parameters
    ----------
    path: dict
        List of strings. Strings form a path for looking parameter.
    default: str
        Function ignores this parameter.

    Returns
    -------
    str
        Found parameter in FMF or `default`.

    """
    tree = FmfMetadataTree().get()
    if not tree:
        return default
    path.insert(0, 'standard-inventory-qcow2')
    value = default
    for provision in tree.prune(names=[".*/provision$"]):
        value = provision.data
        for node in path:
            try:
                value = value[node]
            except (KeyError, TypeError):
                value = default
                break
    return value


def start_qemu(image, cloudinit, skip_missing_device, portrange=(2222, 5555)):
    for _ in range(10):
        port = random.randint(*portrange)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            sock.bind((DEF_HOST, port))
        except IOError:
            pass
        else:
            break
        finally:
            sock.close()
    else:
        raise RuntimeError("unable to find free local port to map SSH to")
    # Log all traffic received from the guest to a file.
    log_file = "{0}.guest.log".format(os.path.basename(image))
    log_guest = get_artifact_path(log_file)
    # Log from qemu itself.
    log_qemu = log_guest.replace(".guest.log", ".qemu.log")
    # Parameters from FMF:
    param_m = str(fmf_get(['qemu', 'm'], "2048"))
    param_net_nic_model = str(fmf_get(['qemu', 'net_nic', 'model'], 'virtio'))
    # QEMU -M param.
    # Most architectures do not need a -M flag, defaults are fine
    qemu_M_param = []
    # List of firmwares QEMU needs to work.
    qemu_firmwares = []
    # Common QEMU parameters used both to run VM and virtio-rng probing.
    qemu_common_params = [
        # Pass through CPU model of host
        "-cpu", "host",
        # Enable KVM full virtualization support
        "-enable-kvm",
        # Do not display video output
        "-display", "none",
    ]

    # Add platform specific settings:
    if "ppc" in platform.machine():
        # Disable VGA card to avoid crash on ppc with PR
        # https://lists.gnu.org/archive/html/qemu-devel/2018-05/msg07070.html
        qemu_common_params += ["-vga", "none"]

    if platform.machine() == "aarch64":
        # Emulate the same generic interrupt controller as the host system has
        qemu_M_param = ["-M", "virt,gic_version=host"]
        # Add AAVMF firmware (without this, qemu-kvm will not work on ARM)
        qemu_firmwares.extend([
            "-drive", "%s,%s,%s,%s,%s" % (
                "file=/usr/share/AAVMF/AAVMF_CODE.fd",
                "if=pflash",
                "format=raw",
                "unit=0",
                "readonly=on"
            )
        ])
    # Include -M param and firmwares to common params:
    qemu_common_params.extend(qemu_M_param)
    qemu_common_params.extend(qemu_firmwares)
    # Lookup for qemu:
    qemu_env = os.environ.get("QEMU_CMD")
    if qemu_env:
        path_lookups = [qemu_env]
    else:
        path_lookups = []
    path_lookups.extend([
        "qemu-kvm", "/usr/libexec/qemu-kvm", "/usr/bin/qemu-system-x86_64"
    ])
    for qemu_cmd in path_lookups:
        qemu_path = which(qemu_cmd)
        if qemu_path:
            break
    # Try to probe virtio-rng device:
    # virtio-rng-pci: https://wiki.qemu.org/Features/VirtIORNG
    virtio_rng = []
    cmd_tmpl = "echo quit | %s -device %%s -S -monitor stdio" % (
        " ".join([qemu_path] + qemu_common_params)
    )
    for x in ["virtio-rng", "virtio-rng-pci", "virtio-rng-ccw"]:
        try:
            subprocess.check_call(
                cmd_tmpl % x,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                shell=True
            )
            virtio_rng = ["-device", x]
            break
        except subprocess.CalledProcessError:
            pass
    if virtio_rng:
        logger.info("qemu-kvm is using %s device" % virtio_rng[1])
    # check for supported devices
    # - key is the name used in provision.fmf
    # - value is the name used by qemu
    supported_devices = {"nvme": "nvme",
                         "scsi": "virtio-scsi-pci"}
    cmd_tmpl = "echo quit | %s -device %%s -S -monitor stdio" % (
        " ".join([qemu_path] + qemu_common_params)
    )
    for name, qemu_name in list(supported_devices.items()):
        try:
            subprocess.check_call(
                cmd_tmpl % qemu_name,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                shell=True
            )
        except subprocess.CalledProcessError:
            del supported_devices[name]

    # Assemble QEMU command with its parameters:
    qemu_cmd = [
        qemu_path
        # Add common parameters
    ] + qemu_common_params + [
        # Simulate SMP system with get_qemu_smp_arg() CPUs
        "-smp", get_qemu_smp_arg(),
        # Set startup RAM size
        "-m", param_m,
        # Add image with RHEL as drive
        "-drive", "if=none,file=%s,id=virtio-disk0" % image,
        "-device", "virtio-blk-pci,drive=virtio-disk0,bootindex=1",
        # Write to temporary files instead of disk image files
        "-snapshot",
        # Use `cloudinit` as CD-ROM image
        "-cdrom", cloudinit,
        # Configure/create an on-board (or machine default) NIC
        "-net", "nic,model=%s" % param_net_nic_model,
        # Configure a host network backend
        "-net", "user,hostfwd=tcp:127.0.0.3:{0}-:22".format(port)
        # Add a source of randomness
    ] + virtio_rng + [
        # Let the RTC start at the current UTC
        "-rtc", "base=utc",
        # Connect the virtual serial port with pts2
        "-serial", "chardev:pts2",
        # Log all traffic received from the guest to log_quest
        "-chardev", "file,id=pts2,path=" + log_guest
    ]
    qemu_cmd += AdditionalDrives.generate(supported_devices, skip_missing_device)
    if diagnose:
        qemu_cmd += ["-vnc", DEF_HOST + ":1,to=4095"]
    # Launch QEMU:
    logger.info("Qemu CMD: {}".format(" ".join(qemu_cmd)))
    qemu_proc = subprocess.Popen(qemu_cmd, stdout=open(log_qemu, 'a'), stderr=subprocess.STDOUT)
    time.sleep(5)
    if qemu_proc and diagnose:
        logger.info("qemu-kvm is running with VNC server. PID: {}".format(qemu_proc.pid))
        logger.info("netstat -ltpn4 | grep {0} # to find VNC server port".format(qemu_proc.pid))
    return qemu_proc, port, log_guest


def image_to_alias(image, hostalias, use_basename):
    if not hostalias and not use_basename:
        return image
    if isinstance(hostalias, list) and len(hostalias) > 0:
        hostalias = hostalias[0]
    if hostalias == "BASENAME" or use_basename:
        return os.path.basename(image)
    return hostalias


def inv_host(opts, image, hostalias):
    if not image.endswith((".qcow2", ".qcow2c")):
        logger.info("Return empty inventory for image: %s.", image)
        return EMPTY_INVENTORY
    null = open(os.devnull, 'w')
    try:
        tty = os.open("/dev/tty", os.O_WRONLY)
        os.dup2(tty, 2)
    except OSError:
        tty = None
    # A directory for temporary stuff
    directory = tempfile.mkdtemp(prefix="inventory-cloud")
    identity = os.path.join(directory, "identity")
    with open(identity, 'w') as f:
        f.write(IDENTITY)
    os.chmod(identity, 0o600)
    metadata = os.path.join(directory, "meta-data")
    with open(metadata, 'w') as f:
        f.write("")
    userdata = os.path.join(directory, "user-data")
    with open(userdata, 'w') as f:
        f.write(USER_DATA)
        if opts.sshd_usedns_no:
            f.write(BOOTCMD_SSHD_USEDNS_NO)
    # Create our cloud init so we can log in
    cloudinit = os.path.join(directory, "cloud-init.iso")
    subprocess.check_call(["/usr/bin/genisoimage", "-input-charset", "utf-8",
                           "-volid", "cidata", "-joliet", "-rock", "-quiet",
                           "-output", cloudinit, userdata, metadata], stdout=null)
    logger.info("Launching virtual machine for {0}".format(image))
    # And launch the actual VM
    proc = None  # for failure detection
    cpe = None  # for exception scoping
    log = None
    for _ in range(0, 5):
        try:
            proc, port, log = start_qemu(image, cloudinit, opts.skip_missing_device)
            break
        except subprocess.CalledProcessError as cpe:
            time.sleep(1)
            continue
    if proc is None:
        raise RuntimeError("Could not launch VM for qcow2 image"
                           " '{0}':{1}".format(image, cpe.output))
    ssh_args = "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
    if opts.extra_ssh_args:
        ssh_args += " " + opts.extra_ssh_args
    hostalias = image_to_alias(image, hostalias, opts.use_basename)
    for _ in range(0, 600):
        try:
            # The variables
            variables = {
                "ansible_port": "{0}".format(port),
                "ansible_host": DEF_HOST,
                "ansible_user": DEF_USER,
                "ansible_ssh_pass": DEF_PASSWD,
                "ansible_ssh_private_key_file": identity,
                "ansible_ssh_common_args": ssh_args,
            }
            # Write out a handy inventory file, for our use and for debugging
            inventory = os.path.join(directory, "inventory")
            write_debug_inventory(inventory, {hostalias: variables})
            # Wait for ssh to come up
            ping = [
                ansible_bin,
                "--inventory",
                inventory,
                "localhost",
                "--module-name",
                "raw",
                "--args",
                "/bin/true"
            ]

            (pid, _) = os.waitpid(proc.pid, os.WNOHANG)
            if pid != 0:
                raise RuntimeError("qemu failed to launch VM for qcow2 image: {0}".format(image))
            subprocess.check_call(ping, stdout=null, stderr=null)
            break
        except subprocess.CalledProcessError:
            time.sleep(3)
    else:
        # Kill the qemu process
        try:
            os.kill(proc.pid, signal.SIGTERM)
        except OSError:
            pass
        # Read the last lines of the log
        try:
            with open(log) as f:
                data = f.readlines()
                output = "\nLast lines of {0}:\n".format(os.path.basename(log)) + "".join(data[-10:])
        except OSError:
            output = ""
        raise RuntimeError("Could not access VM launched from qcow2 image: {0}{1}".format(image,  output))
    # Process of our parent
    ppid = os.getppid()
    child = os.fork()
    if child:
        # Need to figure out what python interpreter to use
        interpreters = ["/usr/bin/python3", "/usr/bin/python2", "/usr/libexec/platform-python"]
        for interpreter in interpreters:
            check_file = [
                ansible_bin,
                "--inventory",
                inventory,
                "localhost",
                "--module-name",
                "raw",
                "--args",
                "ls %s" % interpreter
            ]
            try:
                subprocess.check_call(check_file, stdout=null, stderr=null)
                ansible_python_interpreter = interpreter
                break
            except subprocess.CalledProcessError:
                pass
        else:
            logger.error("Could not set ansible_python_interpreter.")
            return None
        variables["ansible_python_interpreter"] = ansible_python_interpreter
        # Update inventory file
        write_debug_inventory(inventory, {hostalias: variables})
        return variables
    # Daemonize and watch the processes
    os.chdir("/")
    os.setsid()
    os.umask(0)
    if tty is None:
        tty = null.fileno()
    # Duplicate standard input to standard output and standard error.
    os.dup2(null.fileno(), 0)
    os.dup2(tty, 1)
    os.dup2(tty, 2)
    # alternatively, lock on a file
    lock_file = os.environ.get("LOCK_ON_FILE", None)
    ssh_cmd = ("ssh -p {port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {identity} {user}@{host}"
               ).format(port=port, identity=identity, user=DEF_USER, host=DEF_HOST)
    logger.info(ssh_cmd)
    logger.info("Password is: {}".format(DEF_PASSWD))
    logger.info("Cloudinit init location: {}".format(directory))
    logger.info("export ANSIBLE_INVENTORY={0}".format(inventory))
    logger.info("Wait until parent for provision-script (ansible-playbook) dies or qemu.")
    while True:
        time.sleep(3)
        if lock_file:
            if not os.path.exists(lock_file):
                logger.error("Lock file is gone.")
                break
        else:
            # Now wait for the parent process to go away, then kill the VM
            try:
                os.kill(ppid, 0)
                os.kill(proc.pid, 0)
            except OSError:
                logger.info("Either parent process or VM process is gone.")
                break
    if diagnose:
        def _signal_handler(*args):
            logger.info("Diagnose ending.")
        signal.signal(signal.SIGTERM, _signal_handler)
        logger.info("kill {0} # when finished to debug VM.".format(os.getpid()))
        signal.pause()
    # Kill the qemu process
    try:
        os.kill(proc.pid, signal.SIGTERM)
        AdditionalDrives.cleanup()
    except OSError:
        pass
    shutil.rmtree(directory)
    sys.exit(0)


def help_hostalias():
    return """By default, this script will add each subject to the inventory, which is
    typically the full path and filename of the disk image to use.  If you
    cannot use a full path in the inventory, you can use the --hostalias to
    specify an alias to use for the image in the inventory.  The position of
    each --hostalias you specify on the command line must correspond exactly to
    the position of the subject you specify on the command line or in the
    TEST_SUBJECTS env var.  For example, if you specify
    TEST_SUBJECTS="/path/to/a.qcow2 /path/to/b.qcow2" ... --hostalias a --hostalias b
    Then the inventory would use "a" as the alias for /path/to/a.qcow2, and
    b as the alias for /path/to/b.qcow2.  If you want to use the basename of the file
    as the hostalias, specify the special value 'BASENAME'.  For example, if you have
    TEST_SUBJECTS="/path/to/a.qcow2 /path/to/b.qcow2" ... --hostalias BASENAME --hostalias b
    Then the inventory will use "a.qcow2" as the alias for path /path/to/a.qcow2, and will
    use "b" as the alias for path /path/to/b.qcow2.
    If you want all of the subjects to use the basename of the file, use option
    --use-basename, then you do not have to use --hostalias for every subject.
    """


def help_sshd_usedns_no():
    return """On some EL7 systems, sshd uses 'UseDNS yes' by default.  This can
    create a very bad performance problem with ssh and Ansible.  You can use
    --sshd-usedns-no to configure the VM to use 'UseDNS no' instead."""


def help_skip_missing_device():
    return """If you are using the same provision.fmf on multiple systems, the
    qemu on some systems may not support all of the specified devices.  For
    example, qemu on many platforms does not support NVMe.  By default, the
    script will issue an error if you attempt to use an unsupported device.
    Use this flag to skip missing devices, with a warning."""


def main():
    global logger
    global diagnose
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)
    # stderr output
    conhandler = logging.StreamHandler()
    # Print to strerr by default messages with level >= warning, can be changed
    # with setting TEST_DEBUG=1.
    try:
        diagnose = distutils.util.strtobool(os.getenv("TEST_DEBUG", "0"))
    except ValueError:
        diagnose = 0
    conhandler.setLevel(logging.WARNING)
    if diagnose:
        # Collect all messages with any log level to stderr.
        conhandler.setLevel(logging.NOTSET)
    # Log format for stderr.
    log_format = "[%(levelname)-5.5s] {}: %(message)s".format(os.path.basename(__file__))
    formatter = logging.Formatter(log_format)
    conhandler.setFormatter(formatter)
    logger.addHandler(conhandler)
    parser = argparse.ArgumentParser(description="Inventory for a QCow2 test image")
    # list is currently unused
    parser.add_argument("--list", action="store_true", help="Verbose output")
    parser.add_argument('--extra-ssh-args', default=os.environ.get("TEST_EXTRA_SSH_ARGS"), help="Extra arguments to pass to SSH")
    parser.add_argument('--host', help="Full path to qcow2 disk image")
    parser.add_argument("--use-basename", default=bool(distutils.util.strtobool(os.environ.get("TEST_USE_BASENAME", "False"))), action="store_true", help=help_hostalias())
    parser.add_argument("--hostalias", default=shlex.split(os.environ.get("TEST_HOSTALIASES", "")), action="append", help=help_hostalias())
    parser.add_argument("--sshd-usedns-no", default=bool(distutils.util.strtobool(os.environ.get("TEST_SSHD_USEDNS_NO", "False"))), action="store_true", help=help_sshd_usedns_no())
    parser.add_argument("--skip-missing-device", default=bool(distutils.util.strtobool(os.environ.get("TEST_SKIP_MISSING_DEVICE", "False"))), action="store_true", help=help_skip_missing_device())
    parser.add_argument("subjects", nargs="*", default=shlex.split(os.environ.get("TEST_SUBJECTS", "")))
    opts = parser.parse_args()
    # Send logs to common logfile for all default provisioners.
    log_file = get_artifact_path(LOG_FILE)
    fhandler = logging.FileHandler(log_file)
    # Collect all messages with any log level to log file.
    fhandler.setLevel(logging.NOTSET)
    log_format = ("%(asctime)s [{}/%(threadName)-12.12s] [%(levelname)-5.5s]:"
                  "%(message)s").format(os.path.basename(__file__))
    logFormatter = logging.Formatter(log_format)
    fhandler.setFormatter(logFormatter)
    logger.addHandler(fhandler)
    logger.info("Start provisioner.")
    if opts.use_basename and opts.hostalias:
        logger.critical("Use only one of --use-basename or --hostalias")
        sys.exit(1)
    ansibles = ['ansible', 'ansible-3', None]
    global ansible_bin
    ansible_bin = functools.reduce(which, ansibles)
    if not ansible_bin:
        raise Exception("Fail to find ansible.")
    logger.info("Path to ansible: %s", ansible_bin)
    if opts.host:
        data = inv_host(opts, opts.host, opts.hostalias)
    else:
        if (
            opts.hostalias
            and len(opts.hostalias) != len(opts.subjects)
        ):
            logger.critical(
                "If you are using hostalias, you must specify a --hostalias for each subject."
            )
            sys.exit(1)
        data = inv_list(opts)
    # Dump Ansible inventory.
    sys.stdout.write(json.dumps(data, indent=4, separators=(',', ': ')))


if __name__ == '__main__':
    ret = -1
    try:
        main()
        ret = 0
    except RuntimeError as ex:
        print_bad_inventory()
        logger.error("{0}".format(ex))
    except Exception:
        print_bad_inventory()
        # Backtrace stack goes to log file. If TEST_DEBUG == 1, it goes to stderr too.
        logger.error("Fatal error in provision script.", exc_info=True)
    sys.exit(ret)
