#!/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)