#!/usr/bin/env python3
#
# Copyright 2005-2014 Red Hat, Inc.
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.

import argparse
import atexit
import logging
import sys
import time

import libvirt

import virtinst
from virtinst import cli
from virtinst.cli import fail, print_stdout, print_stderr


##############################
# Validation utility helpers #
##############################

install_methods = "--location URL, --cdrom CD/ISO, --pxe, --import, --boot hd|cdrom|..."


def all_install_options(options):
    return [options.pxe, options.cdrom, options.location,
            options.import_install]


def install_specified(options):
    return any([bool(o) for o in all_install_options(options)])


def supports_pxe(guest):
    """
    Return False if we are pretty sure the config doesn't support PXE
    """
    for nic in guest.devices.interface:
        if nic.type == nic.TYPE_USER:
            continue
        if nic.type != nic.TYPE_VIRTUAL:
            return True

        try:
            netobj = nic.conn.networkLookupByName(nic.source)
            xmlobj = virtinst.Network(nic.conn, parsexml=netobj.XMLDesc(0))
            if xmlobj.can_pxe():
                return True
        except Exception:
            logging.debug("Error checking if PXE supported", exc_info=True)
            return True

    return False


def check_cdrom_option_error(options):
    if options.cdrom_short and options.cdrom:
        fail("Cannot specify both -c and --cdrom")

    if options.cdrom_short:
        if "://" in options.cdrom_short:
            fail("-c specified with what looks like a URI. Did you mean "
                 "to use --connect? If not, use --cdrom instead")
        options.cdrom = options.cdrom_short

    if not options.cdrom:
        return

    # Catch a strangely common error of users passing -vcpus=2 instead of
    # --vcpus=2. The single dash happens to map to enough shortened options
    # that things can fail weirdly if --paravirt is also specified.
    for vcpu in [o for o in sys.argv if o.startswith("-vcpu")]:
        if options.cdrom == vcpu[3:]:
            fail("You specified -vcpus, you want --vcpus")


#################################
# Back compat option conversion #
#################################

def convert_old_printxml(options):
    if options.xmlstep:
        options.xmlonly = options.xmlstep
        del(options.xmlstep)


def convert_old_sound(options):
    if not options.sound:
        return
    for idx in range(len(options.sound)):
        if options.sound[idx] is None:
            options.sound[idx] = "default"


def convert_old_init(options):
    if not options.init:
        return
    if not options.boot:
        options.boot = [""]
    options.boot[-1] += ",init=%s" % options.init
    logging.debug("Converted old --init to --boot %s", options.boot[-1])


def _do_convert_old_disks(options):
    paths = virtinst.util.listify(options.file_paths)
    sizes = virtinst.util.listify(options.disksize)

    def padlist(l, padsize):
        l = virtinst.util.listify(l)
        l.extend((padsize - len(l)) * [None])
        return l

    disklist = padlist(paths, max(0, len(sizes)))
    sizelist = padlist(sizes, len(disklist))

    opts = []
    for idx, path in enumerate(disklist):
        optstr = ""
        if path:
            optstr += "path=%s" % path
        if sizelist[idx]:
            if optstr:
                optstr += ","
            optstr += "size=%s" % sizelist[idx]
        if options.sparse is False:
            if optstr:
                optstr += ","
            optstr += "sparse=no"
        logging.debug("Converted to new style: --disk %s", optstr)
        opts.append(optstr)

    options.disk = opts


def convert_old_disks(options):
    if options.nodisks and (options.file_paths or
                            options.disk or
                            options.disksize):
        fail(_("Cannot specify storage and use --nodisks"))

    if ((options.file_paths or options.disksize or not options.sparse) and
        options.disk):
        fail(_("Cannot mix --file, --nonsparse, or --file-size with --disk "
               "options. Use --disk PATH[,size=SIZE][,sparse=yes|no]"))

    if not options.disk:
        if options.nodisks:
            options.disk = ["none"]
        else:
            _do_convert_old_disks(options)

    del(options.file_paths)
    del(options.disksize)
    del(options.sparse)
    del(options.nodisks)
    logging.debug("Distilled --disk options: %s", options.disk)


