X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/bbcf7ad022e5b0e8fd247175ef9cb98f1f6ca78f..839642c2c9696b48d55e584ecd6a5c0f679f6db6:/lib/hypervisor/hv_kvm.py diff --git a/lib/hypervisor/hv_kvm.py b/lib/hypervisor/hv_kvm.py index b9f20f3..77b46b2 100644 --- a/lib/hypervisor/hv_kvm.py +++ b/lib/hypervisor/hv_kvm.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2008 Google Inc. +# Copyright (C) 2008, 2009, 2010, 2011, 2012 Google 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 @@ -23,6 +23,7 @@ """ +import errno import os import os.path import re @@ -30,24 +31,123 @@ import tempfile import time import logging import pwd -from cStringIO import StringIO +import struct +import fcntl +import shutil from ganeti import utils from ganeti import constants from ganeti import errors from ganeti import serializer from ganeti import objects +from ganeti import uidpool +from ganeti import ssconf from ganeti.hypervisor import hv_base +from ganeti import netutils +from ganeti.utils import wrapper as utils_wrapper + + +_KVM_NETWORK_SCRIPT = constants.SYSCONFDIR + "/ganeti/kvm-vif-bridge" + +# TUN/TAP driver constants, taken from +# They are architecture-independent and already hardcoded in qemu-kvm source, +# so we can safely include them here. +TUNSETIFF = 0x400454ca +TUNGETIFF = 0x800454d2 +TUNGETFEATURES = 0x800454cf +IFF_TAP = 0x0002 +IFF_NO_PI = 0x1000 +IFF_VNET_HDR = 0x4000 + + +def _ProbeTapVnetHdr(fd): + """Check whether to enable the IFF_VNET_HDR flag. + + To do this, _all_ of the following conditions must be met: + 1. TUNGETFEATURES ioctl() *must* be implemented + 2. TUNGETFEATURES ioctl() result *must* contain the IFF_VNET_HDR flag + 3. TUNGETIFF ioctl() *must* be implemented; reading the kernel code in + drivers/net/tun.c there is no way to test this until after the tap device + has been created using TUNSETIFF, and there is no way to change the + IFF_VNET_HDR flag after creating the interface, catch-22! However both + TUNGETIFF and TUNGETFEATURES were introduced in kernel version 2.6.27, + thus we can expect TUNGETIFF to be present if TUNGETFEATURES is. + + @type fd: int + @param fd: the file descriptor of /dev/net/tun + + """ + req = struct.pack("I", 0) + try: + res = fcntl.ioctl(fd, TUNGETFEATURES, req) + except EnvironmentError: + logging.warning("TUNGETFEATURES ioctl() not implemented") + return False + + tunflags = struct.unpack("I", res)[0] + if tunflags & IFF_VNET_HDR: + return True + else: + logging.warning("Host does not support IFF_VNET_HDR, not enabling") + return False + + +def _OpenTap(vnet_hdr=True): + """Open a new tap device and return its file descriptor. + + This is intended to be used by a qemu-type hypervisor together with the -net + tap,fd= command line parameter. + + @type vnet_hdr: boolean + @param vnet_hdr: Enable the VNET Header + @return: (ifname, tapfd) + @rtype: tuple + + """ + try: + tapfd = os.open("/dev/net/tun", os.O_RDWR) + except EnvironmentError: + raise errors.HypervisorError("Failed to open /dev/net/tun") + + flags = IFF_TAP | IFF_NO_PI + + if vnet_hdr and _ProbeTapVnetHdr(tapfd): + flags |= IFF_VNET_HDR + + # The struct ifreq ioctl request (see netdevice(7)) + ifr = struct.pack("16sh", "", flags) + + try: + res = fcntl.ioctl(tapfd, TUNSETIFF, ifr) + except EnvironmentError: + raise errors.HypervisorError("Failed to allocate a new TAP device") + + # Get the interface name from the ioctl + ifname = struct.unpack("16sh", res)[0].strip("\x00") + return (ifname, tapfd) class KVMHypervisor(hv_base.BaseHypervisor): """KVM hypervisor interface""" + CAN_MIGRATE = True _ROOT_DIR = constants.RUN_GANETI_DIR + "/kvm-hypervisor" _PIDS_DIR = _ROOT_DIR + "/pid" # contains live instances pids + _UIDS_DIR = _ROOT_DIR + "/uid" # contains instances reserved uids _CTRL_DIR = _ROOT_DIR + "/ctrl" # contains instances control sockets _CONF_DIR = _ROOT_DIR + "/conf" # contains instances startup data - _DIRS = [_ROOT_DIR, _PIDS_DIR, _CTRL_DIR, _CONF_DIR] + _NICS_DIR = _ROOT_DIR + "/nic" # contains instances nic <-> tap associations + _KEYMAP_DIR = _ROOT_DIR + "/keymap" # contains instances keymaps + # KVM instances with chroot enabled are started in empty chroot directories. + _CHROOT_DIR = _ROOT_DIR + "/chroot" # for empty chroot directories + # After an instance is stopped, its chroot directory is removed. + # If the chroot directory is not empty, it can't be removed. + # A non-empty chroot directory indicates a possible security incident. + # To support forensics, the non-empty chroot directory is quarantined in + # a separate directory, called 'chroot-quarantine'. + _CHROOT_QUARANTINE_DIR = _ROOT_DIR + "/chroot-quarantine" + _DIRS = [_ROOT_DIR, _PIDS_DIR, _UIDS_DIR, _CTRL_DIR, _CONF_DIR, _NICS_DIR, + _CHROOT_DIR, _CHROOT_QUARANTINE_DIR, _KEYMAP_DIR] PARAMETERS = { constants.HV_KERNEL_PATH: hv_base.OPT_FILE_CHECK, @@ -57,37 +157,59 @@ class KVMHypervisor(hv_base.BaseHypervisor): constants.HV_ACPI: hv_base.NO_CHECK, constants.HV_SERIAL_CONSOLE: hv_base.NO_CHECK, constants.HV_VNC_BIND_ADDRESS: - (False, lambda x: (utils.IsValidIP(x) or utils.IsNormAbsPath(x)), + (False, lambda x: (netutils.IP4Address.IsValid(x) or + utils.IsNormAbsPath(x)), "the VNC bind address must be either a valid IP address or an absolute" " pathname", None, None), constants.HV_VNC_TLS: hv_base.NO_CHECK, constants.HV_VNC_X509: hv_base.OPT_DIR_CHECK, constants.HV_VNC_X509_VERIFY: hv_base.NO_CHECK, constants.HV_VNC_PASSWORD_FILE: hv_base.OPT_FILE_CHECK, + constants.HV_KVM_SPICE_BIND: hv_base.NO_CHECK, # will be checked later + constants.HV_KVM_SPICE_IP_VERSION: + (False, lambda x: (x == constants.IFACE_NO_IP_VERSION_SPECIFIED or + x in constants.VALID_IP_VERSIONS), + "the SPICE IP version should be 4 or 6", + None, None), + constants.HV_KVM_FLOPPY_IMAGE_PATH: hv_base.OPT_FILE_CHECK, constants.HV_CDROM_IMAGE_PATH: hv_base.OPT_FILE_CHECK, + constants.HV_KVM_CDROM2_IMAGE_PATH: hv_base.OPT_FILE_CHECK, constants.HV_BOOT_ORDER: hv_base.ParamInSet(True, constants.HT_KVM_VALID_BO_TYPES), constants.HV_NIC_TYPE: hv_base.ParamInSet(True, constants.HT_KVM_VALID_NIC_TYPES), constants.HV_DISK_TYPE: hv_base.ParamInSet(True, constants.HT_KVM_VALID_DISK_TYPES), + constants.HV_KVM_CDROM_DISK_TYPE: + hv_base.ParamInSet(False, constants.HT_KVM_VALID_DISK_TYPES), constants.HV_USB_MOUSE: hv_base.ParamInSet(False, constants.HT_KVM_VALID_MOUSE_TYPES), - constants.HV_MIGRATION_PORT: hv_base.NET_PORT_CHECK, + constants.HV_KEYMAP: hv_base.NO_CHECK, + constants.HV_MIGRATION_PORT: hv_base.REQ_NET_PORT_CHECK, + constants.HV_MIGRATION_BANDWIDTH: hv_base.NO_CHECK, + constants.HV_MIGRATION_DOWNTIME: hv_base.NO_CHECK, + constants.HV_MIGRATION_MODE: hv_base.MIGRATION_MODE_CHECK, constants.HV_USE_LOCALTIME: hv_base.NO_CHECK, constants.HV_DISK_CACHE: hv_base.ParamInSet(True, constants.HT_VALID_CACHE_TYPES), constants.HV_SECURITY_MODEL: hv_base.ParamInSet(True, constants.HT_KVM_VALID_SM_TYPES), constants.HV_SECURITY_DOMAIN: hv_base.NO_CHECK, + constants.HV_KVM_FLAG: + hv_base.ParamInSet(False, constants.HT_KVM_FLAG_VALUES), + constants.HV_VHOST_NET: hv_base.NO_CHECK, + constants.HV_KVM_USE_CHROOT: hv_base.NO_CHECK, + constants.HV_MEM_PATH: hv_base.OPT_DIR_CHECK, + constants.HV_REBOOT_BEHAVIOR: + hv_base.ParamInSet(True, constants.REBOOT_BEHAVIORS) } - _MIGRATION_STATUS_RE = re.compile('Migration\s+status:\s+(\w+)', + _MIGRATION_STATUS_RE = re.compile("Migration\s+status:\s+(\w+)", re.M | re.I) _MIGRATION_INFO_MAX_BAD_ANSWERS = 5 _MIGRATION_INFO_RETRY_DELAY = 2 - _KVM_NETWORK_SCRIPT = constants.SYSCONFDIR + "/ganeti/kvm-vif-bridge" + _VERSION_RE = re.compile(r"\b(\d+)\.(\d+)(\.(\d+))?\b") ANCILLARY_FILES = [ _KVM_NETWORK_SCRIPT, @@ -108,6 +230,13 @@ class KVMHypervisor(hv_base.BaseHypervisor): return utils.PathJoin(cls._PIDS_DIR, instance_name) @classmethod + def _InstanceUidFile(cls, instance_name): + """Returns the instance uidfile. + + """ + return utils.PathJoin(cls._UIDS_DIR, instance_name) + + @classmethod def _InstancePidInfo(cls, pid): """Check pid file for instance information. @@ -136,9 +265,9 @@ class KVMHypervisor(hv_base.BaseHypervisor): memory = 0 vcpus = 0 - arg_list = cmdline.split('\x00') + arg_list = cmdline.split("\x00") while arg_list: - arg = arg_list.pop(0) + arg = arg_list.pop(0) if arg == "-name": instance = arg_list.pop(0) elif arg == "-m": @@ -216,19 +345,91 @@ class KVMHypervisor(hv_base.BaseHypervisor): return utils.PathJoin(cls._CONF_DIR, "%s.runtime" % instance_name) @classmethod + def _InstanceChrootDir(cls, instance_name): + """Returns the name of the KVM chroot dir of the instance + + """ + return utils.PathJoin(cls._CHROOT_DIR, instance_name) + + @classmethod + def _InstanceNICDir(cls, instance_name): + """Returns the name of the directory holding the tap device files for a + given instance. + + """ + return utils.PathJoin(cls._NICS_DIR, instance_name) + + @classmethod + def _InstanceNICFile(cls, instance_name, seq): + """Returns the name of the file containing the tap device for a given NIC + + """ + return utils.PathJoin(cls._InstanceNICDir(instance_name), str(seq)) + + @classmethod + def _InstanceKeymapFile(cls, instance_name): + """Returns the name of the file containing the keymap for a given instance + + """ + return utils.PathJoin(cls._KEYMAP_DIR, instance_name) + + @classmethod + def _TryReadUidFile(cls, uid_file): + """Try to read a uid file + + """ + if os.path.exists(uid_file): + try: + uid = int(utils.ReadOneLineFile(uid_file)) + return uid + except EnvironmentError: + logging.warning("Can't read uid file", exc_info=True) + except (TypeError, ValueError): + logging.warning("Can't parse uid file contents", exc_info=True) + return None + + @classmethod def _RemoveInstanceRuntimeFiles(cls, pidfile, instance_name): - """Removes an instance's rutime sockets/files. + """Removes an instance's rutime sockets/files/dirs. """ utils.RemoveFile(pidfile) utils.RemoveFile(cls._InstanceMonitor(instance_name)) utils.RemoveFile(cls._InstanceSerial(instance_name)) utils.RemoveFile(cls._InstanceKVMRuntime(instance_name)) + utils.RemoveFile(cls._InstanceKeymapFile(instance_name)) + uid_file = cls._InstanceUidFile(instance_name) + uid = cls._TryReadUidFile(uid_file) + utils.RemoveFile(uid_file) + if uid is not None: + uidpool.ReleaseUid(uid) + try: + shutil.rmtree(cls._InstanceNICDir(instance_name)) + except OSError, err: + if err.errno != errno.ENOENT: + raise + try: + chroot_dir = cls._InstanceChrootDir(instance_name) + utils.RemoveDir(chroot_dir) + except OSError, err: + if err.errno == errno.ENOTEMPTY: + # The chroot directory is expected to be empty, but it isn't. + new_chroot_dir = tempfile.mkdtemp(dir=cls._CHROOT_QUARANTINE_DIR, + prefix="%s-%s-" % + (instance_name, + utils.TimestampForFilename())) + logging.warning("The chroot directory of instance %s can not be" + " removed as it is not empty. Moving it to the" + " quarantine instead. Please investigate the" + " contents (%s) and clean up manually", + instance_name, new_chroot_dir) + utils.RenameFile(chroot_dir, new_chroot_dir) + else: + raise - def _WriteNetScript(self, instance, seq, nic): - """Write a script to connect a net interface to the proper bridge. - - This can be used by any qemu-type hypervisor. + @staticmethod + def _ConfigureNIC(instance, seq, nic, tap): + """Run the network configuration script for a specified NIC @param instance: instance we're acting on @type instance: instance object @@ -236,66 +437,40 @@ class KVMHypervisor(hv_base.BaseHypervisor): @type seq: int @param nic: nic we're acting on @type nic: nic object - @return: netscript file name - @rtype: string + @param tap: the host's tap interface this NIC corresponds to + @type tap: str """ - script = StringIO() - script.write("#!/bin/sh\n") - script.write("# this is autogenerated by Ganeti, please do not edit\n#\n") - script.write("PATH=$PATH:/sbin:/usr/sbin\n") - script.write("export INSTANCE=%s\n" % instance.name) - script.write("export MAC=%s\n" % nic.mac) + + if instance.tags: + tags = " ".join(instance.tags) + else: + tags = "" + + env = { + "PATH": "%s:/sbin:/usr/sbin" % os.environ["PATH"], + "INSTANCE": instance.name, + "MAC": nic.mac, + "MODE": nic.nicparams[constants.NIC_MODE], + "INTERFACE": tap, + "INTERFACE_INDEX": str(seq), + "TAGS": tags, + } + if nic.ip: - script.write("export IP=%s\n" % nic.ip) - script.write("export MODE=%s\n" % nic.nicparams[constants.NIC_MODE]) + env["IP"] = nic.ip + if nic.nicparams[constants.NIC_LINK]: - script.write("export LINK=%s\n" % nic.nicparams[constants.NIC_LINK]) - if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED: - script.write("export BRIDGE=%s\n" % nic.nicparams[constants.NIC_LINK]) - script.write("export INTERFACE=$1\n") - # TODO: make this configurable at ./configure time - script.write("if [ -x '%s' ]; then\n" % self._KVM_NETWORK_SCRIPT) - script.write(" # Execute the user-specific vif file\n") - script.write(" %s\n" % self._KVM_NETWORK_SCRIPT) - script.write("else\n") - script.write(" ifconfig $INTERFACE 0.0.0.0 up\n") + env["LINK"] = nic.nicparams[constants.NIC_LINK] + if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED: - script.write(" # Connect the interface to the bridge\n") - script.write(" brctl addif $BRIDGE $INTERFACE\n") - elif nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_ROUTED: - if not nic.ip: - raise errors.HypervisorError("nic/%d is routed, but has no ip." % seq) - script.write(" # Route traffic targeted at the IP to the interface\n") - if nic.nicparams[constants.NIC_LINK]: - script.write(" while ip rule del dev $INTERFACE; do :; done\n") - script.write(" ip rule add dev $INTERFACE table $LINK\n") - script.write(" ip route replace $IP table $LINK proto static" - " dev $INTERFACE\n") - else: - script.write(" ip route replace $IP proto static" - " dev $INTERFACE\n") - interface_v4_conf = "/proc/sys/net/ipv4/conf/$INTERFACE" - interface_v6_conf = "/proc/sys/net/ipv6/conf/$INTERFACE" - script.write(" if [ -d %s ]; then\n" % interface_v4_conf) - script.write(" echo 1 > %s/proxy_arp\n" % interface_v4_conf) - script.write(" echo 1 > %s/forwarding\n" % interface_v4_conf) - script.write(" fi\n") - script.write(" if [ -d %s ]; then\n" % interface_v6_conf) - script.write(" echo 1 > %s/proxy_ndp\n" % interface_v6_conf) - script.write(" echo 1 > %s/forwarding\n" % interface_v6_conf) - script.write(" fi\n") - script.write("fi\n\n") - # As much as we'd like to put this in our _ROOT_DIR, that will happen to be - # mounted noexec sometimes, so we'll have to find another place. - (tmpfd, tmpfile_name) = tempfile.mkstemp() - tmpfile = os.fdopen(tmpfd, 'w') - try: - tmpfile.write(script.getvalue()) - finally: - tmpfile.close() - os.chmod(tmpfile_name, 0755) - return tmpfile_name + env["BRIDGE"] = nic.nicparams[constants.NIC_LINK] + + result = utils.RunCmd([constants.KVM_IFUP, tap], env=env) + if result.failed: + raise errors.HypervisorError("Failed to configure interface %s: %s." + " Network configuration script output: %s" % + (tap, result.fail_reason, result.output)) def ListInstances(self): """Get the list of running instances. @@ -345,42 +520,67 @@ class KVMHypervisor(hv_base.BaseHypervisor): data.append(info) return data - def _GenerateKVMRuntime(self, instance, block_devices): + def _GenerateKVMRuntime(self, instance, block_devices, startup_paused): """Generate KVM information to start an instance. + @attention: this function must not have any side-effects; for + example, it must not write to the filesystem, or read values + from the current system the are expected to differ between + nodes, since it is only run once at instance startup; + actions/kvm arguments that can vary between systems should be + done in L{_ExecuteKVMRuntime} + """ - pidfile = self._InstancePidFile(instance.name) + _, v_major, v_min, _ = self._GetKVMVersion() + + pidfile = self._InstancePidFile(instance.name) kvm = constants.KVM_PATH kvm_cmd = [kvm] # used just by the vnc server, if enabled - kvm_cmd.extend(['-name', instance.name]) - kvm_cmd.extend(['-m', instance.beparams[constants.BE_MEMORY]]) - kvm_cmd.extend(['-smp', instance.beparams[constants.BE_VCPUS]]) - kvm_cmd.extend(['-pidfile', pidfile]) - kvm_cmd.extend(['-daemonize']) + kvm_cmd.extend(["-name", instance.name]) + kvm_cmd.extend(["-m", instance.beparams[constants.BE_MEMORY]]) + kvm_cmd.extend(["-smp", instance.beparams[constants.BE_VCPUS]]) + kvm_cmd.extend(["-pidfile", pidfile]) + kvm_cmd.extend(["-daemonize"]) if not instance.hvparams[constants.HV_ACPI]: - kvm_cmd.extend(['-no-acpi']) + kvm_cmd.extend(["-no-acpi"]) + if startup_paused: + kvm_cmd.extend(["-S"]) + if instance.hvparams[constants.HV_REBOOT_BEHAVIOR] == \ + constants.INSTANCE_REBOOT_EXIT: + kvm_cmd.extend(["-no-reboot"]) hvp = instance.hvparams boot_disk = hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_DISK boot_cdrom = hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_CDROM + boot_floppy = hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_FLOPPY boot_network = hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_NETWORK - security_model = hvp[constants.HV_SECURITY_MODEL] - if security_model == constants.HT_SM_USER: - kvm_cmd.extend(['-runas', hvp[constants.HV_SECURITY_DOMAIN]]) + self.ValidateParameters(hvp) + + if hvp[constants.HV_KVM_FLAG] == constants.HT_KVM_ENABLED: + kvm_cmd.extend(["-enable-kvm"]) + elif hvp[constants.HV_KVM_FLAG] == constants.HT_KVM_DISABLED: + kvm_cmd.extend(["-disable-kvm"]) if boot_network: - kvm_cmd.extend(['-boot', 'n']) + kvm_cmd.extend(["-boot", "n"]) disk_type = hvp[constants.HV_DISK_TYPE] if disk_type == constants.HT_DISK_PARAVIRTUAL: - if_val = ',if=virtio' + if_val = ",if=virtio" else: - if_val = ',if=%s' % disk_type + if_val = ",if=%s" % disk_type # Cache mode disk_cache = hvp[constants.HV_DISK_CACHE] - if disk_cache != constants.HT_CACHE_DEFAULT: + if instance.disk_template in constants.DTS_EXT_MIRROR: + if disk_cache != "none": + # TODO: make this a hard error, instead of a silent overwrite + logging.warning("KVM: overriding disk_cache setting '%s' with 'none'" + " to prevent shared storage corruption on migration", + disk_cache) + cache_val = ",cache=none" + elif disk_cache != constants.HT_CACHE_DEFAULT: cache_val = ",cache=%s" % disk_cache else: cache_val = "" @@ -389,96 +589,189 @@ class KVMHypervisor(hv_base.BaseHypervisor): raise errors.HypervisorError("Instance has read-only disks which" " are not supported by KVM") # TODO: handle FD_LOOP and FD_BLKTAP (?) + boot_val = "" if boot_disk: - kvm_cmd.extend(['-boot', 'c']) - boot_val = ',boot=on' - # We only boot from the first disk + kvm_cmd.extend(["-boot", "c"]) boot_disk = False - else: - boot_val = '' + if (v_major, v_min) < (0, 14) and disk_type != constants.HT_DISK_IDE: + boot_val = ",boot=on" - drive_val = 'file=%s,format=raw%s%s%s' % (dev_path, if_val, boot_val, + drive_val = "file=%s,format=raw%s%s%s" % (dev_path, if_val, boot_val, cache_val) - kvm_cmd.extend(['-drive', drive_val]) + kvm_cmd.extend(["-drive", drive_val]) + + #Now we can specify a different device type for CDROM devices. + cdrom_disk_type = hvp[constants.HV_KVM_CDROM_DISK_TYPE] + if not cdrom_disk_type: + cdrom_disk_type = disk_type iso_image = hvp[constants.HV_CDROM_IMAGE_PATH] if iso_image: - options = ',format=raw,media=cdrom' + options = ",format=raw,media=cdrom" if boot_cdrom: - kvm_cmd.extend(['-boot', 'd']) - options = '%s,boot=on' % options + kvm_cmd.extend(["-boot", "d"]) + if cdrom_disk_type != constants.HT_DISK_IDE: + options = "%s,boot=on,if=%s" % (options, constants.HT_DISK_IDE) + else: + options = "%s,boot=on" % options else: - options = '%s,if=virtio' % options - drive_val = 'file=%s%s' % (iso_image, options) - kvm_cmd.extend(['-drive', drive_val]) + if cdrom_disk_type == constants.HT_DISK_PARAVIRTUAL: + if_val = ",if=virtio" + else: + if_val = ",if=%s" % cdrom_disk_type + options = "%s%s" % (options, if_val) + drive_val = "file=%s%s" % (iso_image, options) + kvm_cmd.extend(["-drive", drive_val]) + + iso_image2 = hvp[constants.HV_KVM_CDROM2_IMAGE_PATH] + if iso_image2: + options = ",format=raw,media=cdrom" + if cdrom_disk_type == constants.HT_DISK_PARAVIRTUAL: + if_val = ",if=virtio" + else: + if_val = ",if=%s" % cdrom_disk_type + options = "%s%s" % (options, if_val) + drive_val = "file=%s%s" % (iso_image2, options) + kvm_cmd.extend(["-drive", drive_val]) + + floppy_image = hvp[constants.HV_KVM_FLOPPY_IMAGE_PATH] + if floppy_image: + options = ",format=raw,media=disk" + if boot_floppy: + kvm_cmd.extend(["-boot", "a"]) + options = "%s,boot=on" % options + if_val = ",if=floppy" + options = "%s%s" % (options, if_val) + drive_val = "file=%s%s" % (floppy_image, options) + kvm_cmd.extend(["-drive", drive_val]) kernel_path = hvp[constants.HV_KERNEL_PATH] if kernel_path: - kvm_cmd.extend(['-kernel', kernel_path]) + kvm_cmd.extend(["-kernel", kernel_path]) initrd_path = hvp[constants.HV_INITRD_PATH] if initrd_path: - kvm_cmd.extend(['-initrd', initrd_path]) - root_append = ['root=%s' % hvp[constants.HV_ROOT_PATH], + kvm_cmd.extend(["-initrd", initrd_path]) + root_append = ["root=%s" % hvp[constants.HV_ROOT_PATH], hvp[constants.HV_KERNEL_ARGS]] if hvp[constants.HV_SERIAL_CONSOLE]: - root_append.append('console=ttyS0,38400') - kvm_cmd.extend(['-append', ' '.join(root_append)]) + root_append.append("console=ttyS0,38400") + kvm_cmd.extend(["-append", " ".join(root_append)]) + + mem_path = hvp[constants.HV_MEM_PATH] + if mem_path: + kvm_cmd.extend(["-mem-path", mem_path, "-mem-prealloc"]) + + monitor_dev = ("unix:%s,server,nowait" % + self._InstanceMonitor(instance.name)) + kvm_cmd.extend(["-monitor", monitor_dev]) + if hvp[constants.HV_SERIAL_CONSOLE]: + serial_dev = ("unix:%s,server,nowait" % + self._InstanceSerial(instance.name)) + kvm_cmd.extend(["-serial", serial_dev]) + else: + kvm_cmd.extend(["-serial", "none"]) mouse_type = hvp[constants.HV_USB_MOUSE] + vnc_bind_address = hvp[constants.HV_VNC_BIND_ADDRESS] + spice_bind = hvp[constants.HV_KVM_SPICE_BIND] + spice_ip_version = None + if mouse_type: - kvm_cmd.extend(['-usb']) - kvm_cmd.extend(['-usbdevice', mouse_type]) + kvm_cmd.extend(["-usb"]) + kvm_cmd.extend(["-usbdevice", mouse_type]) + elif vnc_bind_address: + kvm_cmd.extend(["-usbdevice", constants.HT_MOUSE_TABLET]) - vnc_bind_address = hvp[constants.HV_VNC_BIND_ADDRESS] if vnc_bind_address: - if utils.IsValidIP(vnc_bind_address): + if netutils.IP4Address.IsValid(vnc_bind_address): if instance.network_port > constants.VNC_BASE_PORT: display = instance.network_port - constants.VNC_BASE_PORT - if vnc_bind_address == '0.0.0.0': - vnc_arg = ':%d' % (display) + if vnc_bind_address == constants.IP4_ADDRESS_ANY: + vnc_arg = ":%d" % (display) else: - vnc_arg = '%s:%d' % (vnc_bind_address, display) + vnc_arg = "%s:%d" % (vnc_bind_address, display) else: logging.error("Network port is not a valid VNC display (%d < %d)." " Not starting VNC", instance.network_port, constants.VNC_BASE_PORT) - vnc_arg = 'none' + vnc_arg = "none" # Only allow tls and other option when not binding to a file, for now. # kvm/qemu gets confused otherwise about the filename to use. - vnc_append = '' + vnc_append = "" if hvp[constants.HV_VNC_TLS]: - vnc_append = '%s,tls' % vnc_append + vnc_append = "%s,tls" % vnc_append if hvp[constants.HV_VNC_X509_VERIFY]: - vnc_append = '%s,x509verify=%s' % (vnc_append, + vnc_append = "%s,x509verify=%s" % (vnc_append, hvp[constants.HV_VNC_X509]) elif hvp[constants.HV_VNC_X509]: - vnc_append = '%s,x509=%s' % (vnc_append, + vnc_append = "%s,x509=%s" % (vnc_append, hvp[constants.HV_VNC_X509]) if hvp[constants.HV_VNC_PASSWORD_FILE]: - vnc_append = '%s,password' % vnc_append + vnc_append = "%s,password" % vnc_append - vnc_arg = '%s%s' % (vnc_arg, vnc_append) + vnc_arg = "%s%s" % (vnc_arg, vnc_append) else: - vnc_arg = 'unix:%s/%s.vnc' % (vnc_bind_address, instance.name) + vnc_arg = "unix:%s/%s.vnc" % (vnc_bind_address, instance.name) + + kvm_cmd.extend(["-vnc", vnc_arg]) + elif spice_bind: + # FIXME: this is wrong here; the iface ip address differs + # between systems, so it should be done in _ExecuteKVMRuntime + if netutils.IsValidInterface(spice_bind): + # The user specified a network interface, we have to figure out the IP + # address. + addresses = netutils.GetInterfaceIpAddresses(spice_bind) + spice_ip_version = hvp[constants.HV_KVM_SPICE_IP_VERSION] + + # if the user specified an IP version and the interface does not + # have that kind of IP addresses, throw an exception + if spice_ip_version != constants.IFACE_NO_IP_VERSION_SPECIFIED: + if not addresses[spice_ip_version]: + raise errors.HypervisorError("spice: unable to get an IPv%s address" + " for %s" % (spice_ip_version, + spice_bind)) + + # the user did not specify an IP version, we have to figure it out + elif (addresses[constants.IP4_VERSION] and + addresses[constants.IP6_VERSION]): + # we have both ipv4 and ipv6, let's use the cluster default IP + # version + cluster_family = ssconf.SimpleStore().GetPrimaryIPFamily() + spice_ip_version = netutils.IPAddress.GetVersionFromAddressFamily( + cluster_family) + elif addresses[constants.IP4_VERSION]: + spice_ip_version = constants.IP4_VERSION + elif addresses[constants.IP6_VERSION]: + spice_ip_version = constants.IP6_VERSION + else: + raise errors.HypervisorError("spice: unable to get an IP address" + " for %s" % (spice_bind)) - kvm_cmd.extend(['-vnc', vnc_arg]) - else: - kvm_cmd.extend(['-nographic']) + spice_address = addresses[spice_ip_version][0] + + else: + # spice_bind is known to be a valid IP address, because + # ValidateParameters checked it. + spice_address = spice_bind + + spice_arg = "addr=%s,port=%s,disable-ticketing" % (spice_address, + instance.network_port) + if spice_ip_version: + spice_arg = "%s,ipv%s" % (spice_arg, spice_ip_version) + + logging.info("KVM: SPICE will listen on port %s", instance.network_port) + kvm_cmd.extend(["-spice", spice_arg]) - monitor_dev = ("unix:%s,server,nowait" % - self._InstanceMonitor(instance.name)) - kvm_cmd.extend(['-monitor', monitor_dev]) - if hvp[constants.HV_SERIAL_CONSOLE]: - serial_dev = ('unix:%s,server,nowait' % - self._InstanceSerial(instance.name)) - kvm_cmd.extend(['-serial', serial_dev]) else: - kvm_cmd.extend(['-serial', 'none']) + kvm_cmd.extend(["-nographic"]) if hvp[constants.HV_USE_LOCALTIME]: - kvm_cmd.extend(['-localtime']) + kvm_cmd.extend(["-localtime"]) + + if hvp[constants.HV_KVM_USE_CHROOT]: + kvm_cmd.extend(["-chroot", self._InstanceChrootDir(instance.name)]) # Save the current instance nics, but defer their expansion as parameters, # as we'll need to generate executable temp files for them. @@ -527,71 +820,180 @@ class KVMHypervisor(hv_base.BaseHypervisor): kvm_nics = [objects.NIC.FromDict(snic) for snic in serialized_nics] return (kvm_cmd, kvm_nics, hvparams) + def _RunKVMCmd(self, name, kvm_cmd, tap_fds=None): + """Run the KVM cmd and check for errors + + @type name: string + @param name: instance name + @type kvm_cmd: list of strings + @param kvm_cmd: runcmd input for kvm + @type tap_fds: list of int + @param tap_fds: fds of tap devices opened by Ganeti + + """ + try: + result = utils.RunCmd(kvm_cmd, noclose_fds=tap_fds) + finally: + for fd in tap_fds: + utils_wrapper.CloseFdNoError(fd) + + if result.failed: + raise errors.HypervisorError("Failed to start instance %s: %s (%s)" % + (name, result.fail_reason, result.output)) + if not self._InstancePidAlive(name)[2]: + raise errors.HypervisorError("Failed to start instance %s" % name) + def _ExecuteKVMRuntime(self, instance, kvm_runtime, incoming=None): - """Execute a KVM cmd, after completing it with some last minute data + """Execute a KVM cmd, after completing it with some last minute data. @type incoming: tuple of strings @param incoming: (target_host_ip, port) """ - hvp = instance.hvparams + # Small _ExecuteKVMRuntime hv parameters programming howto: + # - conf_hvp contains the parameters as configured on ganeti. they might + # have changed since the instance started; only use them if the change + # won't affect the inside of the instance (which hasn't been rebooted). + # - up_hvp contains the parameters as they were when the instance was + # started, plus any new parameter which has been added between ganeti + # versions: it is paramount that those default to a value which won't + # affect the inside of the instance as well. + conf_hvp = instance.hvparams name = instance.name self._CheckDown(name) temp_files = [] - kvm_cmd, kvm_nics, hvparams = kvm_runtime + kvm_cmd, kvm_nics, up_hvp = kvm_runtime + up_hvp = objects.FillDict(conf_hvp, up_hvp) + + _, v_major, v_min, _ = self._GetKVMVersion() + # We know it's safe to run as a different user upon migration, so we'll use + # the latest conf, from conf_hvp. + security_model = conf_hvp[constants.HV_SECURITY_MODEL] + if security_model == constants.HT_SM_USER: + kvm_cmd.extend(["-runas", conf_hvp[constants.HV_SECURITY_DOMAIN]]) + + keymap = conf_hvp[constants.HV_KEYMAP] + if keymap: + keymap_path = self._InstanceKeymapFile(name) + # If a keymap file is specified, KVM won't use its internal defaults. By + # first including the "en-us" layout, an error on loading the actual + # layout (e.g. because it can't be found) won't lead to a non-functional + # keyboard. A keyboard with incorrect keys is still better than none. + utils.WriteFile(keymap_path, data="include en-us\ninclude %s\n" % keymap) + kvm_cmd.extend(["-k", keymap_path]) + + # We have reasons to believe changing something like the nic driver/type + # upon migration won't exactly fly with the instance kernel, so for nic + # related parameters we'll use up_hvp + tapfds = [] + taps = [] if not kvm_nics: - kvm_cmd.extend(['-net', 'none']) + kvm_cmd.extend(["-net", "none"]) else: - nic_type = hvparams[constants.HV_NIC_TYPE] + vnet_hdr = False + tap_extra = "" + nic_type = up_hvp[constants.HV_NIC_TYPE] if nic_type == constants.HT_NIC_PARAVIRTUAL: - nic_model = "model=virtio" + # From version 0.12.0, kvm uses a new sintax for network configuration. + if (v_major, v_min) >= (0, 12): + nic_model = "virtio-net-pci" + vnet_hdr = True + else: + nic_model = "virtio" + + if up_hvp[constants.HV_VHOST_NET]: + # vhost_net is only available from version 0.13.0 or newer + if (v_major, v_min) >= (0, 13): + tap_extra = ",vhost=on" + else: + raise errors.HypervisorError("vhost_net is configured" + " but it is not available") else: - nic_model = "model=%s" % nic_type + nic_model = nic_type for nic_seq, nic in enumerate(kvm_nics): - nic_val = "nic,vlan=%s,macaddr=%s,%s" % (nic_seq, nic.mac, nic_model) - script = self._WriteNetScript(instance, nic_seq, nic) - kvm_cmd.extend(['-net', nic_val]) - kvm_cmd.extend(['-net', 'tap,vlan=%s,script=%s' % (nic_seq, script)]) - temp_files.append(script) + tapname, tapfd = _OpenTap(vnet_hdr) + tapfds.append(tapfd) + taps.append(tapname) + if (v_major, v_min) >= (0, 12): + nic_val = "%s,mac=%s,netdev=netdev%s" % (nic_model, nic.mac, nic_seq) + tap_val = "type=tap,id=netdev%s,fd=%d%s" % (nic_seq, tapfd, tap_extra) + kvm_cmd.extend(["-netdev", tap_val, "-device", nic_val]) + else: + nic_val = "nic,vlan=%s,macaddr=%s,model=%s" % (nic_seq, + nic.mac, nic_model) + tap_val = "tap,vlan=%s,fd=%d" % (nic_seq, tapfd) + kvm_cmd.extend(["-net", tap_val, "-net", nic_val]) if incoming: target, port = incoming - kvm_cmd.extend(['-incoming', 'tcp:%s:%s' % (target, port)]) + kvm_cmd.extend(["-incoming", "tcp:%s:%s" % (target, port)]) - vnc_pwd_file = hvp[constants.HV_VNC_PASSWORD_FILE] + # Changing the vnc password doesn't bother the guest that much. At most it + # will surprise people who connect to it. Whether positively or negatively + # it's debatable. + vnc_pwd_file = conf_hvp[constants.HV_VNC_PASSWORD_FILE] vnc_pwd = None if vnc_pwd_file: try: - vnc_pwd = utils.ReadFile(vnc_pwd_file) + vnc_pwd = utils.ReadOneLineFile(vnc_pwd_file, strict=True) except EnvironmentError, err: raise errors.HypervisorError("Failed to open VNC password file %s: %s" % (vnc_pwd_file, err)) - result = utils.RunCmd(kvm_cmd) - if result.failed: - raise errors.HypervisorError("Failed to start instance %s: %s (%s)" % - (name, result.fail_reason, result.output)) + if conf_hvp[constants.HV_KVM_USE_CHROOT]: + utils.EnsureDirs([(self._InstanceChrootDir(name), + constants.SECURE_DIR_MODE)]) - if not self._InstancePidAlive(name)[2]: - raise errors.HypervisorError("Failed to start instance %s" % name) + # Configure the network now for starting instances and bridged interfaces, + # during FinalizeMigration for incoming instances' routed interfaces + for nic_seq, nic in enumerate(kvm_nics): + if (incoming and + nic.nicparams[constants.NIC_MODE] != constants.NIC_MODE_BRIDGED): + continue + self._ConfigureNIC(instance, nic_seq, nic, taps[nic_seq]) + + if security_model == constants.HT_SM_POOL: + ss = ssconf.SimpleStore() + uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\n") + all_uids = set(uidpool.ExpandUidPool(uid_pool)) + uid = uidpool.RequestUnusedUid(all_uids) + try: + username = pwd.getpwuid(uid.GetUid()).pw_name + kvm_cmd.extend(["-runas", username]) + self._RunKVMCmd(name, kvm_cmd, tapfds) + except: + uidpool.ReleaseUid(uid) + raise + else: + uid.Unlock() + utils.WriteFile(self._InstanceUidFile(name), data=uid.AsStr()) + else: + self._RunKVMCmd(name, kvm_cmd, tapfds) + + utils.EnsureDirs([(self._InstanceNICDir(instance.name), + constants.RUN_DIRS_MODE)]) + for nic_seq, tap in enumerate(taps): + utils.WriteFile(self._InstanceNICFile(instance.name, nic_seq), + data=tap) if vnc_pwd: - change_cmd = 'change vnc password %s' % vnc_pwd + change_cmd = "change vnc password %s" % vnc_pwd self._CallMonitorCommand(instance.name, change_cmd) for filename in temp_files: utils.RemoveFile(filename) - def StartInstance(self, instance, block_devices): + def StartInstance(self, instance, block_devices, startup_paused): """Start an instance. """ self._CheckDown(instance.name) - kvm_runtime = self._GenerateKVMRuntime(instance, block_devices) + kvm_runtime = self._GenerateKVMRuntime(instance, block_devices, + startup_paused) self._SaveKVMRuntime(instance, kvm_runtime) self._ExecuteKVMRuntime(instance, kvm_runtime) @@ -613,6 +1015,42 @@ class KVMHypervisor(hv_base.BaseHypervisor): return result + @classmethod + def _ParseKVMVersion(cls, text): + """Parse the KVM version from the --help output. + + @type text: string + @param text: output of kvm --help + @return: (version, v_maj, v_min, v_rev) + @raise L{errors.HypervisorError}: when the KVM version cannot be retrieved + + """ + match = cls._VERSION_RE.search(text.splitlines()[0]) + if not match: + raise errors.HypervisorError("Unable to get KVM version") + + v_all = match.group(0) + v_maj = int(match.group(1)) + v_min = int(match.group(2)) + if match.group(4): + v_rev = int(match.group(4)) + else: + v_rev = 0 + return (v_all, v_maj, v_min, v_rev) + + @classmethod + def _GetKVMVersion(cls): + """Return the installed KVM version. + + @return: (version, v_maj, v_min, v_rev) + @raise L{errors.HypervisorError}: when the KVM version cannot be retrieved + + """ + result = utils.RunCmd([constants.KVM_PATH, "--help"]) + if result.failed: + raise errors.HypervisorError("Unable to get KVM version") + return cls._ParseKVMVersion(result.output) + def StopInstance(self, instance, force=False, retry=False, name=None): """Stop an instance. @@ -624,18 +1062,21 @@ class KVMHypervisor(hv_base.BaseHypervisor): acpi = instance.hvparams[constants.HV_ACPI] else: acpi = False - pidfile, pid, alive = self._InstancePidAlive(name) + _, pid, alive = self._InstancePidAlive(name) if pid > 0 and alive: if force or not acpi: utils.KillProcess(pid) else: - self._CallMonitorCommand(name, 'system_powerdown') + self._CallMonitorCommand(name, "system_powerdown") - if not self._InstancePidAlive(name)[2]: - self._RemoveInstanceRuntimeFiles(pidfile, name) - return True - else: - return False + def CleanupInstance(self, instance_name): + """Cleanup after a stopped instance + + """ + pidfile, pid, alive = self._InstancePidAlive(instance_name) + if pid > 0 and alive: + raise errors.HypervisorError("Cannot cleanup a live instance") + self._RemoveInstanceRuntimeFiles(pidfile, instance_name) def RebootInstance(self, instance): """Reboot an instance. @@ -690,10 +1131,28 @@ class KVMHypervisor(hv_base.BaseHypervisor): Stop the incoming mode KVM. @type instance: L{objects.Instance} - @param instance: instance whose migration is being aborted + @param instance: instance whose migration is being finalized """ if success: + kvm_runtime = self._LoadKVMRuntime(instance, serialized_runtime=info) + kvm_nics = kvm_runtime[1] + + for nic_seq, nic in enumerate(kvm_nics): + if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED: + # Bridged interfaces have already been configured + continue + try: + tap = utils.ReadFile(self._InstanceNICFile(instance.name, nic_seq)) + except EnvironmentError, err: + logging.warning("Failed to find host interface for %s NIC #%d: %s", + instance.name, nic_seq, str(err)) + continue + try: + self._ConfigureNIC(instance, nic_seq, nic, tap) + except errors.HypervisorError, err: + logging.warning(str(err)) + self._WriteKVMRuntime(instance.name, info) else: self.StopInstance(instance, force=True) @@ -718,17 +1177,21 @@ class KVMHypervisor(hv_base.BaseHypervisor): if not alive: raise errors.HypervisorError("Instance not running, cannot migrate") - if not utils.TcpPing(target, port, live_port_needed=True): - raise errors.HypervisorError("Remote host %s not listening on port" - " %s, cannot migrate" % (target, port)) - if not live: - self._CallMonitorCommand(instance_name, 'stop') + self._CallMonitorCommand(instance_name, "stop") + + migrate_command = ("migrate_set_speed %dm" % + instance.hvparams[constants.HV_MIGRATION_BANDWIDTH]) + self._CallMonitorCommand(instance_name, migrate_command) - migrate_command = 'migrate -d tcp:%s:%s' % (target, port) + migrate_command = ("migrate_set_downtime %dms" % + instance.hvparams[constants.HV_MIGRATION_DOWNTIME]) self._CallMonitorCommand(instance_name, migrate_command) - info_command = 'info migrate' + migrate_command = "migrate -d tcp:%s:%s" % (target, port) + self._CallMonitorCommand(instance_name, migrate_command) + + info_command = "info migrate" done = False broken_answers = 0 while not done: @@ -744,13 +1207,13 @@ class KVMHypervisor(hv_base.BaseHypervisor): time.sleep(self._MIGRATION_INFO_RETRY_DELAY) else: status = match.group(1) - if status == 'completed': + if status == "completed": done = True - elif status == 'active': + elif status == "active": # reset the broken answers count broken_answers = 0 time.sleep(self._MIGRATION_INFO_RETRY_DELAY) - elif status == 'failed' or status == 'cancelled': + elif status == "failed" or status == "cancelled": if not live: self._CallMonitorCommand(instance_name, 'cont') raise errors.HypervisorError("Migration %s at the kvm level" % @@ -779,28 +1242,35 @@ class KVMHypervisor(hv_base.BaseHypervisor): return self.GetLinuxNodeInfo() @classmethod - def GetShellCommandForConsole(cls, instance, hvparams, beparams): + def GetInstanceConsole(cls, instance, hvparams, beparams): """Return a command for connecting to the console of an instance. """ if hvparams[constants.HV_SERIAL_CONSOLE]: - shell_command = ("%s STDIO,%s UNIX-CONNECT:%s" % - (constants.SOCAT_PATH, cls._SocatUnixConsoleParams(), - utils.ShellQuote(cls._InstanceSerial(instance.name)))) - else: - shell_command = "echo 'No serial shell for instance %s'" % instance.name + cmd = [constants.KVM_CONSOLE_WRAPPER, + constants.SOCAT_PATH, utils.ShellQuote(instance.name), + utils.ShellQuote(cls._InstanceMonitor(instance.name)), + "STDIO,%s" % cls._SocatUnixConsoleParams(), + "UNIX-CONNECT:%s" % cls._InstanceSerial(instance.name)] + return objects.InstanceConsole(instance=instance.name, + kind=constants.CONS_SSH, + host=instance.primary_node, + user=constants.GANETI_RUNAS, + command=cmd) vnc_bind_address = hvparams[constants.HV_VNC_BIND_ADDRESS] - if vnc_bind_address: - if instance.network_port > constants.VNC_BASE_PORT: - display = instance.network_port - constants.VNC_BASE_PORT - vnc_command = ("echo 'Instance has VNC listening on %s:%d" - " (display: %d)'" % (vnc_bind_address, - instance.network_port, - display)) - shell_command = "%s; %s" % (vnc_command, shell_command) - - return shell_command + if vnc_bind_address and instance.network_port > constants.VNC_BASE_PORT: + display = instance.network_port - constants.VNC_BASE_PORT + return objects.InstanceConsole(instance=instance.name, + kind=constants.CONS_VNC, + host=vnc_bind_address, + port=instance.network_port, + display=display) + + return objects.InstanceConsole(instance=instance.name, + kind=constants.CONS_MESSAGE, + message=("No serial shell for instance %s" % + instance.name)) def Verify(self): """Verify the hypervisor. @@ -813,7 +1283,6 @@ class KVMHypervisor(hv_base.BaseHypervisor): if not os.path.exists(constants.SOCAT_PATH): return "The socat binary ('%s') does not exist." % constants.SOCAT_PATH - @classmethod def CheckParameterSyntax(cls, hvparams): """Check the given parameters for validity. @@ -853,8 +1322,24 @@ class KVMHypervisor(hv_base.BaseHypervisor): if hvparams[constants.HV_SECURITY_DOMAIN]: raise errors.HypervisorError("Cannot have a security domain when the" " security model is 'none' or 'pool'") - if security_model == constants.HT_SM_POOL: - raise errors.HypervisorError("Security model pool is not supported yet") + + spice_bind = hvparams[constants.HV_KVM_SPICE_BIND] + if spice_bind: + spice_ip_version = hvparams[constants.HV_KVM_SPICE_IP_VERSION] + if spice_ip_version != constants.IFACE_NO_IP_VERSION_SPECIFIED: + # if an IP version is specified, the spice_bind parameter must be an + # IP of that family + if (netutils.IP4Address.IsValid(spice_bind) and + spice_ip_version != constants.IP4_VERSION): + raise errors.HypervisorError("spice: got an IPv4 address (%s), but" + " the specified IP version is %s" % + (spice_bind, spice_ip_version)) + + if (netutils.IP6Address.IsValid(spice_bind) and + spice_ip_version != constants.IP6_VERSION): + raise errors.HypervisorError("spice: got an IPv6 address (%s), but" + " the specified IP version is %s" % + (spice_bind, spice_ip_version)) @classmethod def ValidateParameters(cls, hvparams): @@ -876,6 +1361,28 @@ class KVMHypervisor(hv_base.BaseHypervisor): raise errors.HypervisorError("Unknown security domain user %s" % username) + spice_bind = hvparams[constants.HV_KVM_SPICE_BIND] + if spice_bind: + # only one of VNC and SPICE can be used currently. + if hvparams[constants.HV_VNC_BIND_ADDRESS]: + raise errors.HypervisorError("both SPICE and VNC are configured, but" + " only one of them can be used at a" + " given time.") + + # KVM version should be >= 0.14.0 + _, v_major, v_min, _ = cls._GetKVMVersion() + if (v_major, v_min) < (0, 14): + raise errors.HypervisorError("spice is configured, but it is not" + " available in versions of KVM < 0.14") + + # if spice_bind is not an IP address, it must be a valid interface + bound_to_addr = (netutils.IP4Address.IsValid(spice_bind) + or netutils.IP6Address.IsValid(spice_bind)) + if not bound_to_addr and not netutils.IsValidInterface(spice_bind): + raise errors.HypervisorError("spice: the %s parameter must be either" + " a valid IP address or interface name" % + constants.HV_KVM_SPICE_BIND) + @classmethod def PowercycleNode(cls): """KVM powercycle, just a wrapper over Linux powercycle.