#!/usr/bin/python -tt # # Copyright 2005-2013 Red Hat, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301 USA. import argparse import logging import re import sys import time import libvirt import urlgrabber.progress as progress import virtinst import virtinst.cli as 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|..." install_missing = (_("An install method must be specified\n(%(methods)s)") % {"methods" : install_methods}) disk_missing = _("--disk storage must be specified (override with --nodisks)") def install_specified(location, cdpath, pxe, import_install): return bool(pxe or cdpath or location or import_install) def cdrom_specified(guest, disk=None): disks = guest.get_devices("disk") for disk in disks: if disk.device == virtinst.VirtualDisk.DEVICE_CDROM: return True # Probably haven't set up disks yet if not disks and disk: for opts in disk: if opts.count("device=cdrom"): return True return False def storage_specified(disks, nodisks, filesystems): return bool(disks or nodisks or filesystems) def supports_pxe(guest): """ Return False if we are pretty sure the config doesn't support PXE """ for nic in guest.get_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: 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") ############################## # Device validation wrappers # ############################## 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 += ",init=%s" % options.init logging.debug("Converted old --init to --boot %s", options.boot) def convert_old_disks(options): if options.disk or options.nodisks: return 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 in range(len(disklist)): optstr = "" if disklist[idx]: optstr += "path=%s" % disklist[idx] 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 options.file_paths = [] options.disksize = None options.sparse = True ######################## # Virt type validation # ######################## def get_guest(conn, options): # Set up all virt/hypervisor parameters if sum([bool(f) for f in [options.fullvirt, options.paravirt, options.container]]) > 1: fail(_("Can't do more than one of --hvm, --paravirt, or --container")) req_accel = True req_hv_type = options.hv_type and options.hv_type.lower() or None if options.fullvirt: req_virt_type = "hvm" elif options.paravirt: req_virt_type = "xen" elif options.container: req_virt_type = "exe" else: # This should force capabilities to give us the most sensible default req_virt_type = None logging.debug("Requesting virt method '%s', hv type '%s'.", (req_virt_type and req_virt_type or _("default")), (req_hv_type and req_hv_type or _("default"))) arch = options.arch if re.match("i.86", arch or ""): arch = "i686" try: (capsguest, capsdomain) = conn.caps.guest_lookup( os_type=req_virt_type, arch=arch, typ=req_hv_type, accelerated=req_accel, machine=options.machine) guest = conn.caps.build_virtinst_guest(conn, capsguest, capsdomain) guest.os.machine = options.machine except Exception, e: fail(e) if (not req_virt_type and not req_hv_type and req_accel and conn.is_qemu() and capsguest.arch in ["i686", "x86_64"] and not capsdomain.is_accelerated()): logging.warn("KVM acceleration not available, using '%s'", capsdomain.hypervisor_type) return guest ################################## # Install media setup/validation # ################################## def get_install_media(guest, location, cdpath): manual_cdrom = cdrom_specified(guest) cdinstall = bool(not location and (cdpath or cdrom_specified(guest))) if not (location or cdpath or manual_cdrom): return try: validate_install_media(guest, location, cdpath, cdinstall) except ValueError, e: fail(_("Error validating install location: %s" % str(e))) def validate_install_media(guest, location, cdpath, cdinstall=False): if cdinstall or cdpath: guest.installer.cdrom = True if location or cdpath: guest.installer.location = (location or cdpath) guest.installer.check_location(guest) ############################# # General option validation # ############################# def validate_required_options(options, guest): # 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 options.name: msg += "\n" + cli.name_missing if not options.memory: msg += "\n" + _("--memory amount in MB is required") if (not guest.os.is_container() and not storage_specified(options.disk, options.nodisks, options.filesystem)): msg += "\n" + disk_missing if (not guest.os.is_container() and (not install_specified(options.location, options.cdrom, options.pxe, options.import_install)) and (not cdrom_specified(guest, options.disk))): msg += "\n" + install_missing if msg: fail(msg) def check_option_collisions(options, guest): # Disk collisions 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]")) # Network collisions 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 --network with --nonetworks")) return # Install collisions if sum([bool(l) for l in [options.pxe, options.location, options.cdrom, options.import_install]]) > 1: fail(_("Only one install method can be used (%(methods)s)") % {"methods" : install_methods}) if (guest.os.is_container() and install_specified(options.location, options.cdrom, options.pxe, options.import_install)): 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 guest.installer.cdrom: cdrom_err = " " + _("See the man page for examples of " "using --location with CDROM media") 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): if guest.get_devices("graphics"): return if not options.autoconsole: return if guest.installer.cdrom: logging.warn(_("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.")) return if not options.location: return # Trying --location --nographics with console connect. Warn if # they likely won't see any output. if not guest.get_devices("console"): logging.warn(_("No --console device added, you likely will not " "see text install output from the guest.")) return serial_arg = "console=ttyS0" virtio_arg = "console=hvc0" console_type = None if guest.conn.is_test() or guest.conn.is_qemu(): console_type = serial_arg if guest.get_devices("console")[0].target_type == "virtio": console_type = virtio_arg if not options.extra_args or "console=" not in options.extra_args: logging.warn(_("No 'console' seen in --extra-args, a '%s' kernel " "argument is likely required to see text install output from " "the guest."), console_type or "console=") return if console_type in options.extra_args: return if (serial_arg not in options.extra_args and virtio_arg not in options.extra_args): return has = (serial_arg in options.extra_args) and serial_arg or virtio_arg need = (serial_arg in options.extra_args) and virtio_arg or serial_arg logging.warn(_("'%s' found in --extra-args, but the device attached " "to the guest likely requires '%s'. You may not see text install " "output from the guest."), has, need) if has == serial_arg: logging.warn(_("To make '--extra-args %s' work, you can force a " "plain serial device with '--console pty'"), serial_arg) def show_warnings(options, guest): if options.pxe and not supports_pxe(guest): logging.warn(_("The guest's network configuration does not support " "PXE")) _show_nographics_warnings(options, guest) ########################## # Guest building helpers # ########################## def build_installer(options, conn, virt_type): # Build the Installer instance if options.livecd: instclass = virtinst.LiveCDInstaller elif options.pxe: if options.nonetworks: fail(_("Can't use --pxe with --nonetworks")) instclass = virtinst.PXEInstaller elif options.cdrom or options.location: instclass = virtinst.DistroInstaller elif virt_type == "exe": instclass = virtinst.ContainerInstaller elif options.import_install or options.boot: if options.import_install and options.nodisks: fail(_("A disk device must be specified with --import.")) options.import_install = True instclass = virtinst.ImportInstaller else: instclass = virtinst.DistroInstaller # Only set installer params here that impact the hw config, not # anything to do with install media installer = instclass(conn) return installer def build_guest_instance(conn, options, parsermap): guest = get_guest(conn, options) logging.debug("Received virt method '%s'", guest.type) logging.debug("Hypervisor name is '%s'", guest.os.os_type) guest.installer = build_installer(options, conn, guest.os.os_type) cli.convert_old_memory(options) convert_old_sound(options) cli.convert_old_networks(options, not options.nonetworks and 1 or 0) cli.convert_old_graphics(guest, options) convert_old_disks(options) cli.convert_old_features(options) cli.convert_old_cpuset(options) convert_old_init(options) # non-xml install options guest.installer.extraargs = options.extra_args guest.installer.initrd_injections = options.initrd_inject cli.set_os_variant(guest, options.distro_type, options.distro_variant) guest.autostart = options.autostart if options.name: guest.name = options.name if options.uuid: guest.uuid = options.uuid if options.description: guest.description = options.description validate_required_options(options, guest) # We don't want to auto-parse --disk, but we wanted it for earlier # parameter introspection cli.parse_option_strings(parsermap, options, guest, None) # Extra disk validation for disk in guest.get_devices("disk"): cli.validate_disk(disk) guest.add_default_devices() get_install_media(guest, options.location, options.cdrom) # Various little validations about option collisions. Need to do # this after setting guest.installer at least check_option_collisions(options, guest) show_warnings(options, guest) return guest ########################### # Install process helpers # ########################### def domain_is_crashed(domain): """ Return True if the created domain object is in a crashed state """ if not domain: return False dominfo = domain.info() state = dominfo[0] return state == libvirt.VIR_DOMAIN_CRASHED def domain_is_shutdown(domain): """ Return True if the created domain object is shutdown """ if not domain: return False dominfo = domain.info() state = dominfo[0] cpu_time = dominfo[4] if state == libvirt.VIR_DOMAIN_SHUTOFF: return True # If 'wait' was specified, the dom object we have was looked up # before initially shutting down, which seems to bogus up the # info data (all 0's). So, if it is bogus, assume the domain is # shutdown. We will catch the error later. return state == libvirt.VIR_DOMAIN_NOSTATE and cpu_time == 0 def start_install(guest, continue_inst, options): # There are two main cases we care about: # # Scripts: these should specify --wait always, maintaining the # semantics of virt-install exit implying the domain has finished # installing. # # Interactive: If this is a continue_inst domain, we default to # waiting. Otherwise, we can exit before the domain has finished # installing. Passing --wait will give the above semantics. # wait_on_install = continue_inst wait_time = -1 if options.wait is not None: wait_on_install = True wait_time = options.wait * 60 # 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 # --wait 0 implies --noautoconsole options.autoconsole = (wait_time != 0) and options.autoconsole or False conscb = options.autoconsole and cli.show_console_for_guest or None meter = (options.quiet and progress.BaseMeter() or progress.TextMeter(fo=sys.stdout)) logging.debug("Guest.has_install_phase: %s", guest.installer.has_install_phase()) # we've got everything -- try to start the install print_stdout(_("\nStarting install...")) try: start_time = time.time() # Do first install phase dom = guest.start_install(meter=meter, noboot=options.noreboot) cli.connect_console(guest, conscb, wait_on_console) dom = check_domain(guest, dom, conscb, wait_on_install, wait_time, start_time) if continue_inst: dom = guest.continue_install(meter=meter) cli.connect_console(guest, conscb, wait_on_console) dom = check_domain(guest, dom, conscb, wait_on_install, wait_time, start_time) if options.noreboot or not guest.installer.has_install_phase(): print_stdout( _("Domain creation completed. You can restart your domain by " "running:\n %s") % cli.virsh_start_cmd(guest)) else: print_stdout( _("Guest installation complete... restarting guest.")) dom.create() cli.connect_console(guest, conscb, True) except KeyboardInterrupt: logging.debug("", exc_info=True) print_stderr(_("Domain install interrupted.")) raise except RuntimeError, e: fail(e) except Exception, e: fail(e, do_exit=False) cli.install_fail(guest) def check_domain(guest, dom, conscb, wait_for_install, wait_time, start_time): """ Make sure domain ends up in expected state, and wait if for install to complete if requested """ wait_forever = (wait_time < 0) # Wait a bit so info is accurate def check_domain_state(): dominfo = dom.info() state = dominfo[0] if domain_is_crashed(guest.domain): fail(_("Domain has crashed.")) if domain_is_shutdown(guest.domain): return dom, state return None, state do_sleep = bool(conscb) try: ret, state = check_domain_state() if ret: return ret except Exception, e: # Sometimes we see errors from libvirt here due to races logging.exception(e) do_sleep = True if do_sleep: # Sleep a bit and try again to be sure the HV has caught up time.sleep(2) ret, state = check_domain_state() if ret: return ret # Domain seems to be running logging.debug("Domain state after install: %s", state) 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 guest.installer.has_install_phase(): return dom print_stdout( _("Domain installation still in progress. You can reconnect" " to \nthe console to complete the installation process.")) sys.exit(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 domain_is_shutdown(guest.domain): print_stdout(_("Domain has shutdown. Continuing.")) try: # Lookup a new domain object incase current # one returned bogus data (see comment in # domain_is_shutdown) dom = guest.conn.lookupByName(guest.name) except Exception, e: raise RuntimeError(_("Could not lookup domain after " "install: %s" % str(e))) break time_elapsed = (time.time() - start_time) if not wait_forever and time_elapsed >= wait_time: print_stdout( _("Installation has exceeded specified time limit. " "Exiting application.")) sys.exit(1) time.sleep(2) return dom ######################## # XML printing helpers # ######################## def xml_to_print(guest, continue_inst, xmlonly, xmlstep, dry): start_xml, final_xml = guest.start_install(dry=dry, return_xml=True) second_xml = None if not start_xml: start_xml = final_xml final_xml = None if continue_inst: second_xml, final_xml = guest.continue_install(dry=dry, return_xml=True) if dry and not (xmlonly or xmlstep): print_stdout(_("Dry run completed successfully")) return # --print-xml if xmlonly and not xmlstep: if second_xml or final_xml: fail(_("--print-xml can only be used with guests that do not have " "an installation phase (--import, --boot, etc.). To see all" " generated XML, please use --print-step all.")) return start_xml # --print-step if xmlstep == "1": return start_xml if xmlstep == "2": if not (second_xml or final_xml): fail(_("Requested installation does not have XML step 2")) return second_xml or final_xml if xmlstep == "3": if not second_xml: fail(_("Requested installation does not have XML step 3")) return final_xml # "all" case xml = start_xml if second_xml: xml += second_xml if final_xml: xml += final_xml return xml ####################### # CLI option handling # ####################### def parse_args(): parser = cli.setupParser( "%(prog)s --name NAME --ram RAM 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) cli.add_guest_xml_options(geng) 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=_("Installation source (eg, nfs:host:/path, " "http://host/path, ftp://host/path)")) 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", 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")) cli.add_distro_options(insg) cli.add_boot_options(insg) insg.add_argument("--init", help=argparse.SUPPRESS) stog = parser.add_argument_group(_("Storage Configuration")) cli.add_disk_option(stog) stog.add_argument("--nodisks", action="store_true", help=_("Don't set up any disks for the guest.")) cli.add_fs_option(stog) # Deprecated storage options stog.add_argument("-f", "--file", dest="file_paths", action="append", help=argparse.SUPPRESS) stog.add_argument("-s", "--file-size", type=float, action="append", dest="disksize", help=argparse.SUPPRESS) stog.add_argument("--nonsparse", action="store_false", default=True, dest="sparse", help=argparse.SUPPRESS) netg = cli.network_option_group(parser) netg.add_argument("--nonetworks", action="store_true", help=_("Don't create network interfaces for the guest.")) cli.graphics_option_group(parser) devg = parser.add_argument_group(_("Device Options")) cli.add_device_options(devg, sound_back_compat=True) virg = parser.add_argument_group(_("Virtualization Platform Options")) virg.add_argument("-v", "--hvm", action="store_true", dest="fullvirt", help=_("This guest should be a fully virtualized guest")) virg.add_argument("-p", "--paravirt", action="store_true", help=_("This guest should be a paravirtualized guest")) virg.add_argument("--container", action="store_true", default=False, help=_("This guest should be a container guest")) virg.add_argument("--virt-type", dest="hv_type", default="", help=_("Hypervisor name to use (kvm, qemu, xen, ...)")) virg.add_argument("--accelerate", action="store_true", default=False, help=argparse.SUPPRESS) virg.add_argument("--arch", help=_("The CPU architecture to simulate")) virg.add_argument("--machine", help=_("The machine type to emulate")) cli.add_old_feature_options(virg) misc = parser.add_argument_group(_("Miscellaneous Options")) misc.add_argument("--autostart", action="store_true", dest="autostart", default=False, help=_("Have domain autostart on host boot up.")) misc.add_argument("--wait", type=int, dest="wait", 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 # ################### def main(conn=None): cli.earlyLogging() options = parse_args() # Default setup options options.quiet = options.xmlstep or options.xmlonly or options.quiet cli.setupLogging("virt-install", options.debug, options.quiet) check_cdrom_option_error(options) if options.distro_variant == "list": logging.debug("OS list requested") for t in virtinst.osdict.list_os(list_types=True): for v in virtinst.osdict.list_os(typename=t.name): print "%-20s : %s" % (v.name, v.label) return 0 cli.set_force(options.force) cli.set_prompt(options.prompt) parsermap = cli.build_parser_map(options) if cli.check_option_introspection(options, parsermap): return 0 if conn is None: conn = cli.getConnection(options.connect) if options.xmlstep not in [None, "1", "2", "3", "all"]: fail(_("--print-step must be 1, 2, 3, or all")) guest = build_guest_instance(conn, options, parsermap) continue_inst = guest.get_continue_inst() if options.xmlstep or options.xmlonly or options.dry: xml = xml_to_print(guest, continue_inst, options.xmlonly, options.xmlstep, options.dry) if xml: print_stdout(xml, do_force=True) else: start_install(guest, continue_inst, options) return 0 if __name__ == "__main__": try: sys.exit(main()) except SystemExit, sys_e: sys.exit(sys_e.code) except KeyboardInterrupt: logging.debug("", exc_info=True) print_stderr(_("Installation aborted at user request")) except Exception, main_e: fail(main_e)