def convert_old_os_options(options):
    distro_variant = options.distro_variant
    distro_type = options.distro_type
    if not distro_type and not distro_variant:
        # Default to distro autodetection
        options.distro_variant = "auto"
        return

    distro_variant = distro_variant and str(distro_variant).lower() or None
    distro_type = distro_type and str(distro_type).lower() or None
    distkey = distro_variant or distro_type
    if not distkey or distkey == "none":
        options.distro_variant = "none"
    else:
        options.distro_variant = distkey


def convert_old_memory(options):
    if options.memory:
        return
    if not options.oldmemory:
        return
    options.memory = str(options.oldmemory)


def convert_old_cpuset(options):
    if not options.cpuset:
        return
    if not options.vcpus:
        options.vcpus = [""]
    options.vcpus[-1] += ",cpuset=%s" % options.cpuset
    logging.debug("Generated compat cpuset: --vcpus %s", options.vcpus[-1])


def convert_old_networks(options):
    if options.nonetworks:
        if options.mac:
            fail(_("Cannot use --mac with --nonetworks"))
        if options.bridge:
            fail(_("Cannot use --bridge with --nonetworks"))
        if options.network:
            fail(_("Cannot use --nonetworks with --network"))
        options.network = ["none"]

    macs = virtinst.util.listify(options.mac)
    networks = virtinst.util.listify(options.network)
    bridges = virtinst.util.listify(options.bridge)

    if bridges and networks:
        fail(_("Cannot mix both --bridge and --network arguments"))

    if bridges:
        # Convert old --bridges to --networks
        networks = ["bridge:" + b for b in bridges]

    def padlist(l, padsize):
        l = virtinst.util.listify(l)
        l.extend((padsize - len(l)) * [None])
        return l

    # If a plain mac is specified, have it imply a default network
    networks = padlist(networks, max(len(macs), 1))
    macs = padlist(macs, len(networks))

    for idx, ignore in enumerate(networks):
        if networks[idx] is None:
            networks[idx] = "default"
        if macs[idx]:
            networks[idx] += ",mac=%s" % macs[idx]

        # Handle old format of bridge:foo instead of bridge=foo
        for prefix in ["network", "bridge"]:
            if networks[idx].startswith(prefix + ":"):
                networks[idx] = networks[idx].replace(prefix + ":",
                                                      prefix + "=")

    del(options.mac)
    del(options.bridge)
    del(options.nonetworks)

    options.network = networks
    logging.debug("Distilled --network options: %s", options.network)


def convert_old_graphics(options):
    vnc = options.vnc
    vncport = options.vncport
    vnclisten = options.vnclisten
    nographics = options.nographics
    sdl = options.sdl
    keymap = options.keymap
    graphics = options.graphics

    if graphics and (vnc or sdl or keymap or vncport or vnclisten):
        fail(_("Cannot mix --graphics and old style graphical options"))

    optnum = sum([bool(g) for g in [vnc, nographics, sdl, graphics]])
    if optnum > 1:
        raise ValueError(_("Can't specify more than one of VNC, SDL, "
                           "--graphics or --nographics"))

    if options.graphics:
        return

    if optnum == 0:
        return

    # Build a --graphics command line from old style opts
    optstr = ((vnc and "vnc") or
              (sdl and "sdl") or
              (nographics and ("none")))
    if vnclisten:
        optstr += ",listen=%s" % vnclisten
    if vncport:
        optstr += ",port=%s" % vncport
    if keymap:
        optstr += ",keymap=%s" % keymap

    logging.debug("--graphics compat generated: %s", optstr)
    options.graphics = [optstr]


def convert_old_features(options):
    if options.features:
        return

    opts = ""
    if options.noacpi:
        opts += "acpi=off"
    if options.noapic:
        if opts:
            opts += ","
        opts += "apic=off"
    if opts:
        options.features = [opts]


##################################
# Install media setup/validation #
##################################

def set_distro_variant(options, guest, installer):
    distro = None
    try:
        installer.check_location(guest)

        if options.distro_variant == "auto":
            distro = installer.detect_distro(guest)
        elif options.distro_variant != "none":
            distro = options.distro_variant

    except ValueError as e:
        fail(_("Error validating install location: %s") % str(e))

    if distro:
        guest.set_os_name(distro)


def do_test_media_detection(conn, options):
    url = options.test_media_detection
    guest = virtinst.Guest(conn)
    if options.arch:
        guest.os.arch = options.arch
    if options.os_type:
        guest.os.os_type = options.os_type
    guest.set_capabilities_defaults()

    installer = virtinst.Installer(conn, location=url)
    print_stdout(installer.detect_distro(guest), do_force=True)


#############################
# General option validation #
#############################

def validate_required_options(options, guest, installer):
    # Required config. Don't error right away if nothing is specified,
    # aggregate the errors to help first time users get it right
    msg = ""

    if not guest.name:
        msg += "\n" + _("--name is required")

    if not guest.memory:
        msg += "\n" + _("--memory amount in MiB is required")

    if (not guest.os.is_container() and
        not (options.disk or options.filesystem)):
        msg += "\n" + (
            _("--disk storage must be specified (override with --disk none)"))

    if not installer:
        msg += "\n" + (
            _("An install method must be specified\n(%(methods)s)") %
            {"methods": install_methods})

    if msg:
        fail(msg)


_cdrom_location_man_page = _("See the man page for examples of "
    "using --location with CDROM media")


def check_option_collisions(options, guest, installer):
    if options.noreboot and options.transient:
        fail(_("--noreboot and --transient can not be specified together"))

    # Install collisions
    if sum([bool(l) for l in all_install_options(options)]) > 1:
        fail(_("Only one install method can be used (%(methods)s)") %
             {"methods": install_methods})

    if guest.os.is_container() and install_specified(options):
        fail(_("Install methods (%s) cannot be specified for "
               "container guests") % install_methods)

    if guest.os.is_xenpv():
        if options.pxe:
            fail(_("Network PXE boot is not supported for paravirtualized "
                   "guests"))
        if options.cdrom or options.livecd:
            fail(_("Paravirtualized guests cannot install off cdrom media."))

    if (options.location and
        guest.conn.is_remote() and not
        guest.conn.support_remote_url_install()):
        fail(_("Libvirt version does not support remote --location installs"))

    cdrom_err = ""
    if installer.cdrom:
        cdrom_err = " " + _cdrom_location_man_page
    if not options.location and options.extra_args:
        fail(_("--extra-args only work if specified with --location.") +
             cdrom_err)
    if not options.location and options.initrd_inject:
        fail(_("--initrd-inject only works if specified with --location.") +
             cdrom_err)


def _show_nographics_warnings(options, guest, installer):
    if guest.devices.graphics:
        return
    if not options.autoconsole:
        return

    if installer.cdrom:
        logging.warning(_("CDROM media does not print to the text console "
            "by default, so you likely will not see text install output. "
            "You might want to use --location.") + " " +
            _cdrom_location_man_page)
        return

    if not options.location:
        return

    # Trying --location --nographics with console connect. Warn if
    # they likely won't see any output.

    if not guest.devices.console:
        logging.warning(_("No --console device added, you likely will not "
            "see text install output from the guest."))
        return

    serial_arg = "console=ttyS0"
    serial_arm_arg = "console=ttyAMA0"
    hvc_arg = "console=hvc0"

    console_type = serial_arg
    if guest.os.is_arm():
        console_type = serial_arm_arg
    if guest.devices.console[0].target_type in ["virtio", "xen"]:
        console_type = hvc_arg
    if guest.os.is_ppc64() or guest.os.is_arm_machvirt():
        # Later arm/ppc kernels figure out console= automatically, so don't
        # warn about it.
        return

    for args in (options.extra_args or []):
        if console_type in (args or ""):
            return

    logging.warning(_("Did not find '%(console_string)s' in --extra-args, "
        "which is likely required to see text install output from the "
        "guest."), {"console_string": console_type})


def show_warnings(options, guest, installer):
    if options.pxe and not supports_pxe(guest):
        logging.warning(_("The guest's network configuration does not support "
                       "PXE"))

    # Limit it to hvm x86 guests which presently our defaults
    # only really matter for
    if (guest.osinfo.name == "generic" and
        options.distro_variant not in ["none", "generic"] and
        guest.os.is_x86() and guest.os.is_hvm()):
        logging.warning(_("No operating system detected, VM performance may "
            "suffer. Specify an OS with --os-variant for optimal results."))

    _show_nographics_warnings(options, guest, installer)


##########################
# Guest building helpers #
##########################

def build_installer(options, guest):
    cdrom = None
    location = None
    install_bootdev = None

    has_installer = True
    if options.location:
        location = options.location
    elif options.cdrom:
        cdrom = options.cdrom
    elif options.pxe:
        install_bootdev = "network"
    elif (guest.os.is_container() or
          options.import_install or
          options.xmlonly or
          options.boot):
        pass
    else:
        has_installer = False

    if not has_installer:
        # This triggers an error in validate_required_options
        return None

    installer = virtinst.Installer(guest.conn,
            cdrom=cdrom,
            location=location,
            install_bootdev=install_bootdev)
    if cdrom and options.livecd:
        installer.livecd = True
    if options.extra_args:
        installer.extra_args = options.extra_args
    if options.initrd_inject:
        installer.set_initrd_injections(options.initrd_inject)
    if options.autostart:
        installer.autostart = True

    return installer


def build_guest_instance(conn, options):
    guest = virtinst.Guest(conn)

    if options.name:
        guest.name = options.name
    if options.uuid:
        guest.uuid = options.uuid
    if options.description:
        guest.description = options.description
    if options.os_type:
        guest.os.os_type = options.os_type
    if options.virt_type:
        guest.type = options.virt_type
    if options.arch:
        guest.os.arch = options.arch
    if options.machine:
        guest.os.machine = options.machine

    cli.parse_option_strings(options, guest, None)

    # cli specific disk validation
    for disk in guest.devices.disk:
        cli.validate_disk(disk)

    # Do this a bit early so we have os_type/arch checks for installer
    # building, and distro variant detection
    guest.set_capabilities_defaults()

    installer = build_installer(options, guest)
    if installer:
        set_distro_variant(options, guest, installer)
        installer.set_install_defaults(guest)

    validate_required_options(options, guest, installer)
    check_option_collisions(options, guest, installer)
    show_warnings(options, guest, installer)

    return guest, installer


###########################
# Install process helpers #
###########################

def start_install(guest, installer, options):
    if options.wait is not None:
        wait_on_install = True
        wait_time = options.wait * 60
    else:
        wait_on_install = False
        wait_time = -1

    # If --wait specified, we don't want the default behavior of waiting
    # for virt-viewer to exit, since then we can't exit the app when time
    # expires
    wait_on_console = not wait_on_install

    if wait_time == 0:
        # --wait 0 implies --noautoconsole
        autoconsole = False
    else:
        autoconsole = options.autoconsole

    conscb = None
    if autoconsole:
        conscb = cli.get_console_cb(guest)
        if not conscb:
            # If there isn't any console to actually connect up,
            # default to --wait -1 to get similarish behavior
            autoconsole = False
            if options.wait is None:
                logging.warning(_("No console to launch for the guest, "
                    "defaulting to --wait -1"))
                wait_on_install = True
                wait_time = -1

    meter = cli.get_meter()
    logging.debug("Guest.has_install_phase: %s",
                  installer.has_install_phase())

    # we've got everything -- try to start the install
    print_stdout(_("\nStarting install..."))

    domain = None
    try:
        start_time = time.time()

        domain = installer.start_install(guest, meter=meter,
                doboot=not options.noreboot,
                transient=options.transient)

        if options.destroy_on_exit:
            atexit.register(_destroy_on_exit, domain)

        cli.connect_console(guest, domain, conscb, wait_on_console,
                options.destroy_on_exit)
        check_domain(installer, domain, conscb, options.transient,
                     wait_on_install, wait_time, start_time)

        print_stdout(_("Domain creation completed."))
        if not options.transient and not domain.isActive():
            if options.noreboot or not installer.has_install_phase():
                print_stdout(
                    _("You can restart your domain by running:\n  %s") %
                    cli.virsh_start_cmd(guest))
            else:
                print_stdout(_("Restarting guest."))
                domain.create()
                cli.connect_console(guest, domain, conscb, True,
                        options.destroy_on_exit)

    except KeyboardInterrupt:
        logging.debug("", exc_info=True)
        print_stderr(_("Domain install interrupted."))
        raise
    except Exception as e:
        fail(e, do_exit=False)
        if domain is None:
            installer.cleanup_created_disks(guest, meter)
        cli.install_fail(guest)


def check_domain(installer, domain, conscb, transient,
                 wait_for_install, wait_time, start_time):
    """
    Make sure domain ends up in expected state, and wait if for install
    to complete if requested
    """
    def check_domain_inactive():
        try:
            dominfo = domain.info()
            state = dominfo[0]
            logging.debug("Domain state after install: %s", state)

            if state == libvirt.VIR_DOMAIN_CRASHED:
                fail(_("Domain has crashed."))

            return not domain.isActive()
        except libvirt.libvirtError as e:
            if transient and e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN:
                logging.debug("transient VM shutdown and disappeared.")
                return True
            raise

    if check_domain_inactive():
        return

    if bool(conscb):
        # We are trying to detect if the VM shutdown, or the user
        # just closed the console and the VM is still running. In the
        # the former case, libvirt may not have caught up yet with the
        # VM having exited, so wait a bit and check again
        if not cli.in_testsuite():
            time.sleep(2)
        if check_domain_inactive():
            return

    # If we reach here, the VM still appears to be running.
    if not wait_for_install or wait_time == 0:
        # User either:
        #   used --noautoconsole
        #   used --wait 0
        #   killed console and guest is still running
        if not installer.has_install_phase():
            return

        print_stdout(
            _("Domain installation still in progress. You can reconnect"
              " to \nthe console to complete the installation process."))
        sys.exit(0)

    wait_forever = (wait_time < 0)
    timestr = (not wait_forever and
               _(" %d minutes") % (int(wait_time) / 60) or "")
    print_stdout(
        _("Domain installation still in progress. Waiting"
          "%(time_string)s for installation to complete.") %
        {"time_string": timestr})

    # Wait loop
    while True:
        if not domain.isActive():
            print_stdout(_("Domain has shutdown. Continuing."))
            break

        if not cli.in_testsuite():
            time.sleep(1)

        time_elapsed = (time.time() - start_time)
        if not cli.in_testsuite():
            if wait_forever:
                continue
            if time_elapsed < wait_time:
                continue

        print_stdout(
            _("Installation has exceeded specified time limit. "
              "Exiting application."))
        sys.exit(1)


########################
# XML printing helpers #
########################

def xml_to_print(guest, installer, xmlonly, dry):
    start_xml, final_xml = installer.start_install(
            guest, dry=dry, return_xml=True)
    if not start_xml:
        start_xml = final_xml
        final_xml = None

    if dry and not xmlonly:
        print_stdout(_("Dry run completed successfully"))
        return

    if xmlonly not in [False, "1", "2", "all"]:
        fail(_("Unknown XML step request '%s', must be 1, 2, or all") %
             xmlonly)

    if xmlonly == "1":
        return start_xml
    if xmlonly == "2":
        if not final_xml:
            fail(_("Requested installation does not have XML step 2"))
        return final_xml

    # "all" case
    xml = start_xml
    if final_xml:
        xml += final_xml
    return xml


#######################
# CLI option handling #
#######################

def parse_args():
    parser = cli.setupParser(
        "%(prog)s --name NAME --memory MB STORAGE INSTALL [options]",
        _("Create a new virtual machine from specified install media."),
        introspection_epilog=True)
    cli.add_connect_option(parser)

    geng = parser.add_argument_group(_("General Options"))
    geng.add_argument("-n", "--name",
                    help=_("Name of the guest instance"))
    cli.add_memory_option(geng, backcompat=True)
    cli.vcpu_cli_options(geng)
    cli.add_metadata_option(geng)
    geng.add_argument("-u", "--uuid", help=argparse.SUPPRESS)
    geng.add_argument("--description", help=argparse.SUPPRESS)

    insg = parser.add_argument_group(_("Installation Method Options"))
    insg.add_argument("-c", dest="cdrom_short", help=argparse.SUPPRESS)
    insg.add_argument("--cdrom", help=_("CD-ROM installation media"))
    insg.add_argument("-l", "--location",
            help=_("Distro install URL, eg. https://host/path. See man "
                   "page for specific distro examples."))
    insg.add_argument("--pxe", action="store_true",
                    help=_("Boot from the network using the PXE protocol"))
    insg.add_argument("--import", action="store_true", dest="import_install",
                    help=_("Build guest around an existing disk image"))
    insg.add_argument("--livecd", action="store_true",
                    help=_("Treat the CD-ROM media as a Live CD"))
    insg.add_argument("-x", "--extra-args", action="append",
                    help=_("Additional arguments to pass to the install kernel "
                           "booted from --location"))
    insg.add_argument("--initrd-inject", action="append",
                    help=_("Add given file to root of initrd from --location"))

    # Takes a URL and just prints to stdout the detected distro name
    insg.add_argument("--test-media-detection", help=argparse.SUPPRESS)
    # Helper for cli testing, fills in standard stub options
    insg.add_argument("--test-stub-command", action="store_true",
            help=argparse.SUPPRESS)

    insg.add_argument("--os-type", dest="distro_type", help=argparse.SUPPRESS)
    insg.add_argument("--os-variant", dest="distro_variant",
        help=_("The OS variant being installed guests, "
               "e.g. 'fedora18', 'rhel6', 'winxp', etc."))

    cli.add_boot_options(insg)
    insg.add_argument("--init", help=argparse.SUPPRESS)


    devg = parser.add_argument_group(_("Device Options"))
    cli.add_disk_option(devg)
    cli.add_net_option(devg)
    cli.add_gfx_option(devg)
    cli.add_device_options(devg, sound_back_compat=True)

    # Deprecated device options
    devg.add_argument("-f", "--file", dest="file_paths", action="append",
                    help=argparse.SUPPRESS)
    devg.add_argument("-s", "--file-size", type=float,
                    action="append", dest="disksize",
                    help=argparse.SUPPRESS)
    devg.add_argument("--nonsparse", action="store_false",
                    default=True, dest="sparse",
                    help=argparse.SUPPRESS)
    devg.add_argument("--nodisks", action="store_true", help=argparse.SUPPRESS)
    devg.add_argument("--nonetworks", action="store_true",
        help=argparse.SUPPRESS)
    devg.add_argument("-b", "--bridge", action="append",
        help=argparse.SUPPRESS)
    devg.add_argument("-m", "--mac", action="append", help=argparse.SUPPRESS)
    devg.add_argument("--vnc", action="store_true", help=argparse.SUPPRESS)
    devg.add_argument("--vncport", type=int, help=argparse.SUPPRESS)
    devg.add_argument("--vnclisten", help=argparse.SUPPRESS)
    devg.add_argument("-k", "--keymap", help=argparse.SUPPRESS)
    devg.add_argument("--sdl", action="store_true", help=argparse.SUPPRESS)
    devg.add_argument("--nographics", action="store_true",
        help=argparse.SUPPRESS)


    gxmlg = parser.add_argument_group(_("Guest Configuration Options"))
    cli.add_guest_xml_options(gxmlg)


    virg = parser.add_argument_group(_("Virtualization Platform Options"))
    ostypeg = virg.add_mutually_exclusive_group()
    ostypeg.add_argument("-v", "--hvm",
        action="store_const", const="hvm", dest="os_type",
        help=_("This guest should be a fully virtualized guest"))
    ostypeg.add_argument("-p", "--paravirt",
        action="store_const", const="xen", dest="os_type",
        help=_("This guest should be a paravirtualized guest"))
    ostypeg.add_argument("--container",
        action="store_const", const="exe", dest="os_type",
        help=_("This guest should be a container guest"))
    virg.add_argument("--virt-type",
        help=_("Hypervisor name to use (kvm, qemu, xen, ...)"))
    virg.add_argument("--arch", help=_("The CPU architecture to simulate"))
    virg.add_argument("--machine", help=_("The machine type to emulate"))
    virg.add_argument("--accelerate", action="store_true",
        help=argparse.SUPPRESS)
    virg.add_argument("--noapic", action="store_true",
        default=False, help=argparse.SUPPRESS)
    virg.add_argument("--noacpi", action="store_true",
        default=False, help=argparse.SUPPRESS)


    misc = parser.add_argument_group(_("Miscellaneous Options"))
    misc.add_argument("--autostart", action="store_true", default=False,
                      help=_("Have domain autostart on host boot up."))
    misc.add_argument("--transient", action="store_true", default=False,
                      help=_("Create a transient domain."))
    misc.add_argument("--destroy-on-exit", action="store_true", default=False,
                      help=_("Force power off the domain when the console "
                             "viewer is closed."))
    misc.add_argument("--wait", type=int,
                      help=_("Minutes to wait for install to complete."))

    cli.add_misc_options(misc, prompt=True, printxml=True, printstep=True,
                         noreboot=True, dryrun=True, noautoconsole=True)

    return parser.parse_args()


###################
# main() handling #
###################

# Catchall for destroying the VM on ex. ctrl-c
def _destroy_on_exit(domain):
    try:
        isactive = bool(domain and domain.isActive())
        if isactive:
            domain.destroy()
    except libvirt.libvirtError as e:
        if e.get_error_code() != libvirt.VIR_ERR_NO_DOMAIN:
            logging.debug("Error invoking atexit destroy_on_exit",
                    exc_info=True)


def set_test_stub_options(options):
    # Set some basic options that will let virt-install succeed. Helps
    # save boiler plate typing when testing new command line additions
    if not options.test_stub_command:
        return

    if not options.connect:
        options.connect = "test:///default"
    if not options.name:
        options.name = "test-stub-command"
    if not options.memory:
        options.memory = "256"
    if not options.disk:
        options.disk = "none"
    if not install_specified(options):
        options.import_install = True
    if not options.graphics:
        options.graphics = "none"
    if not options.distro_variant:
        options.distro_variant = "fedora27"


def main(conn=None):
    cli.earlyLogging()
    options = parse_args()

    # Default setup options
    convert_old_printxml(options)
    options.quiet = (options.xmlonly or
        options.test_media_detection or options.quiet)
    cli.setupLogging("virt-install", options.debug, options.quiet)

    if cli.check_option_introspection(options):
        return 0

    check_cdrom_option_error(options)
    cli.convert_old_force(options)
    cli.parse_check(options.check)
    cli.set_prompt(options.prompt)
    convert_old_memory(options)
    convert_old_sound(options)
    convert_old_networks(options)
    convert_old_graphics(options)
    convert_old_disks(options)
    convert_old_features(options)
    convert_old_cpuset(options)
    convert_old_init(options)
    set_test_stub_options(options)
    convert_old_os_options(options)

    if conn is None:
        conn = cli.getConnection(options.connect)

    if options.test_media_detection:
        do_test_media_detection(conn, options)
        return 0

    guest, installer = build_guest_instance(conn, options)
    if options.xmlonly or options.dry:
        xml = xml_to_print(guest, installer, options.xmlonly, options.dry)
        if xml:
            print_stdout(xml, do_force=True)
    else:
        start_install(guest, installer, options)

    return 0

if __name__ == "__main__":
    try:
        sys.exit(main())
    except SystemExit as sys_e:
        sys.exit(sys_e.code)
    except KeyboardInterrupt:
        logging.debug("", exc_info=True)
        print_stderr(_("Installation aborted at user request"))
    except Exception as main_e:
        fail(main_e)
