| # |
| # |
| |
| # Copyright (C) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Google Inc. |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # 1. Redistributions of source code must retain the above copyright notice, |
| # this list of conditions and the following disclaimer. |
| # |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
| # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
| # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
| # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
| # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
| # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| |
| """KVM hypervisor |
| |
| """ |
| |
| import errno |
| import os |
| import os.path |
| import re |
| import tempfile |
| import time |
| import logging |
| import pwd |
| import shutil |
| import urllib2 |
| from bitarray import bitarray |
| try: |
| import psutil # pylint: disable=F0401 |
| if psutil.version_info < (2, 0, 0): |
| # The psutil version seems too old, we ignore it |
| psutil_err = "too old (2.x.x needed, %s found)" % psutil.__version__ |
| psutil = None |
| elif psutil.version_info >= (3,): |
| psutil_err = "too new (2.x.x needed, %s found)" % psutil.__version__ |
| psutil = None |
| else: |
| psutil_err = "<no error>" |
| except ImportError: |
| psutil_err = "not found" |
| psutil = None |
| try: |
| import fdsend # pylint: disable=F0401 |
| except ImportError: |
| fdsend = None |
| |
| 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 import netutils |
| from ganeti import pathutils |
| from ganeti.hypervisor import hv_base |
| from ganeti.utils import wrapper as utils_wrapper |
| |
| from ganeti.hypervisor.hv_kvm.monitor import QmpConnection, QmpMessage, \ |
| MonitorSocket |
| from ganeti.hypervisor.hv_kvm.netdev import OpenTap |
| |
| |
| _KVM_NETWORK_SCRIPT = pathutils.CONF_DIR + "/kvm-vif-bridge" |
| _KVM_START_PAUSED_FLAG = "-S" |
| |
| #: SPICE parameters which depend on L{constants.HV_KVM_SPICE_BIND} |
| _SPICE_ADDITIONAL_PARAMS = frozenset([ |
| constants.HV_KVM_SPICE_IP_VERSION, |
| constants.HV_KVM_SPICE_PASSWORD_FILE, |
| constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR, |
| constants.HV_KVM_SPICE_JPEG_IMG_COMPR, |
| constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR, |
| constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION, |
| constants.HV_KVM_SPICE_USE_TLS, |
| ]) |
| |
| # below constants show the format of runtime file |
| # the nics are in second possition, while the disks in 4th (last) |
| # moreover disk entries are stored as a list of in tuples |
| # (L{objects.Disk}, link_name, uri) |
| _KVM_NICS_RUNTIME_INDEX = 1 |
| _KVM_DISKS_RUNTIME_INDEX = 3 |
| _DEVICE_RUNTIME_INDEX = { |
| constants.HOTPLUG_TARGET_DISK: _KVM_DISKS_RUNTIME_INDEX, |
| constants.HOTPLUG_TARGET_NIC: _KVM_NICS_RUNTIME_INDEX |
| } |
| _FIND_RUNTIME_ENTRY = { |
| constants.HOTPLUG_TARGET_NIC: |
| lambda nic, kvm_nics: [n for n in kvm_nics if n.uuid == nic.uuid], |
| constants.HOTPLUG_TARGET_DISK: |
| lambda disk, kvm_disks: [(d, l, u) for (d, l, u) in kvm_disks |
| if d.uuid == disk.uuid] |
| } |
| _RUNTIME_DEVICE = { |
| constants.HOTPLUG_TARGET_NIC: lambda d: d, |
| constants.HOTPLUG_TARGET_DISK: lambda (d, e, _): d |
| } |
| _RUNTIME_ENTRY = { |
| constants.HOTPLUG_TARGET_NIC: lambda d, e: d, |
| constants.HOTPLUG_TARGET_DISK: lambda d, e: (d, e[0], e[1]) |
| } |
| |
| _DEVICE_TYPE = { |
| constants.HOTPLUG_TARGET_NIC: lambda hvp: hvp[constants.HV_NIC_TYPE], |
| constants.HOTPLUG_TARGET_DISK: lambda hvp: hvp[constants.HV_DISK_TYPE], |
| } |
| |
| _DEVICE_DRIVER = { |
| constants.HOTPLUG_TARGET_NIC: |
| lambda ht: "virtio-net-pci" if ht == constants.HT_NIC_PARAVIRTUAL else ht, |
| constants.HOTPLUG_TARGET_DISK: |
| lambda ht: "virtio-blk-pci" if ht == constants.HT_DISK_PARAVIRTUAL else ht, |
| } |
| |
| |
| # NICs and paravirtual disks |
| # show up as devices on the PCI bus (one slot per device). |
| # SCSI disks will be placed on the SCSI bus. |
| _DEVICE_BUS = { |
| constants.HOTPLUG_TARGET_NIC: |
| lambda _: _PCI_BUS, |
| constants.HOTPLUG_TARGET_DISK: |
| lambda ht: _SCSI_BUS if ht in constants.HT_SCSI_DEVICE_TYPES else _PCI_BUS |
| } |
| |
| _HOTPLUGGABLE_DEVICE_TYPES = { |
| # All available NIC types except for ne2k_isa |
| constants.HOTPLUG_TARGET_NIC: [ |
| constants.HT_NIC_E1000, |
| constants.HT_NIC_I82551, |
| constants.HT_NIC_I8259ER, |
| constants.HT_NIC_I85557B, |
| constants.HT_NIC_NE2K_PCI, |
| constants.HT_NIC_PARAVIRTUAL, |
| constants.HT_NIC_PCNET, |
| constants.HT_NIC_RTL8139, |
| ], |
| constants.HOTPLUG_TARGET_DISK: [ |
| constants.HT_DISK_PARAVIRTUAL, |
| constants.HT_DISK_SCSI_BLOCK, |
| constants.HT_DISK_SCSI_GENERIC, |
| constants.HT_DISK_SCSI_HD, |
| constants.HT_DISK_SCSI_CD, |
| ] |
| } |
| |
| _PCI_BUS = "pci.0" |
| _SCSI_BUS = "scsi.0" |
| |
| _MIGRATION_CAPS_DELIM = ":" |
| |
| |
| def _with_qmp(fn): |
| """Wrapper used on hotplug related methods""" |
| def wrapper(self, instance, *args, **kwargs): |
| """Create a QmpConnection and run the wrapped method""" |
| if not getattr(self, "qmp", None): |
| filename = self._InstanceQmpMonitor(instance.name)# pylint: disable=W0212 |
| self.qmp = QmpConnection(filename) |
| return fn(self, instance, *args, **kwargs) |
| return wrapper |
| |
| |
| def _GetDriveURI(disk, link, uri): |
| """Helper function to get the drive uri to be used in --drive kvm option |
| |
| Invoked during startup and disk hot-add. In latter case and if no userspace |
| access mode is used it will be overriden with /dev/fdset/<fdset-id> (see |
| HotAddDisk() and AddFd() of QmpConnection). |
| |
| @type disk: L{objects.Disk} |
| @param disk: A disk configuration object |
| @type link: string |
| @param link: The device link as returned by _SymlinkBlockDev() |
| @type uri: string |
| @param uri: The drive uri as returned by _CalculateDeviceURI() |
| |
| @return: The drive uri to use in kvm option |
| |
| """ |
| access_mode = disk.params.get(constants.LDP_ACCESS, |
| constants.DISK_KERNELSPACE) |
| # If uri is available, use it during startup/hot-add |
| if (uri and access_mode == constants.DISK_USERSPACE): |
| drive_uri = uri |
| # Otherwise use the link previously created |
| else: |
| drive_uri = link |
| |
| return drive_uri |
| |
| |
| def _GenerateDeviceKVMId(dev_type, dev): |
| """Helper function to generate a unique device name used by KVM |
| |
| QEMU monitor commands use names to identify devices. Since the UUID |
| is too long for a device ID (36 chars vs. 30), we choose to use |
| only the part until the third '-' with a disk/nic prefix. |
| For example if a disk has UUID '932df160-7a22-4067-a566-7e0ca8386133' |
| the resulting device ID would be 'disk-932df160-7a22-4067'. |
| |
| @type dev_type: string |
| @param dev_type: device type of param dev (HOTPLUG_TARGET_DISK|NIC) |
| @type dev: L{objects.Disk} or L{objects.NIC} |
| @param dev: the device object for which we generate a kvm name |
| |
| """ |
| return "%s-%s" % (dev_type.lower(), dev.uuid.rsplit("-", 2)[0]) |
| |
| |
| def _GenerateDeviceHVInfoStr(hvinfo): |
| """Construct the -device option string for hvinfo dict |
| |
| PV disk: virtio-blk-pci,id=disk-1234,bus=pci.0,addr=0x9 |
| PV NIC: virtio-net-pci,id=nic-1234,bus=pci.0,addr=0x9 |
| SG disk: scsi-generic,id=disk-1234,bus=scsi.0,channel=0,scsi-id=1,lun=0 |
| |
| @type hvinfo: dict |
| @param hvinfo: dictionary created by _GenerateDeviceHVInfo() |
| |
| @rtype: string |
| @return: The constructed string to be passed along with a -device option |
| |
| """ |
| |
| # work on a copy |
| d = dict(hvinfo) |
| hvinfo_str = d.pop("driver") |
| for k, v in d.items(): |
| hvinfo_str += ",%s=%s" % (k, v) |
| |
| return hvinfo_str |
| |
| |
| def _GenerateDeviceHVInfo(dev_type, kvm_devid, hv_dev_type, bus_slots): |
| """Helper function to generate hvinfo of a device (disk, NIC) |
| |
| hvinfo will hold all necessary info for generating the -device QEMU option. |
| We have two main buses: a PCI bus and a SCSI bus (created by a SCSI |
| controller on the PCI bus). |
| |
| In case of PCI devices we add them on a free PCI slot (addr) on the first PCI |
| bus (pci.0), and in case of SCSI devices we decide to put each disk on a |
| different SCSI target (scsi-id) on the first SCSI bus (scsi.0). |
| |
| @type dev_type: string |
| @param dev_type: either HOTPLUG_TARGET_DISK or HOTPLUG_TARGET_NIC |
| @type kvm_devid: string |
| @param kvm_devid: the id of the device |
| @type hv_dev_type: string |
| @param hv_dev_type: either disk_type or nic_type hvparam |
| @type bus_slots: dict |
| @param bus_slots: the current slots of the first PCI and SCSI buses |
| |
| @rtype: dict |
| @return: dict including all necessary info (driver, id, bus and bus location) |
| for generating a -device QEMU option for either a disk or a NIC |
| |
| """ |
| driver = _DEVICE_DRIVER[dev_type](hv_dev_type) |
| bus = _DEVICE_BUS[dev_type](hv_dev_type) |
| slots = bus_slots[bus] |
| slot = utils.GetFreeSlot(slots, reserve=True) |
| |
| hvinfo = { |
| "driver": driver, |
| "id": kvm_devid, |
| "bus": bus, |
| } |
| |
| if bus == _PCI_BUS: |
| hvinfo.update({ |
| "addr": hex(slot), |
| }) |
| elif bus == _SCSI_BUS: |
| hvinfo.update({ |
| "channel": 0, |
| "scsi-id": slot, |
| "lun": 0, |
| }) |
| |
| return hvinfo |
| |
| |
| def _GetExistingDeviceInfo(dev_type, device, runtime): |
| """Helper function to get an existing device inside the runtime file |
| |
| Used when an instance is running. Load kvm runtime file and search |
| for a device based on its type and uuid. |
| |
| @type dev_type: sting |
| @param dev_type: device type of param dev |
| @type device: L{objects.Disk} or L{objects.NIC} |
| @param device: the device object for which we generate a kvm name |
| @type runtime: tuple (cmd, nics, hvparams, disks) |
| @param runtime: the runtime data to search for the device |
| @raise errors.HotplugError: in case the requested device does not |
| exist (e.g. device has been added without --hotplug option) |
| |
| """ |
| index = _DEVICE_RUNTIME_INDEX[dev_type] |
| found = _FIND_RUNTIME_ENTRY[dev_type](device, runtime[index]) |
| if not found: |
| raise errors.HotplugError("Cannot find runtime info for %s with UUID %s" % |
| (dev_type, device.uuid)) |
| |
| return found[0] |
| |
| |
| def _UpgradeSerializedRuntime(serialized_runtime): |
| """Upgrade runtime data |
| |
| Remove any deprecated fields or change the format of the data. |
| The runtime files are not upgraded when Ganeti is upgraded, so the required |
| modification have to be performed here. |
| |
| @type serialized_runtime: string |
| @param serialized_runtime: raw text data read from actual runtime file |
| @return: (cmd, nic dicts, hvparams, bdev dicts) |
| @rtype: tuple |
| |
| """ |
| loaded_runtime = serializer.Load(serialized_runtime) |
| kvm_cmd, serialized_nics, hvparams = loaded_runtime[:3] |
| if len(loaded_runtime) >= 4: |
| serialized_disks = loaded_runtime[3] |
| else: |
| serialized_disks = [] |
| |
| def update_hvinfo(dev, dev_type): |
| """ Remove deprecated pci slot and substitute it with hvinfo """ |
| if "hvinfo" not in dev: |
| dev["hvinfo"] = {} |
| uuid = dev["uuid"] |
| # Ganeti used to save the PCI slot of paravirtual devices |
| # (virtio-blk-pci, virtio-net-pci) in runtime files during |
| # _GenerateKVMRuntime() and HotAddDevice(). |
| # In this case we had a -device QEMU option in the command line with id, |
| # drive|netdev, bus, and addr params. All other devices did not have an |
| # id nor placed explicitly on a bus. |
| # hot- prefix is removed in 2.16. Here we add it explicitly to |
| # handle old instances in the cluster properly. |
| if "pci" in dev: |
| # This is practically the old _GenerateDeviceKVMId() |
| dev["hvinfo"]["id"] = "hot%s-%s-%s-%s" % (dev_type.lower(), |
| uuid.split("-")[0], |
| "pci", |
| dev["pci"]) |
| dev["hvinfo"]["addr"] = hex(dev["pci"]) |
| dev["hvinfo"]["bus"] = _PCI_BUS |
| del dev["pci"] |
| |
| for nic in serialized_nics: |
| # Add a dummy uuid slot if an pre-2.8 NIC is found |
| if "uuid" not in nic: |
| nic["uuid"] = utils.NewUUID() |
| update_hvinfo(nic, constants.HOTPLUG_TARGET_NIC) |
| |
| for disk_entry in serialized_disks: |
| # We have a (Disk, link, uri) tuple |
| update_hvinfo(disk_entry[0], constants.HOTPLUG_TARGET_DISK) |
| |
| return kvm_cmd, serialized_nics, hvparams, serialized_disks |
| |
| |
| def _AnalyzeSerializedRuntime(serialized_runtime): |
| """Return runtime entries for a serialized runtime file |
| |
| @type serialized_runtime: string |
| @param serialized_runtime: raw text data read from actual runtime file |
| @return: (cmd, nics, hvparams, bdevs) |
| @rtype: tuple |
| |
| """ |
| kvm_cmd, serialized_nics, hvparams, serialized_disks = \ |
| _UpgradeSerializedRuntime(serialized_runtime) |
| kvm_nics = [objects.NIC.FromDict(snic) for snic in serialized_nics] |
| kvm_disks = [(objects.Disk.FromDict(sdisk), link, uri) |
| for sdisk, link, uri in serialized_disks] |
| |
| return (kvm_cmd, kvm_nics, hvparams, kvm_disks) |
| |
| |
| class HeadRequest(urllib2.Request): |
| def get_method(self): |
| return "HEAD" |
| |
| |
| def _CheckUrl(url): |
| """Check if a given URL exists on the server |
| |
| """ |
| try: |
| urllib2.urlopen(HeadRequest(url)) |
| return True |
| except urllib2.URLError: |
| return False |
| |
| |
| class KVMHypervisor(hv_base.BaseHypervisor): |
| """KVM hypervisor interface |
| |
| """ |
| CAN_MIGRATE = True |
| |
| _ROOT_DIR = pathutils.RUN_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 |
| _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_KVM_PATH: hv_base.REQ_FILE_CHECK, |
| constants.HV_KERNEL_PATH: hv_base.OPT_FILE_CHECK, |
| constants.HV_INITRD_PATH: hv_base.OPT_FILE_CHECK, |
| constants.HV_ROOT_PATH: hv_base.NO_CHECK, |
| constants.HV_KERNEL_ARGS: hv_base.NO_CHECK, |
| constants.HV_ACPI: hv_base.NO_CHECK, |
| constants.HV_SERIAL_CONSOLE: hv_base.NO_CHECK, |
| constants.HV_SERIAL_SPEED: hv_base.NO_CHECK, |
| constants.HV_VNC_BIND_ADDRESS: hv_base.NO_CHECK, # will be checked later |
| 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_SPICE_PASSWORD_FILE: hv_base.OPT_FILE_CHECK, |
| constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR: |
| hv_base.ParamInSet( |
| False, constants.HT_KVM_SPICE_VALID_LOSSLESS_IMG_COMPR_OPTIONS), |
| constants.HV_KVM_SPICE_JPEG_IMG_COMPR: |
| hv_base.ParamInSet( |
| False, constants.HT_KVM_SPICE_VALID_LOSSY_IMG_COMPR_OPTIONS), |
| constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR: |
| hv_base.ParamInSet( |
| False, constants.HT_KVM_SPICE_VALID_LOSSY_IMG_COMPR_OPTIONS), |
| constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION: |
| hv_base.ParamInSet( |
| False, constants.HT_KVM_SPICE_VALID_VIDEO_STREAM_DETECTION_OPTIONS), |
| constants.HV_KVM_SPICE_AUDIO_COMPR: hv_base.NO_CHECK, |
| constants.HV_KVM_SPICE_USE_TLS: hv_base.NO_CHECK, |
| constants.HV_KVM_SPICE_TLS_CIPHERS: hv_base.NO_CHECK, |
| constants.HV_KVM_SPICE_USE_VDAGENT: hv_base.NO_CHECK, |
| constants.HV_KVM_FLOPPY_IMAGE_PATH: hv_base.OPT_FILE_CHECK, |
| constants.HV_CDROM_IMAGE_PATH: hv_base.OPT_FILE_OR_URL_CHECK, |
| constants.HV_KVM_CDROM2_IMAGE_PATH: hv_base.OPT_FILE_OR_URL_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_SCSI_CONTROLLER_TYPE: |
| hv_base.ParamInSet(True, constants.HT_KVM_VALID_SCSI_CONTROLLER_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_KEYMAP: hv_base.NO_CHECK, |
| constants.HV_MIGRATION_PORT: hv_base.REQ_NET_PORT_CHECK, |
| constants.HV_MIGRATION_BANDWIDTH: hv_base.REQ_NONNEGATIVE_INT_CHECK, |
| constants.HV_MIGRATION_DOWNTIME: hv_base.REQ_NONNEGATIVE_INT_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_KVM_DISK_AIO: |
| hv_base.ParamInSet(False, constants.HT_KVM_VALID_AIO_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_VIRTIO_NET_QUEUES: hv_base.OPT_VIRTIO_NET_QUEUES_CHECK, |
| constants.HV_KVM_USE_CHROOT: hv_base.NO_CHECK, |
| constants.HV_KVM_USER_SHUTDOWN: hv_base.NO_CHECK, |
| constants.HV_MEM_PATH: hv_base.OPT_DIR_CHECK, |
| constants.HV_REBOOT_BEHAVIOR: |
| hv_base.ParamInSet(True, constants.REBOOT_BEHAVIORS), |
| constants.HV_CPU_MASK: hv_base.OPT_MULTI_CPU_MASK_CHECK, |
| constants.HV_CPU_TYPE: hv_base.NO_CHECK, |
| constants.HV_CPU_CORES: hv_base.OPT_NONNEGATIVE_INT_CHECK, |
| constants.HV_CPU_THREADS: hv_base.OPT_NONNEGATIVE_INT_CHECK, |
| constants.HV_CPU_SOCKETS: hv_base.OPT_NONNEGATIVE_INT_CHECK, |
| constants.HV_SOUNDHW: hv_base.NO_CHECK, |
| constants.HV_USB_DEVICES: hv_base.NO_CHECK, |
| constants.HV_VGA: hv_base.NO_CHECK, |
| constants.HV_KVM_EXTRA: hv_base.NO_CHECK, |
| constants.HV_KVM_MACHINE_VERSION: hv_base.NO_CHECK, |
| constants.HV_KVM_MIGRATION_CAPS: hv_base.NO_CHECK, |
| constants.HV_KVM_PCI_RESERVATIONS: |
| (False, lambda x: (x >= 0 and x <= constants.QEMU_PCI_SLOTS), |
| "The number of PCI slots managed by QEMU (max: %s)" % |
| constants.QEMU_PCI_SLOTS, |
| None, None), |
| constants.HV_VNET_HDR: hv_base.NO_CHECK, |
| } |
| |
| _VIRTIO = "virtio" |
| _VIRTIO_NET_PCI = "virtio-net-pci" |
| _VIRTIO_BLK_PCI = "virtio-blk-pci" |
| |
| _MIGRATION_STATUS_RE = re.compile(r"Migration\s+status:\s+(\w+)", |
| re.M | re.I) |
| _MIGRATION_PROGRESS_RE = \ |
| re.compile(r"\s*transferred\s+ram:\s+(?P<transferred>\d+)\s+kbytes\s*\n" |
| r"\s*remaining\s+ram:\s+(?P<remaining>\d+)\s+kbytes\s*\n" |
| r"\s*total\s+ram:\s+(?P<total>\d+)\s+kbytes\s*\n", re.I) |
| |
| _MIGRATION_INFO_MAX_BAD_ANSWERS = 5 |
| _MIGRATION_INFO_RETRY_DELAY = 2 |
| |
| _VERSION_RE = re.compile(r"\b(\d+)\.(\d+)(\.(\d+))?\b") |
| |
| _CPU_INFO_RE = re.compile(r"cpu\s+\#(\d+).*thread_id\s*=\s*(\d+)", re.I) |
| _CPU_INFO_CMD = "info cpus" |
| _CONT_CMD = "cont" |
| |
| _DEFAULT_MACHINE_VERSION_RE = re.compile(r"^(\S+).*\(default\)", re.M) |
| _CHECK_MACHINE_VERSION_RE = \ |
| staticmethod(lambda x: re.compile(r"^(%s)[ ]+.*PC" % x, re.M)) |
| |
| _QMP_RE = re.compile(r"^-qmp\s", re.M) |
| _SPICE_RE = re.compile(r"^-spice\s", re.M) |
| _VHOST_RE = re.compile(r"^-net\s.*,vhost=on|off", re.M) |
| _VIRTIO_NET_QUEUES_RE = re.compile(r"^-net\s.*,fds=x:y:...:z", re.M) |
| _ENABLE_KVM_RE = re.compile(r"^-enable-kvm\s", re.M) |
| _DISABLE_KVM_RE = re.compile(r"^-disable-kvm\s", re.M) |
| _NETDEV_RE = re.compile(r"^-netdev\s", re.M) |
| _DISPLAY_RE = re.compile(r"^-display\s", re.M) |
| _MACHINE_RE = re.compile(r"^-machine\s", re.M) |
| _DEVICE_DRIVER_SUPPORTED = \ |
| staticmethod(lambda drv, devlist: |
| re.compile(r"^name \"%s\"" % drv, re.M).search(devlist)) |
| # match -drive.*boot=on|off on different lines, but in between accept only |
| # dashes not preceeded by a new line (which would mean another option |
| # different than -drive is starting) |
| _BOOT_RE = re.compile(r"^-drive\s([^-]|(?<!^)-)*,boot=on\|off", re.M | re.S) |
| _UUID_RE = re.compile(r"^-uuid\s", re.M) |
| |
| _INFO_VERSION_RE = \ |
| re.compile(r'^QEMU (\d+)\.(\d+)(\.(\d+))?.*monitor.*', re.M) |
| _INFO_VERSION_CMD = "info version" |
| |
| # Slot 0 for Host bridge, Slot 1 for ISA bridge, Slot 2 for VGA controller |
| # and the rest up to slot 11 will be used by QEMU implicitly. |
| # Ganeti will add disks and NICs from slot 12 onwards. |
| # NOTE: This maps to the default PCI bus created by pc machine type |
| # by default (pci.0). The q35 creates a PCIe bus that is not hotpluggable |
| # and should be handled differently (pcie.0). |
| # NOTE: This bitarray here is defined for more fine-grained control. |
| # Currently the number of slots is QEMU_PCI_SLOTS and the reserved |
| # ones are the first QEMU_DEFAULT_PCI_RESERVATIONS. |
| # If the above constants change without updating _DEFAULT_PCI_RESERVATIONS |
| # properly, TestGenerateDeviceHVInfo() will probably break. |
| _DEFAULT_PCI_RESERVATIONS = "11111111111100000000000000000000" |
| # The SCSI bus is created on demand or automatically and is empty. |
| # For simplicity we decide to use a different target (scsi-id) |
| # for each SCSI disk. Here we support 16 SCSI disks which is |
| # actually the current hard limit (constants.MAX_DISKS). |
| # NOTE: Max device counts depend on the SCSI controller type; |
| # Just for the record, lsi supports up to 7, megasas 64, |
| # and virtio-scsi-pci 255. |
| _DEFAULT_SCSI_RESERVATIONS = "0000000000000000" |
| |
| ANCILLARY_FILES = [ |
| _KVM_NETWORK_SCRIPT, |
| ] |
| ANCILLARY_FILES_OPT = [ |
| _KVM_NETWORK_SCRIPT, |
| ] |
| |
| # Supported kvm options to get output from |
| _KVMOPT_HELP = "help" |
| _KVMOPT_MLIST = "mlist" |
| _KVMOPT_DEVICELIST = "devicelist" |
| |
| # Command to execute to get the output from kvm, and whether to |
| # accept the output even on failure. |
| _KVMOPTS_CMDS = { |
| _KVMOPT_HELP: (["--help"], False), |
| _KVMOPT_MLIST: (["-M", "?"], False), |
| _KVMOPT_DEVICELIST: (["-device", "?"], True), |
| } |
| |
| def __init__(self): |
| hv_base.BaseHypervisor.__init__(self) |
| # Let's make sure the directories we need exist, even if the RUN_DIR lives |
| # in a tmpfs filesystem or has been otherwise wiped out. |
| dirs = [(dname, constants.RUN_DIRS_MODE) for dname in self._DIRS] |
| utils.EnsureDirs(dirs) |
| self.qmp = None |
| |
| @staticmethod |
| def VersionsSafeForMigration(src, target): |
| """Predict if migration is safe between those versions |
| |
| """ |
| # Actually, it is not that easy. However, with the kvm machine_version |
| # feature, migration suceeds in most cases. So we try not to block |
| # legitimate migrations. |
| return True |
| |
| @classmethod |
| def _InstancePidFile(cls, instance_name): |
| """Returns the instance pidfile. |
| |
| """ |
| 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. |
| |
| Check that a pid file is associated with an instance, and retrieve |
| information from its command line. |
| |
| @type pid: string or int |
| @param pid: process id of the instance to check |
| @rtype: tuple |
| @return: (instance_name, memory, vcpus) |
| @raise errors.HypervisorError: when an instance cannot be found |
| |
| """ |
| alive = utils.IsProcessAlive(pid) |
| if not alive: |
| raise errors.HypervisorError("Cannot get info for pid %s" % pid) |
| |
| cmdline_file = utils.PathJoin("/proc", str(pid), "cmdline") |
| try: |
| cmdline = utils.ReadFile(cmdline_file) |
| except EnvironmentError, err: |
| raise errors.HypervisorError("Can't open cmdline file for pid %s: %s" % |
| (pid, err)) |
| |
| instance = None |
| memory = 0 |
| vcpus = 0 |
| |
| arg_list = cmdline.split("\x00") |
| while arg_list: |
| arg = arg_list.pop(0) |
| if arg == "-name": |
| instance = arg_list.pop(0) |
| elif arg == "-m": |
| memory = int(arg_list.pop(0)) |
| elif arg == "-smp": |
| vcpus = int(arg_list.pop(0).split(",")[0]) |
| |
| if instance is None: |
| raise errors.HypervisorError("Pid %s doesn't contain a ganeti kvm" |
| " instance" % pid) |
| |
| return (instance, memory, vcpus) |
| |
| @classmethod |
| def _InstancePidAlive(cls, instance_name): |
| """Returns the instance pidfile, pid, and liveness. |
| |
| @type instance_name: string |
| @param instance_name: instance name |
| @rtype: tuple |
| @return: (pid file name, pid, liveness) |
| |
| """ |
| pidfile = cls._InstancePidFile(instance_name) |
| pid = utils.ReadPidFile(pidfile) |
| |
| alive = False |
| try: |
| cmd_instance = cls._InstancePidInfo(pid)[0] |
| alive = (cmd_instance == instance_name) |
| except errors.HypervisorError: |
| pass |
| |
| return (pidfile, pid, alive) |
| |
| @classmethod |
| def _CheckDown(cls, instance_name): |
| """Raises an error unless the given instance is down. |
| |
| """ |
| alive = cls._InstancePidAlive(instance_name)[2] |
| if alive: |
| raise errors.HypervisorError("Failed to start instance %s: %s" % |
| (instance_name, "already running")) |
| |
| @classmethod |
| def _InstanceMonitor(cls, instance_name): |
| """Returns the instance monitor socket name |
| |
| """ |
| return utils.PathJoin(cls._CTRL_DIR, "%s.monitor" % instance_name) |
| |
| @classmethod |
| def _InstanceSerial(cls, instance_name): |
| """Returns the instance serial socket name |
| |
| """ |
| return utils.PathJoin(cls._CTRL_DIR, "%s.serial" % instance_name) |
| |
| @classmethod |
| def _InstanceQmpMonitor(cls, instance_name): |
| """Returns the instance serial QMP socket name |
| |
| """ |
| return utils.PathJoin(cls._CTRL_DIR, "%s.qmp" % instance_name) |
| |
| @classmethod |
| def _InstanceKvmdMonitor(cls, instance_name): |
| """Returns the instance kvm daemon socket name |
| |
| """ |
| return utils.PathJoin(cls._CTRL_DIR, "%s.kvmd" % instance_name) |
| |
| @classmethod |
| def _InstanceShutdownMonitor(cls, instance_name): |
| """Returns the instance QMP output filename |
| |
| """ |
| return utils.PathJoin(cls._CTRL_DIR, "%s.shutdown" % instance_name) |
| |
| @staticmethod |
| def _SocatUnixConsoleParams(): |
| """Returns the correct parameters for socat |
| |
| If we have a new-enough socat we can use raw mode with an escape character. |
| |
| """ |
| if constants.SOCAT_USE_ESCAPE: |
| return "raw,echo=0,escape=%s" % constants.SOCAT_ESCAPE_CODE |
| else: |
| return "echo=0,icanon=0" |
| |
| @classmethod |
| def _InstanceKVMRuntime(cls, instance_name): |
| """Returns the instance KVM runtime filename |
| |
| """ |
| 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/dirs. |
| |
| """ |
| utils.RemoveFile(pidfile) |
| utils.RemoveFile(cls._InstanceMonitor(instance_name)) |
| utils.RemoveFile(cls._InstanceSerial(instance_name)) |
| utils.RemoveFile(cls._InstanceQmpMonitor(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 |
| |
| @staticmethod |
| def _ConfigureNIC(instance, seq, nic, tap): |
| """Run the network configuration script for a specified NIC |
| |
| See L{hv_base.ConfigureNIC}. |
| |
| @param instance: instance we're acting on |
| @type instance: instance object |
| @param seq: nic sequence number |
| @type seq: int |
| @param nic: nic we're acting on |
| @type nic: nic object |
| @param tap: the host's tap interface this NIC corresponds to |
| @type tap: str |
| |
| """ |
| hv_base.ConfigureNIC([pathutils.KVM_IFUP, tap], instance, seq, nic, tap) |
| |
| @classmethod |
| def _SetProcessAffinity(cls, process_id, cpus): |
| """Sets the affinity of a process to the given CPUs. |
| |
| @type process_id: int |
| @type cpus: list of int |
| @param cpus: The list of CPUs the process ID may use. |
| |
| """ |
| if psutil is None: |
| raise errors.HypervisorError("psutil Python package %s" |
| "; cannot use CPU pinning" |
| " under KVM" % psutil_err) |
| |
| target_process = psutil.Process(process_id) |
| if cpus == constants.CPU_PINNING_OFF: |
| # we checked this at import time |
| # pylint: disable=E1101 |
| target_process.set_cpu_affinity(range(psutil.cpu_count())) |
| else: |
| target_process.set_cpu_affinity(cpus) |
| |
| @classmethod |
| def _AssignCpuAffinity(cls, cpu_mask, process_id, thread_dict): |
| """Change CPU affinity for running VM according to given CPU mask. |
| |
| @param cpu_mask: CPU mask as given by the user. e.g. "0-2,4:all:1,3" |
| @type cpu_mask: string |
| @param process_id: process ID of KVM process. Used to pin entire VM |
| to physical CPUs. |
| @type process_id: int |
| @param thread_dict: map of virtual CPUs to KVM thread IDs |
| @type thread_dict: dict int:int |
| |
| """ |
| # Convert the string CPU mask to a list of list of int's |
| cpu_list = utils.ParseMultiCpuMask(cpu_mask) |
| |
| if len(cpu_list) == 1: |
| all_cpu_mapping = cpu_list[0] |
| if all_cpu_mapping == constants.CPU_PINNING_OFF: |
| # If CPU pinning has 1 entry that's "all", then do nothing |
| pass |
| else: |
| # If CPU pinning has one non-all entry, map the entire VM to |
| # one set of physical CPUs |
| cls._SetProcessAffinity(process_id, all_cpu_mapping) |
| else: |
| # The number of vCPUs mapped should match the number of vCPUs |
| # reported by KVM. This was already verified earlier, so |
| # here only as a sanity check. |
| assert len(thread_dict) == len(cpu_list) |
| |
| # For each vCPU, map it to the proper list of physical CPUs |
| for i, vcpu in enumerate(cpu_list): |
| cls._SetProcessAffinity(thread_dict[i], vcpu) |
| |
| def _GetVcpuThreadIds(self, instance_name): |
| """Get a mapping of vCPU no. to thread IDs for the instance |
| |
| @type instance_name: string |
| @param instance_name: instance in question |
| @rtype: dictionary of int:int |
| @return: a dictionary mapping vCPU numbers to thread IDs |
| |
| """ |
| result = {} |
| output = self._CallMonitorCommand(instance_name, self._CPU_INFO_CMD) |
| for line in output.stdout.splitlines(): |
| match = self._CPU_INFO_RE.search(line) |
| if not match: |
| continue |
| grp = map(int, match.groups()) |
| result[grp[0]] = grp[1] |
| |
| return result |
| |
| def _ExecuteCpuAffinity(self, instance_name, cpu_mask): |
| """Complete CPU pinning. |
| |
| @type instance_name: string |
| @param instance_name: name of instance |
| @type cpu_mask: string |
| @param cpu_mask: CPU pinning mask as entered by user |
| |
| """ |
| # Get KVM process ID, to be used if need to pin entire VM |
| _, pid, _ = self._InstancePidAlive(instance_name) |
| # Get vCPU thread IDs, to be used if need to pin vCPUs separately |
| thread_dict = self._GetVcpuThreadIds(instance_name) |
| # Run CPU pinning, based on configured mask |
| self._AssignCpuAffinity(cpu_mask, pid, thread_dict) |
| |
| def ListInstances(self, hvparams=None): |
| """Get the list of running instances. |
| |
| We can do this by listing our live instances directory and |
| checking whether the associated kvm process is still alive. |
| |
| """ |
| result = [] |
| for name in os.listdir(self._PIDS_DIR): |
| if self._InstancePidAlive(name)[2]: |
| result.append(name) |
| return result |
| |
| @classmethod |
| def _IsUserShutdown(cls, instance_name): |
| return os.path.exists(cls._InstanceShutdownMonitor(instance_name)) |
| |
| @classmethod |
| def _ClearUserShutdown(cls, instance_name): |
| utils.RemoveFile(cls._InstanceShutdownMonitor(instance_name)) |
| |
| def GetInstanceInfo(self, instance_name, hvparams=None): |
| """Get instance properties. |
| |
| @type instance_name: string |
| @param instance_name: the instance name |
| @type hvparams: dict of strings |
| @param hvparams: hypervisor parameters to be used with this instance |
| @rtype: tuple of strings |
| @return: (name, id, memory, vcpus, stat, times) |
| |
| """ |
| _, pid, alive = self._InstancePidAlive(instance_name) |
| if not alive: |
| if self._IsUserShutdown(instance_name): |
| return (instance_name, -1, 0, 0, hv_base.HvInstanceState.SHUTDOWN, 0) |
| else: |
| return None |
| |
| _, memory, vcpus = self._InstancePidInfo(pid) |
| istat = hv_base.HvInstanceState.RUNNING |
| times = 0 |
| |
| try: |
| qmp = QmpConnection(self._InstanceQmpMonitor(instance_name)) |
| qmp.connect() |
| vcpus = len(qmp.Execute("query-cpus")) |
| # Will fail if ballooning is not enabled, but we can then just resort to |
| # the value above. |
| mem_bytes = qmp.Execute("query-balloon")[qmp.ACTUAL_KEY] |
| memory = mem_bytes / 1048576 |
| except errors.HypervisorError: |
| pass |
| |
| return (instance_name, pid, memory, vcpus, istat, times) |
| |
| def GetAllInstancesInfo(self, hvparams=None): |
| """Get properties of all instances. |
| |
| @type hvparams: dict of strings |
| @param hvparams: hypervisor parameters |
| @return: list of tuples (name, id, memory, vcpus, stat, times) |
| |
| """ |
| data = [] |
| for name in os.listdir(self._PIDS_DIR): |
| try: |
| info = self.GetInstanceInfo(name) |
| except errors.HypervisorError: |
| # Ignore exceptions due to instances being shut down |
| continue |
| if info: |
| data.append(info) |
| return data |
| |
| def _GenerateKVMBlockDevicesOptions(self, up_hvp, kvm_disks, |
| kvmhelp, devlist): |
| """Generate KVM options regarding instance's block devices. |
| |
| @type up_hvp: dict |
| @param up_hvp: the instance's runtime hypervisor parameters |
| @type kvm_disks: list of tuples |
| @param kvm_disks: list of tuples [(disk, link_name, uri)..] |
| @type kvmhelp: string |
| @param kvmhelp: output of kvm --help |
| @type devlist: string |
| @param devlist: output of kvm -device ? |
| @rtype: list |
| @return: list of command line options eventually used by kvm executable |
| |
| """ |
| kernel_path = up_hvp[constants.HV_KERNEL_PATH] |
| if kernel_path: |
| boot_disk = False |
| else: |
| boot_disk = up_hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_DISK |
| |
| # whether this is an older KVM version that uses the boot=on flag |
| # on devices |
| needs_boot_flag = self._BOOT_RE.search(kvmhelp) |
| |
| dev_opts = [] |
| disk_type = up_hvp[constants.HV_DISK_TYPE] |
| # paravirtual implies either '-device virtio-blk-pci... -drive if=none...' |
| # for new QEMU versions or '-drive if=virtio' for old QEMU versions |
| if disk_type == constants.HT_DISK_PARAVIRTUAL: |
| driver = self._VIRTIO_BLK_PCI |
| iface = self._VIRTIO |
| else: |
| driver = iface = disk_type |
| |
| # Check if a specific driver is supported by QEMU device model. |
| if self._DEVICE_DRIVER_SUPPORTED(driver, devlist): |
| if_val = ",if=none" # for the -drive option |
| device_driver = driver # for the -device option |
| else: |
| if_val = ",if=%s" % iface # for the -drive option |
| device_driver = None # without -device option |
| |
| # AIO mode |
| aio_mode = up_hvp[constants.HV_KVM_DISK_AIO] |
| if aio_mode == constants.HT_KVM_AIO_NATIVE: |
| aio_val = ",aio=%s" % aio_mode |
| else: |
| aio_val = "" |
| # Cache mode |
| disk_cache = up_hvp[constants.HV_DISK_CACHE] |
| for cfdev, link_name, uri in kvm_disks: |
| if cfdev.dev_type 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 = "" |
| if cfdev.mode != constants.DISK_RDWR: |
| 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: |
| dev_opts.extend(["-boot", "c"]) |
| boot_disk = False |
| if needs_boot_flag and disk_type != constants.HT_DISK_IDE: |
| boot_val = ",boot=on" |
| |
| drive_uri = _GetDriveURI(cfdev, link_name, uri) |
| |
| drive_val = "file=%s,format=raw%s%s%s%s" % \ |
| (drive_uri, if_val, boot_val, cache_val, aio_val) |
| |
| # virtio-blk-pci case |
| if device_driver is not None: |
| # hvinfo will exist for paravirtual devices either due to |
| # _UpgradeSerializedRuntime() for old instances or due to |
| # _GenerateKVMRuntime() for new instances. |
| kvm_devid = cfdev.hvinfo["id"] |
| drive_val += ",id=%s" % kvm_devid |
| # Add driver, id, bus, and addr or channel, scsi-id, lun if any. |
| dev_val = _GenerateDeviceHVInfoStr(cfdev.hvinfo) |
| dev_val += ",drive=%s" % kvm_devid |
| dev_opts.extend(["-device", dev_val]) |
| |
| dev_opts.extend(["-drive", drive_val]) |
| |
| return dev_opts |
| |
| @staticmethod |
| def _CdromOption(kvm_cmd, cdrom_disk_type, cdrom_image, cdrom_boot, |
| needs_boot_flag): |
| """Extends L{kvm_cmd} with the '-drive' option for a cdrom, and |
| optionally the '-boot' option. |
| |
| Example: -drive file=cdrom.iso,media=cdrom,format=raw,if=ide -boot d |
| |
| Example: -drive file=cdrom.iso,media=cdrom,format=raw,if=ide,boot=on |
| |
| Example: -drive file=http://hostname.com/cdrom.iso,media=cdrom |
| |
| @type kvm_cmd: string |
| @param kvm_cmd: KVM command line |
| |
| @type cdrom_disk_type: |
| @param cdrom_disk_type: |
| |
| @type cdrom_image: |
| @param cdrom_image: |
| |
| @type cdrom_boot: |
| @param cdrom_boot: |
| |
| @type needs_boot_flag: |
| @param needs_boot_flag: |
| |
| """ |
| # Check that the ISO image is accessible |
| # See https://bugs.launchpad.net/qemu/+bug/597575 |
| if utils.IsUrl(cdrom_image) and not _CheckUrl(cdrom_image): |
| raise errors.HypervisorError("Cdrom ISO image '%s' is not accessible" % |
| cdrom_image) |
| |
| # set cdrom 'media' and 'format', if needed |
| if utils.IsUrl(cdrom_image): |
| options = ",media=cdrom" |
| else: |
| options = ",media=cdrom,format=raw" |
| |
| # set cdrom 'if' type |
| if cdrom_boot: |
| if_val = ",if=" + constants.HT_DISK_IDE |
| elif cdrom_disk_type == constants.HT_DISK_PARAVIRTUAL: |
| if_val = ",if=virtio" |
| else: |
| if_val = ",if=" + cdrom_disk_type |
| |
| # set boot flag, if needed |
| boot_val = "" |
| if cdrom_boot: |
| kvm_cmd.extend(["-boot", "d"]) |
| |
| # whether this is an older KVM version that requires the 'boot=on' flag |
| # on devices |
| if needs_boot_flag: |
| boot_val = ",boot=on" |
| |
| # build '-drive' option |
| drive_val = "file=%s%s%s%s" % (cdrom_image, options, if_val, boot_val) |
| kvm_cmd.extend(["-drive", drive_val]) |
| |
| def _GenerateKVMRuntime(self, instance, block_devices, startup_paused, |
| kvmhelp): |
| """Generate KVM information to start an instance. |
| |
| @type kvmhelp: string |
| @param kvmhelp: output of kvm --help |
| @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} |
| |
| """ |
| # pylint: disable=R0912,R0914,R0915 |
| hvp = instance.hvparams |
| self.ValidateParameters(hvp) |
| |
| pidfile = self._InstancePidFile(instance.name) |
| kvm = hvp[constants.HV_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_MAXMEM]]) |
| |
| smp_list = ["%s" % instance.beparams[constants.BE_VCPUS]] |
| if hvp[constants.HV_CPU_CORES]: |
| smp_list.append("cores=%s" % hvp[constants.HV_CPU_CORES]) |
| if hvp[constants.HV_CPU_THREADS]: |
| smp_list.append("threads=%s" % hvp[constants.HV_CPU_THREADS]) |
| if hvp[constants.HV_CPU_SOCKETS]: |
| smp_list.append("sockets=%s" % hvp[constants.HV_CPU_SOCKETS]) |
| |
| kvm_cmd.extend(["-smp", ",".join(smp_list)]) |
| |
| kvm_cmd.extend(["-pidfile", pidfile]) |
| |
| bus_slots = self._GetBusSlots(hvp) |
| |
| # As requested by music lovers |
| if hvp[constants.HV_SOUNDHW]: |
| soundhw = hvp[constants.HV_SOUNDHW] |
| kvm_cmd.extend(["-soundhw", soundhw]) |
| |
| if hvp[constants.HV_DISK_TYPE] in constants.HT_SCSI_DEVICE_TYPES: |
| # In case a SCSI disk is given, QEMU adds a SCSI contorller |
| # (LSI Logic / Symbios Logic 53c895a) implicitly. |
| # Here, we add the controller explicitly with the default id. |
| kvm_cmd.extend([ |
| "-device", |
| "%s,id=scsi" % hvp[constants.HV_KVM_SCSI_CONTROLLER_TYPE] |
| ]) |
| |
| kvm_cmd.extend(["-balloon", "virtio"]) |
| kvm_cmd.extend(["-daemonize"]) |
| if not instance.hvparams[constants.HV_ACPI]: |
| kvm_cmd.extend(["-no-acpi"]) |
| if instance.hvparams[constants.HV_REBOOT_BEHAVIOR] == \ |
| constants.INSTANCE_REBOOT_EXIT: |
| kvm_cmd.extend(["-no-reboot"]) |
| |
| mversion = hvp[constants.HV_KVM_MACHINE_VERSION] |
| if not mversion: |
| mversion = self._GetDefaultMachineVersion(kvm) |
| if self._MACHINE_RE.search(kvmhelp): |
| # TODO (2.8): kernel_irqchip and kvm_shadow_mem machine properties, as |
| # extra hypervisor parameters. We should also investigate whether and how |
| # shadow_mem should be considered for the resource model. |
| if (hvp[constants.HV_KVM_FLAG] == constants.HT_KVM_ENABLED): |
| specprop = ",accel=kvm" |
| else: |
| specprop = "" |
| machinespec = "%s%s" % (mversion, specprop) |
| kvm_cmd.extend(["-machine", machinespec]) |
| else: |
| kvm_cmd.extend(["-M", mversion]) |
| if (hvp[constants.HV_KVM_FLAG] == constants.HT_KVM_ENABLED and |
| self._ENABLE_KVM_RE.search(kvmhelp)): |
| kvm_cmd.extend(["-enable-kvm"]) |
| elif (hvp[constants.HV_KVM_FLAG] == constants.HT_KVM_DISABLED and |
| self._DISABLE_KVM_RE.search(kvmhelp)): |
| kvm_cmd.extend(["-disable-kvm"]) |
| |
| kernel_path = hvp[constants.HV_KERNEL_PATH] |
| if kernel_path: |
| boot_cdrom = boot_floppy = boot_network = False |
| else: |
| 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 |
| |
| if startup_paused: |
| kvm_cmd.extend([_KVM_START_PAUSED_FLAG]) |
| |
| if boot_network: |
| kvm_cmd.extend(["-boot", "n"]) |
| |
| disk_type = hvp[constants.HV_DISK_TYPE] |
| |
| # 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 |
| |
| cdrom_image1 = hvp[constants.HV_CDROM_IMAGE_PATH] |
| if cdrom_image1: |
| needs_boot_flag = self._BOOT_RE.search(kvmhelp) |
| self._CdromOption(kvm_cmd, cdrom_disk_type, cdrom_image1, boot_cdrom, |
| needs_boot_flag) |
| |
| cdrom_image2 = hvp[constants.HV_KVM_CDROM2_IMAGE_PATH] |
| if cdrom_image2: |
| self._CdromOption(kvm_cmd, cdrom_disk_type, cdrom_image2, False, False) |
| |
| 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]) |
| |
| if 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], |
| hvp[constants.HV_KERNEL_ARGS]] |
| if hvp[constants.HV_SERIAL_CONSOLE]: |
| serial_speed = hvp[constants.HV_SERIAL_SPEED] |
| root_append.append("console=ttyS0,%s" % serial_speed) |
| 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 |
| |
| kvm_cmd.extend(["-usb"]) |
| |
| if mouse_type: |
| kvm_cmd.extend(["-usbdevice", mouse_type]) |
| elif vnc_bind_address: |
| kvm_cmd.extend(["-usbdevice", constants.HT_MOUSE_TABLET]) |
| |
| if vnc_bind_address: |
| if netutils.IsValidInterface(vnc_bind_address): |
| if_addresses = netutils.GetInterfaceIpAddresses(vnc_bind_address) |
| if_ip4_addresses = if_addresses[constants.IP4_VERSION] |
| if len(if_ip4_addresses) < 1: |
| logging.error("Could not determine IPv4 address of interface %s", |
| vnc_bind_address) |
| else: |
| vnc_bind_address = if_ip4_addresses[0] |
| 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 == constants.IP4_ADDRESS_ANY: |
| vnc_arg = ":%d" % (display) |
| else: |
| 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" |
| |
| # 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 = "" |
| if hvp[constants.HV_VNC_TLS]: |
| vnc_append = "%s,tls" % vnc_append |
| if hvp[constants.HV_VNC_X509_VERIFY]: |
| 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, |
| hvp[constants.HV_VNC_X509]) |
| if hvp[constants.HV_VNC_PASSWORD_FILE]: |
| vnc_append = "%s,password" % vnc_append |
| |
| vnc_arg = "%s%s" % (vnc_arg, vnc_append) |
| |
| else: |
| 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)) |
| |
| 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" % spice_address |
| if hvp[constants.HV_KVM_SPICE_USE_TLS]: |
| spice_arg = ("%s,tls-port=%s,x509-cacert-file=%s" % |
| (spice_arg, instance.network_port, |
| pathutils.SPICE_CACERT_FILE)) |
| spice_arg = ("%s,x509-key-file=%s,x509-cert-file=%s" % |
| (spice_arg, pathutils.SPICE_CERT_FILE, |
| pathutils.SPICE_CERT_FILE)) |
| tls_ciphers = hvp[constants.HV_KVM_SPICE_TLS_CIPHERS] |
| if tls_ciphers: |
| spice_arg = "%s,tls-ciphers=%s" % (spice_arg, tls_ciphers) |
| else: |
| spice_arg = "%s,port=%s" % (spice_arg, instance.network_port) |
| |
| if not hvp[constants.HV_KVM_SPICE_PASSWORD_FILE]: |
| spice_arg = "%s,disable-ticketing" % spice_arg |
| |
| if spice_ip_version: |
| spice_arg = "%s,ipv%s" % (spice_arg, spice_ip_version) |
| |
| # Image compression options |
| img_lossless = hvp[constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR] |
| img_jpeg = hvp[constants.HV_KVM_SPICE_JPEG_IMG_COMPR] |
| img_zlib_glz = hvp[constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR] |
| if img_lossless: |
| spice_arg = "%s,image-compression=%s" % (spice_arg, img_lossless) |
| if img_jpeg: |
| spice_arg = "%s,jpeg-wan-compression=%s" % (spice_arg, img_jpeg) |
| if img_zlib_glz: |
| spice_arg = "%s,zlib-glz-wan-compression=%s" % (spice_arg, img_zlib_glz) |
| |
| # Video stream detection |
| video_streaming = hvp[constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION] |
| if video_streaming: |
| spice_arg = "%s,streaming-video=%s" % (spice_arg, video_streaming) |
| |
| # Audio compression, by default in qemu-kvm it is on |
| if not hvp[constants.HV_KVM_SPICE_AUDIO_COMPR]: |
| spice_arg = "%s,playback-compression=off" % spice_arg |
| if not hvp[constants.HV_KVM_SPICE_USE_VDAGENT]: |
| spice_arg = "%s,agent-mouse=off" % spice_arg |
| else: |
| # Enable the spice agent communication channel between the host and the |
| # agent. |
| kvm_cmd.extend(["-device", "virtio-serial-pci,id=spice"]) |
| kvm_cmd.extend([ |
| "-device", |
| "virtserialport,chardev=spicechannel0,name=com.redhat.spice.0", |
| ]) |
| kvm_cmd.extend(["-chardev", "spicevmc,id=spicechannel0,name=vdagent"]) |
| |
| logging.info("KVM: SPICE will listen on port %s", instance.network_port) |
| kvm_cmd.extend(["-spice", spice_arg]) |
| |
| else: |
| # From qemu 1.4 -nographic is incompatible with -daemonize. The new way |
| # also works in earlier versions though (tested with 1.1 and 1.3) |
| if self._DISPLAY_RE.search(kvmhelp): |
| kvm_cmd.extend(["-display", "none"]) |
| else: |
| kvm_cmd.extend(["-nographic"]) |
| |
| if hvp[constants.HV_USE_LOCALTIME]: |
| kvm_cmd.extend(["-localtime"]) |
| |
| if hvp[constants.HV_KVM_USE_CHROOT]: |
| kvm_cmd.extend(["-chroot", self._InstanceChrootDir(instance.name)]) |
| |
| # Add qemu-KVM -cpu param |
| if hvp[constants.HV_CPU_TYPE]: |
| kvm_cmd.extend(["-cpu", hvp[constants.HV_CPU_TYPE]]) |
| |
| # Pass a -vga option if requested, or if spice is used, for backwards |
| # compatibility. |
| if hvp[constants.HV_VGA]: |
| kvm_cmd.extend(["-vga", hvp[constants.HV_VGA]]) |
| elif spice_bind: |
| kvm_cmd.extend(["-vga", "qxl"]) |
| |
| # Various types of usb devices, comma separated |
| if hvp[constants.HV_USB_DEVICES]: |
| for dev in hvp[constants.HV_USB_DEVICES].split(","): |
| kvm_cmd.extend(["-usbdevice", dev]) |
| |
| # Set system UUID to instance UUID |
| if self._UUID_RE.search(kvmhelp): |
| kvm_cmd.extend(["-uuid", instance.uuid]) |
| |
| if hvp[constants.HV_KVM_EXTRA]: |
| kvm_cmd.extend(hvp[constants.HV_KVM_EXTRA].split(" ")) |
| |
| def _generate_kvm_device(dev_type, dev): |
| """Helper for generating a kvm device out of a Ganeti device.""" |
| kvm_devid = _GenerateDeviceKVMId(dev_type, dev) |
| hv_dev_type = _DEVICE_TYPE[dev_type](hvp) |
| dev.hvinfo = _GenerateDeviceHVInfo(dev_type, kvm_devid, |
| hv_dev_type, bus_slots) |
| |
| kvm_disks = [] |
| for disk, link_name, uri in block_devices: |
| _generate_kvm_device(constants.HOTPLUG_TARGET_DISK, disk) |
| kvm_disks.append((disk, link_name, uri)) |
| |
| kvm_nics = [] |
| for nic in instance.nics: |
| _generate_kvm_device(constants.HOTPLUG_TARGET_NIC, nic) |
| kvm_nics.append(nic) |
| |
| hvparams = hvp |
| |
| return (kvm_cmd, kvm_nics, hvparams, kvm_disks) |
| |
| def _WriteKVMRuntime(self, instance_name, data): |
| """Write an instance's KVM runtime |
| |
| """ |
| try: |
| utils.WriteFile(self._InstanceKVMRuntime(instance_name), |
| data=data) |
| except EnvironmentError, err: |
| raise errors.HypervisorError("Failed to save KVM runtime file: %s" % err) |
| |
| def _ReadKVMRuntime(self, instance_name): |
| """Read an instance's KVM runtime |
| |
| """ |
| try: |
| file_content = utils.ReadFile(self._InstanceKVMRuntime(instance_name)) |
| except EnvironmentError, err: |
| raise errors.HypervisorError("Failed to load KVM runtime file: %s" % err) |
| return file_content |
| |
| def _SaveKVMRuntime(self, instance, kvm_runtime): |
| """Save an instance's KVM runtime |
| |
| """ |
| kvm_cmd, kvm_nics, hvparams, kvm_disks = kvm_runtime |
| |
| serialized_nics = [nic.ToDict() for nic in kvm_nics] |
| serialized_disks = [(blk.ToDict(), link, uri) |
| for blk, link, uri in kvm_disks] |
| serialized_form = serializer.Dump((kvm_cmd, serialized_nics, hvparams, |
| serialized_disks)) |
| |
| self._WriteKVMRuntime(instance.name, serialized_form) |
| |
| def _LoadKVMRuntime(self, instance, serialized_runtime=None): |
| """Load an instance's KVM runtime |
| |
| """ |
| if not serialized_runtime: |
| serialized_runtime = self._ReadKVMRuntime(instance.name) |
| |
| return _AnalyzeSerializedRuntime(serialized_runtime) |
| |
| 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) |
| |
| @staticmethod |
| def _GenerateKvmTapName(nic): |
| """Generate a TAP network interface name for a NIC. |
| |
| See L{hv_base.GenerateTapName}. |
| |
| For the case of the empty string, see L{OpenTap} |
| |
| @type nic: ganeti.objects.NIC |
| @param nic: NIC object for the name should be generated |
| |
| @rtype: string |
| @return: TAP network interface name, or the empty string if the |
| NIC is not used in instance communication |
| |
| """ |
| if nic.name is None or not \ |
| nic.name.startswith(constants.INSTANCE_COMMUNICATION_NIC_PREFIX): |
| return "" |
| |
| return hv_base.GenerateTapName() |
| |
| def _GetNetworkDeviceFeatures(self, up_hvp, devlist, kvmhelp): |
| """Get network device options to properly enable supported features. |
| |
| Return a dict of supported and enabled tap features with nic_model along |
| with the extra strings to be appended to the --netdev and --device options. |
| This function is called before opening a new tap device. |
| |
| Currently the features_dict includes the following attributes: |
| - vhost (boolean) |
| - vnet_hdr (boolean) |
| - mq (boolean, int) |
| |
| @rtype: (dict, str, str) tuple |
| @return: The supported features, |
| the string to be appended to the --netdev option, |
| the string to be appended to the --device option |
| |
| """ |
| nic_type = up_hvp[constants.HV_NIC_TYPE] |
| nic_extra_str = "" |
| tap_extra_str = "" |
| features = { |
| "vhost": False, |
| "vnet_hdr": False, |
| "mq": (False, 1) |
| } |
| update_features = {} |
| if nic_type == constants.HT_NIC_PARAVIRTUAL: |
| if self._DEVICE_DRIVER_SUPPORTED(self._VIRTIO_NET_PCI, devlist): |
| nic_model = self._VIRTIO_NET_PCI |
| update_features["vnet_hdr"] = up_hvp[constants.HV_VNET_HDR] |
| else: |
| # Older versions of kvm don't support DEVICE_LIST, but they don't |
| # have new virtio syntax either. |
| nic_model = self._VIRTIO |
| |
| if up_hvp[constants.HV_VHOST_NET]: |
| # Check for vhost_net support. |
| if self._VHOST_RE.search(kvmhelp): |
| update_features["vhost"] = True |
| tap_extra_str = ",vhost=on" |
| else: |
| raise errors.HypervisorError("vhost_net is configured" |
| " but it is not available") |
| virtio_net_queues = up_hvp.get(constants.HV_VIRTIO_NET_QUEUES, 1) |
| if virtio_net_queues > 1: |
| # Check for multiqueue virtio-net support. |
| if self._VIRTIO_NET_QUEUES_RE.search(kvmhelp): |
| # As advised at http://www.linux-kvm.org/page/Multiqueue formula |
| # for calculating vector size is: vectors=2*N+1 where N is the |
| # number of queues (HV_VIRTIO_NET_QUEUES). |
| nic_extra_str = ",mq=on,vectors=%d" % (2 * virtio_net_queues + 1) |
| update_features["mq"] = (True, virtio_net_queues) |
| else: |
| raise errors.HypervisorError("virtio_net_queues is configured" |
| " but it is not available") |
| else: |
| nic_model = nic_type |
| |
| update_features["driver"] = nic_model |
| features.update(update_features) |
| |
| return features, tap_extra_str, nic_extra_str |
| |
| # too many local variables |
| # pylint: disable=R0914 |
| def _ExecuteKVMRuntime(self, instance, kvm_runtime, kvmhelp, incoming=None): |
| """Execute a KVM cmd, after completing it with some last minute data. |
| |
| @type instance: L{objects.Instance} object |
| @param instance: the VM this command acts upon |
| @type kvm_runtime: tuple of (list of str, list of L{objects.NIC} objects, |
| dict of hypervisor options, list of tuples (L{objects.Disk}, str, str) |
| @param kvm_runtime: (kvm command, NICs of the instance, options at startup |
| of the instance, [(disk, link_name, uri)..]) |
| @type incoming: tuple of strings |
| @param incoming: (target_host_ip, port) for migration. |
| @type kvmhelp: string |
| @param kvmhelp: output of kvm --help |
| |
| """ |
| # 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) |
| |
| self._ClearUserShutdown(instance.name) |
| self._StartKvmd(instance.hvparams) |
| |
| temp_files = [] |
| |
| kvm_cmd, kvm_nics, up_hvp, kvm_disks = kvm_runtime |
| # the first element of kvm_cmd is always the path to the kvm binary |
| kvm_path = kvm_cmd[0] |
| up_hvp = objects.FillDict(conf_hvp, up_hvp) |
| |
| # 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 = [] |
| devlist = self._GetKVMOutput(kvm_path, self._KVMOPT_DEVICELIST) |
| if not kvm_nics: |
| kvm_cmd.extend(["-net", "none"]) |
| else: |
| features, tap_extra, nic_extra = \ |
| self._GetNetworkDeviceFeatures(up_hvp, devlist, kvmhelp) |
| nic_model = features["driver"] |
| kvm_supports_netdev = self._NETDEV_RE.search(kvmhelp) |
| for nic_seq, nic in enumerate(kvm_nics): |
| tapname, nic_tapfds, nic_vhostfds = \ |
| OpenTap(features=features, name=self._GenerateKvmTapName(nic)) |
| |
| tapfds.extend(nic_tapfds) |
| tapfds.extend(nic_vhostfds) |
| taps.append(tapname) |
| tapfd = "%s%s" % ("fds=" if len(nic_tapfds) > 1 else "fd=", |
| ":".join(str(fd) for fd in nic_tapfds)) |
| |
| if nic_vhostfds: |
| vhostfd = "%s%s" % (",vhostfds=" |
| if len(nic_vhostfds) > 1 else ",vhostfd=", |
| ":".join(str(fd) for fd in nic_vhostfds)) |
| else: |
| vhostfd = "" |
| |
| if kvm_supports_netdev: |
| # Non paravirtual NICs hvinfo is empty |
| if "id" in nic.hvinfo: |
| nic_val = _GenerateDeviceHVInfoStr(nic.hvinfo) |
| netdev = nic.hvinfo["id"] |
| else: |
| nic_val = "%s" % nic_model |
| netdev = "netdev%d" % nic_seq |
| nic_val += (",netdev=%s,mac=%s%s" % (netdev, nic.mac, nic_extra)) |
| tap_val = ("type=tap,id=%s,%s%s%s" % |
| (netdev, tapfd, vhostfd, 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,%s" % (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)]) |
| |
| # 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.ReadOneLineFile(vnc_pwd_file, strict=True) |
| except EnvironmentError, err: |
| raise errors.HypervisorError("Failed to open VNC password file %s: %s" |
| % (vnc_pwd_file, err)) |
| |
| if conf_hvp[constants.HV_KVM_USE_CHROOT]: |
| utils.EnsureDirs([(self._InstanceChrootDir(name), |
| constants.SECURE_DIR_MODE)]) |
| |
| # Automatically enable QMP if version is >= 0.14 |
| if self._QMP_RE.search(kvmhelp): |
| logging.debug("Enabling QMP") |
| kvm_cmd.extend(["-qmp", "unix:%s,server,nowait" % |
| self._InstanceQmpMonitor(instance.name)]) |
| # Add a second monitor for kvmd |
| kvm_cmd.extend(["-qmp", "unix:%s,server,nowait" % |
| self._InstanceKvmdMonitor(instance.name)]) |
| |
| # Configure the network now for starting instances and bridged/OVS |
| # 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_ROUTED): |
| continue |
| self._ConfigureNIC(instance, nic_seq, nic, taps[nic_seq]) |
| |
| bdev_opts = self._GenerateKVMBlockDevicesOptions(up_hvp, |
| kvm_disks, |
| kvmhelp, |
| devlist) |
| kvm_cmd.extend(bdev_opts) |
| # CPU affinity requires kvm to start paused, so we set this flag if the |
| # instance is not already paused and if we are not going to accept a |
| # migrating instance. In the latter case, pausing is not needed. |
| start_kvm_paused = not (_KVM_START_PAUSED_FLAG in kvm_cmd) and not incoming |
| if start_kvm_paused: |
| kvm_cmd.extend([_KVM_START_PAUSED_FLAG]) |
| |
| # Note: CPU pinning is using up_hvp since changes take effect |
| # during instance startup anyway, and to avoid problems when soft |
| # rebooting the instance. |
| cpu_pinning = False |
| if up_hvp.get(constants.HV_CPU_MASK, None): |
| cpu_pinning = True |
| |
| 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 |
| self._CallMonitorCommand(instance.name, change_cmd) |
| |
| # Setting SPICE password. We are not vulnerable to malicious passwordless |
| # connection attempts because SPICE by default does not allow connections |
| # if neither a password nor the "disable_ticketing" options are specified. |
| # As soon as we send the password via QMP, that password is a valid ticket |
| # for connection. |
| spice_password_file = conf_hvp[constants.HV_KVM_SPICE_PASSWORD_FILE] |
| if spice_password_file: |
| spice_pwd = "" |
| try: |
| spice_pwd = utils.ReadOneLineFile(spice_password_file, strict=True) |
| except EnvironmentError, err: |
| raise errors.HypervisorError("Failed to open SPICE password file %s: %s" |
| % (spice_password_file, err)) |
| |
| qmp = QmpConnection(self._InstanceQmpMonitor(instance.name)) |
| qmp.connect() |
| arguments = { |
| "protocol": "spice", |
| "password": spice_pwd, |
| } |
| qmp.Execute("set_password", arguments) |
| |
| for filename in temp_files: |
| utils.RemoveFile(filename) |
| |
| # If requested, set CPU affinity and resume instance execution |
| if cpu_pinning: |
| self._ExecuteCpuAffinity(instance.name, up_hvp[constants.HV_CPU_MASK]) |
| |
| start_memory = self._InstanceStartupMemory(instance) |
| if start_memory < instance.beparams[constants.BE_MAXMEM]: |
| self.BalloonInstanceMemory(instance, start_memory) |
| |
| if start_kvm_paused: |
| # To control CPU pinning, ballooning, and vnc/spice passwords |
| # the VM was started in a frozen state. If freezing was not |
| # explicitly requested resume the vm status. |
| self._CallMonitorCommand(instance.name, self._CONT_CMD) |
| |
| @staticmethod |
| def _StartKvmd(hvparams): |
| """Ensure that the Kvm daemon is running. |
| |
| @type hvparams: dict of strings |
| @param hvparams: hypervisor parameters |
| |
| """ |
| if hvparams is None \ |
| or not hvparams[constants.HV_KVM_USER_SHUTDOWN] \ |
| or utils.IsDaemonAlive(constants.KVMD): |
| return |
| |
| result = utils.RunCmd([pathutils.DAEMON_UTIL, "start", constants.KVMD]) |
| |
| if result.failed: |
| raise errors.HypervisorError("Failed to start KVM daemon") |
| |
| def StartInstance(self, instance, block_devices, startup_paused): |
| """Start an instance. |
| |
| """ |
| self._CheckDown(instance.name) |
| kvmpath = instance.hvparams[constants.HV_KVM_PATH] |
| kvmhelp = self._GetKVMOutput(kvmpath, self._KVMOPT_HELP) |
| kvm_runtime = self._GenerateKVMRuntime(instance, block_devices, |
| startup_paused, kvmhelp) |
| self._SaveKVMRuntime(instance, kvm_runtime) |
| self._ExecuteKVMRuntime(instance, kvm_runtime, kvmhelp) |
| |
| @classmethod |
| def _CallMonitorCommand(cls, instance_name, command, timeout=None): |
| """Invoke a command on the instance monitor. |
| |
| """ |
| if timeout is not None: |
| timeout_cmd = "timeout %s" % (timeout, ) |
| else: |
| timeout_cmd = "" |
| |
| # TODO: Replace monitor calls with QMP once KVM >= 0.14 is the minimum |
| # version. The monitor protocol is designed for human consumption, whereas |
| # QMP is made for programmatic usage. In the worst case QMP can also |
| # execute monitor commands. As it is, all calls to socat take at least |
| # 500ms and likely more: socat can't detect the end of the reply and waits |
| # for 500ms of no data received before exiting (500 ms is the default for |
| # the "-t" parameter). |
| socat = ("echo %s | %s %s STDIO UNIX-CONNECT:%s" % |
| (utils.ShellQuote(command), |
| timeout_cmd, |
| constants.SOCAT_PATH, |
| utils.ShellQuote(cls._InstanceMonitor(instance_name)))) |
| result = utils.RunCmd(socat) |
| if result.failed: |
| msg = ("Failed to send command '%s' to instance '%s', reason '%s'," |
| " output: %s" % |
| (command, instance_name, result.fail_reason, result.output)) |
| raise errors.HypervisorError(msg) |
| |
| return result |
| |
| @_with_qmp |
| def VerifyHotplugSupport(self, instance, action, dev_type): |
| """Verifies that hotplug is supported. |
| |
| Hotplug is not supported if: |
| |
| - the instance is not running |
| - the device type is not hotplug-able |
| - the QMP version does not support the corresponding commands |
| |
| @raise errors.HypervisorError: if one of the above applies |
| |
| """ |
| runtime = self._LoadKVMRuntime(instance) |
| device_type = _DEVICE_TYPE[dev_type](runtime[2]) |
| if device_type not in _HOTPLUGGABLE_DEVICE_TYPES[dev_type]: |
| msg = "Hotplug is not supported for device type %s" % device_type |
| raise errors.HypervisorError(msg) |
| |
| if dev_type == constants.HOTPLUG_TARGET_DISK: |
| if action == constants.HOTPLUG_ACTION_ADD: |
| self.qmp.CheckDiskHotAddSupport() |
| if dev_type == constants.HOTPLUG_TARGET_NIC: |
| if action == constants.HOTPLUG_ACTION_ADD: |
| self.qmp.CheckNicHotAddSupport() |
| |
| def HotplugSupported(self, instance): |
| """Checks if hotplug is generally supported. |
| |
| Hotplug is *not* supported in case of: |
| - qemu versions < 1.7 (where all qmp related commands are supported) |
| - for stopped instances |
| |
| @raise errors.HypervisorError: in one of the previous cases |
| |
| """ |
| try: |
| output = self._CallMonitorCommand(instance.name, self._INFO_VERSION_CMD) |
| except errors.HypervisorError: |
| raise errors.HotplugError("Instance is probably down") |
| |
| match = self._INFO_VERSION_RE.search(output.stdout) |
| if not match: |
| raise errors.HotplugError("Cannot parse qemu version via monitor") |
| |
| #TODO: delegate more fine-grained checks to VerifyHotplugSupport |
| v_major, v_min, _, _ = match.groups() |
| if (int(v_major), int(v_min)) < (1, 7): |
| raise errors.HotplugError("Hotplug not supported for qemu versions < 1.7") |
| |
| def _GetBusSlots(self, hvp=None, runtime=None): |
| """Helper function to get the slots of PCI and SCSI QEMU buses. |
| |
| This will return the status of the first PCI and SCSI buses. By default |
| QEMU boots with one PCI bus (pci.0) and occupies the first 3 PCI slots. If |
| a SCSI disk is found then a SCSI controller is added on the PCI bus and a |
| SCSI bus (scsi.0) is created. |
| |
| During hotplug we could query QEMU via info qtree HMP command but parsing |
| the result is too complicated. Instead we use the info stored in runtime |
| files. We parse NIC and disk entries and based on their hvinfo we reserve |
| the corresponding slots. |
| |
| The runtime argument is a tuple as returned by _LoadKVMRuntime(). Obtain |
| disks and NICs from it. In case a runtime file is not available (see |
| _GenerateKVMRuntime()) we return the bus slots that QEMU boots with by |
| default. |
| |
| """ |
| # This is by default and returned during _GenerateKVMRuntime() |
| bus_slots = { |
| _PCI_BUS: bitarray(self._DEFAULT_PCI_RESERVATIONS), |
| _SCSI_BUS: bitarray(self._DEFAULT_SCSI_RESERVATIONS), |
| } |
| |
| # Adjust the empty slots depending of the corresponding hvparam |
| if hvp and constants.HV_KVM_PCI_RESERVATIONS in hvp: |
| res = hvp[constants.HV_KVM_PCI_RESERVATIONS] |
| pci = bitarray(constants.QEMU_PCI_SLOTS) |
| pci.setall(False) # pylint: disable=E1101 |
| pci[0:res:1] = True |
| bus_slots[_PCI_BUS] = pci |
| |
| # This is during hot-add |
| if runtime: |
| _, nics, _, disks = runtime |
| disks = [d for d, _, _ in disks] |
| for d in disks + nics: |
| if not d.hvinfo or "bus" not in d.hvinfo: |
| continue |
| bus = d.hvinfo["bus"] |
| slots = bus_slots[bus] |
| if bus == _PCI_BUS: |
| slot = d.hvinfo["addr"] |
| slots[int(slot, 16)] = True |
| elif bus == _SCSI_BUS: |
| slot = d.hvinfo["scsi-id"] |
| slots[slot] = True |
| |
| return bus_slots |
| |
| @_with_qmp |
| def _VerifyHotplugCommand(self, _instance, kvm_devid, should_exist): |
| """Checks if a previous hotplug command has succeeded. |
| |
| Depending on the should_exist value, verifies that an entry identified by |
| device ID is present or not. |
| |
| @raise errors.HypervisorError: if result is not the expected one |
| |
| """ |
| for i in range(5): |
| found = self.qmp.HasDevice(kvm_devid) |
| logging.info("Verifying hotplug command (retry %s): %s", i, found) |
| if found and should_exist: |
| break |
| if not found and not should_exist: |
| break |
| time.sleep(1) |
| |
| if found and not should_exist: |
| msg = "Device %s should have been removed but is still there" % kvm_devid |
| raise errors.HypervisorError(msg) |
| |
| if not found and should_exist: |
| msg = "Device %s should have been added but is missing" % kvm_devid |
| raise errors.HypervisorError(msg) |
| |
| logging.info("Device %s has been correctly hot-plugged", kvm_devid) |
| |
| @_with_qmp |
| def HotAddDevice(self, instance, dev_type, device, extra, seq): |
| """ Helper method to hot-add a new device |
| |
| It generates the device ID and hvinfo, and invokes the |
| device-specific method. |
| |
| """ |
| kvm_devid = _GenerateDeviceKVMId(dev_type, device) |
| runtime = self._LoadKVMRuntime(instance) |
| up_hvp = runtime[2] |
| device_type = _DEVICE_TYPE[dev_type](up_hvp) |
| bus_state = self._GetBusSlots(up_hvp, runtime) |
| # in case of hot-mod this is given |
| if not device.hvinfo: |
| device.hvinfo = _GenerateDeviceHVInfo(dev_type, kvm_devid, |
| device_type, bus_state) |
| if dev_type == constants.HOTPLUG_TARGET_DISK: |
| uri = _GetDriveURI(device, extra[0], extra[1]) |
| |
| def drive_add_fn(filename): |
| """Helper function that uses HMP to hot-add a drive.""" |
| cmd = "drive_add dummy file=%s,if=none,id=%s,format=raw" % \ |
| (filename, kvm_devid) |
| self._CallMonitorCommand(instance.name, cmd) |
| |
| # This must be done indirectly due to the fact that we pass the drive's |
| # file descriptor via QMP first, then we add the corresponding drive that |
| # refers to this fd. Note that if the QMP connection terminates before |
| # a drive which keeps a reference to the fd passed via the add-fd QMP |
| # command has been created, then the fd gets closed and cannot be used |
| # later (e.g., via an drive_add HMP command). |
| self.qmp.HotAddDisk(device, kvm_devid, uri, drive_add_fn) |
| elif dev_type == constants.HOTPLUG_TARGET_NIC: |
| kvmpath = instance.hvparams[constants.HV_KVM_PATH] |
| kvmhelp = self._GetKVMOutput(kvmpath, self._KVMOPT_HELP) |
| devlist = self._GetKVMOutput(kvmpath, self._KVMOPT_DEVICELIST) |
| features, _, _ = self._GetNetworkDeviceFeatures(up_hvp, devlist, kvmhelp) |
| (tap, tapfds, vhostfds) = OpenTap(features=features) |
| self._ConfigureNIC(instance, seq, device, tap) |
| self.qmp.HotAddNic(device, kvm_devid, tapfds, vhostfds, features) |
| utils.WriteFile(self._InstanceNICFile(instance.name, seq), data=tap) |
| |
| self._VerifyHotplugCommand(instance, kvm_devid, True) |
| # update relevant entries in runtime file |
| index = _DEVICE_RUNTIME_INDEX[dev_type] |
| entry = _RUNTIME_ENTRY[dev_type](device, extra) |
| runtime[index].append(entry) |
| self._SaveKVMRuntime(instance, runtime) |
| |
| @_with_qmp |
| def HotDelDevice(self, instance, dev_type, device, _, seq): |
| """ Helper method for hot-del device |
| |
| It gets device info from runtime file, generates the device name and |
| invokes the device-specific method. |
| |
| """ |
| runtime = self._LoadKVMRuntime(instance) |
| entry = _GetExistingDeviceInfo(dev_type, device, runtime) |
| kvm_device = _RUNTIME_DEVICE[dev_type](entry) |
| kvm_devid = _GenerateDeviceKVMId(dev_type, kvm_device) |
| if dev_type == constants.HOTPLUG_TARGET_DISK: |
| self.qmp.HotDelDisk(kvm_devid) |
| # drive_del is not implemented yet in qmp |
| command = "drive_del %s\n" % kvm_devid |
| self._CallMonitorCommand(instance.name, command) |
| elif dev_type == constants.HOTPLUG_TARGET_NIC: |
| self.qmp.HotDelNic(kvm_devid) |
| utils.RemoveFile(self._InstanceNICFile(instance.name, seq)) |
| self._VerifyHotplugCommand(instance, kvm_devid, False) |
| index = _DEVICE_RUNTIME_INDEX[dev_type] |
| runtime[index].remove(entry) |
| self._SaveKVMRuntime(instance, runtime) |
| |
| return kvm_device.hvinfo |
| |
| def HotModDevice(self, instance, dev_type, device, _, seq): |
| """ Helper method for hot-mod device |
| |
| It gets device info from runtime file, generates the device name and |
| invokes the device-specific method. Currently only NICs support hot-mod |
| |
| """ |
| if dev_type == constants.HOTPLUG_TARGET_NIC: |
| # putting it back in the same bus and slot |
| device.hvinfo = self.HotDelDevice(instance, dev_type, device, _, seq) |
| self.HotAddDevice(instance, dev_type, device, _, seq) |
| |
| @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 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 _GetKVMOutput(cls, kvm_path, option): |
| """Return the output of a kvm invocation |
| |
| @type kvm_path: string |
| @param kvm_path: path to the kvm executable |
| @type option: a key of _KVMOPTS_CMDS |
| @param option: kvm option to fetch the output from |
| @return: output a supported kvm invocation |
| @raise errors.HypervisorError: when the KVM help output cannot be retrieved |
| |
| """ |
| assert option in cls._KVMOPTS_CMDS, "Invalid output option" |
| |
| optlist, can_fail = cls._KVMOPTS_CMDS[option] |
| |
| result = utils.RunCmd([kvm_path] + optlist) |
| if result.failed and not can_fail: |
| raise errors.HypervisorError("Unable to get KVM %s output" % |
| " ".join(optlist)) |
| return result.output |
| |
| @classmethod |
| def _GetKVMVersion(cls, kvm_path): |
| """Return the installed KVM version. |
| |
| @return: (version, v_maj, v_min, v_rev) |
| @raise errors.HypervisorError: when the KVM version cannot be retrieved |
| |
| """ |
| return cls._ParseKVMVersion(cls._GetKVMOutput(kvm_path, cls._KVMOPT_HELP)) |
| |
| @classmethod |
| def _GetDefaultMachineVersion(cls, kvm_path): |
| """Return the default hardware revision (e.g. pc-1.1) |
| |
| """ |
| output = cls._GetKVMOutput(kvm_path, cls._KVMOPT_MLIST) |
| match = cls._DEFAULT_MACHINE_VERSION_RE.search(output) |
| if match: |
| return match.group(1) |
| else: |
| return "pc" |
| |
| @classmethod |
| def _StopInstance(cls, instance, force=False, name=None, timeout=None): |
| """Stop an instance. |
| |
| """ |
| assert(timeout is None or force is not None) |
| |
| if name is not None and not force: |
| raise errors.HypervisorError("Cannot shutdown cleanly by name only") |
| if name is None: |
| name = instance.name |
| acpi = instance.hvparams[constants.HV_ACPI] |
| else: |
| acpi = False |
| _, pid, alive = cls._InstancePidAlive(name) |
| if pid > 0 and alive: |
| if force or not acpi: |
| utils.KillProcess(pid) |
| else: |
| cls._CallMonitorCommand(name, "system_powerdown", timeout) |
| cls._ClearUserShutdown(instance.name) |
| |
| def StopInstance(self, instance, force=False, retry=False, name=None, |
| timeout=None): |
| """Stop an instance. |
| |
| """ |
| self._StopInstance(instance, force, name=name, timeout=timeout) |
| |
| 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) |
| self._ClearUserShutdown(instance_name) |
| |
| def RebootInstance(self, instance): |
| """Reboot an instance. |
| |
| """ |
| # For some reason if we do a 'send-key ctrl-alt-delete' to the control |
| # socket the instance will stop, but now power up again. So we'll resort |
| # to shutdown and restart. |
| _, _, alive = self._InstancePidAlive(instance.name) |
| if not alive: |
| raise errors.HypervisorError("Failed to reboot instance %s:" |
| " not running" % instance.name) |
| # StopInstance will delete the saved KVM runtime so: |
| # ...first load it... |
| kvm_runtime = self._LoadKVMRuntime(instance) |
| # ...now we can safely call StopInstance... |
| if not self.StopInstance(instance): |
| self.StopInstance(instance, force=True) |
| # ...and finally we can save it again, and execute it... |
| self._SaveKVMRuntime(instance, kvm_runtime) |
| kvmpath = instance.hvparams[constants.HV_KVM_PATH] |
| kvmhelp = self._GetKVMOutput(kvmpath, self._KVMOPT_HELP) |
| self._ExecuteKVMRuntime(instance, kvm_runtime, kvmhelp) |
| |
| def MigrationInfo(self, instance): |
| """Get instance information to perform a migration. |
| |
| @type instance: L{objects.Instance} |
| @param instance: instance to be migrated |
| @rtype: string |
| @return: content of the KVM runtime file |
| |
| """ |
| return self._ReadKVMRuntime(instance.name) |
| |
| def AcceptInstance(self, instance, info, target): |
| """Prepare to accept an instance. |
| |
| @type instance: L{objects.Instance} |
| @param instance: instance to be accepted |
| @type info: string |
| @param info: content of the KVM runtime file on the source node |
| @type target: string |
| @param target: target host (usually ip), on this node |
| |
| """ |
| kvm_runtime = self._LoadKVMRuntime(instance, serialized_runtime=info) |
| incoming_address = (target, instance.hvparams[constants.HV_MIGRATION_PORT]) |
| kvmpath = instance.hvparams[constants.HV_KVM_PATH] |
| kvmhelp = self._GetKVMOutput(kvmpath, self._KVMOPT_HELP) |
| self._ExecuteKVMRuntime(instance, kvm_runtime, kvmhelp, |
| incoming=incoming_address) |
| |
| def FinalizeMigrationDst(self, instance, info, success): |
| """Finalize the instance migration on the target node. |
| |
| Stop the incoming mode KVM. |
| |
| @type instance: L{objects.Instance} |
| @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_ROUTED: |
| # Bridged/OVS 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) |
| |
| def MigrateInstance(self, cluster_name, instance, target, live): |
| """Migrate an instance to a target node. |
| |
| The migration will not be attempted if the instance is not |
| currently running. |
| |
| @type cluster_name: string |
| @param cluster_name: name of the cluster |
| @type instance: L{objects.Instance} |
| @param instance: the instance to be migrated |
| @type target: string |
| @param target: ip address of the target node |
| @type live: boolean |
| @param live: perform a live migration |
| |
| """ |
| instance_name = instance.name |
| port = instance.hvparams[constants.HV_MIGRATION_PORT] |
| _, _, alive = self._InstancePidAlive(instance_name) |
| if not alive: |
| raise errors.HypervisorError("Instance not running, cannot migrate") |
| |
| if not live: |
| 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_set_downtime %dms" % |
| instance.hvparams[constants.HV_MIGRATION_DOWNTIME]) |
| self._CallMonitorCommand(instance_name, migrate_command) |
| |
| migration_caps = instance.hvparams[constants.HV_KVM_MIGRATION_CAPS] |
| if migration_caps: |
| for c in migration_caps.split(_MIGRATION_CAPS_DELIM): |
| migrate_command = ("migrate_set_capability %s on" % c) |
| self._CallMonitorCommand(instance_name, migrate_command) |
| |
| migrate_command = "migrate -d tcp:%s:%s" % (target, port) |
| self._CallMonitorCommand(instance_name, migrate_command) |
| |
| def FinalizeMigrationSource(self, instance, success, live): |
| """Finalize the instance migration on the source node. |
| |
| @type instance: L{objects.Instance} |
| @param instance: the instance that was migrated |
| @type success: bool |
| @param success: whether the migration succeeded or not |
| @type live: bool |
| @param live: whether the user requested a live migration or not |
| |
| """ |
| if success: |
| pidfile, pid, _ = self._InstancePidAlive(instance.name) |
| utils.KillProcess(pid) |
| self._RemoveInstanceRuntimeFiles(pidfile, instance.name) |
| elif live: |
| self._CallMonitorCommand(instance.name, self._CONT_CMD) |
| self._ClearUserShutdown(instance.name) |
| |
| def GetMigrationStatus(self, instance): |
| """Get the migration status |
| |
| @type instance: L{objects.Instance} |
| @param instance: the instance that is being migrated |
| @rtype: L{objects.MigrationStatus} |
| @return: the status of the current migration (one of |
| L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional |
| progress info that can be retrieved from the hypervisor |
| |
| """ |
| info_command = "info migrate" |
| for _ in range(self._MIGRATION_INFO_MAX_BAD_ANSWERS): |
| result = self._CallMonitorCommand(instance.name, info_command) |
| match = self._MIGRATION_STATUS_RE.search(result.stdout) |
| if not match: |
| if not result.stdout: |
| logging.info("KVM: empty 'info migrate' result") |
| else: |
| logging.warning("KVM: unknown 'info migrate' result: %s", |
| result.stdout) |
| else: |
| status = match.group(1) |
| if status in constants.HV_KVM_MIGRATION_VALID_STATUSES: |
| migration_status = objects.MigrationStatus(status=status) |
| match = self._MIGRATION_PROGRESS_RE.search(result.stdout) |
| if match: |
| migration_status.transferred_ram = match.group("transferred") |
| migration_status.total_ram = match.group("total") |
| |
| return migration_status |
| |
| logging.warning("KVM: unknown migration status '%s'", status) |
| |
| time.sleep(self._MIGRATION_INFO_RETRY_DELAY) |
| |
| return objects.MigrationStatus(status=constants.HV_MIGRATION_FAILED) |
| |
| def BalloonInstanceMemory(self, instance, mem): |
| """Balloon an instance memory to a certain value. |
| |
| @type instance: L{objects.Instance} |
| @param instance: instance to be accepted |
| @type mem: int |
| @param mem: actual memory size to use for instance runtime |
| |
| """ |
| self._CallMonitorCommand(instance.name, "balloon %d" % mem) |
| |
| def GetNodeInfo(self, hvparams=None): |
| """Return information about the node. |
| |
| @type hvparams: dict of strings |
| @param hvparams: hypervisor parameters, not used in this class |
| |
| @return: a dict as returned by L{BaseHypervisor.GetLinuxNodeInfo} plus |
| the following keys: |
| - hv_version: the hypervisor version in the form (major, minor, |
| revision) |
| |
| """ |
| result = self.GetLinuxNodeInfo() |
| kvmpath = constants.KVM_PATH |
| if hvparams is not None: |
| kvmpath = hvparams.get(constants.HV_KVM_PATH, constants.KVM_PATH) |
| _, v_major, v_min, v_rev = self._GetKVMVersion(kvmpath) |
| result[constants.HV_NODEINFO_KEY_VERSION] = (v_major, v_min, v_rev) |
| return result |
| |
| @classmethod |
| def GetInstanceConsole(cls, instance, primary_node, node_group, |
| hvparams, beparams): |
| """Return a command for connecting to the console of an instance. |
| |
| """ |
| if hvparams[constants.HV_SERIAL_CONSOLE]: |
| cmd = [pathutils.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)] |
| ndparams = node_group.FillND(primary_node) |
| return objects.InstanceConsole(instance=instance.name, |
| kind=constants.CONS_SSH, |
| host=primary_node.name, |
| port=ndparams.get(constants.ND_SSH_PORT), |
| user=constants.SSH_CONSOLE_USER, |
| command=cmd) |
| |
| vnc_bind_address = hvparams[constants.HV_VNC_BIND_ADDRESS] |
| 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) |
| |
| spice_bind = hvparams[constants.HV_KVM_SPICE_BIND] |
| if spice_bind: |
| return objects.InstanceConsole(instance=instance.name, |
| kind=constants.CONS_SPICE, |
| host=spice_bind, |
| port=instance.network_port) |
| |
| return objects.InstanceConsole(instance=instance.name, |
| kind=constants.CONS_MESSAGE, |
| message=("No serial shell for instance %s" % |
| instance.name)) |
| |
| def Verify(self, hvparams=None): |
| """Verify the hypervisor. |
| |
| Check that the required binaries exist. |
| |
| @type hvparams: dict of strings |
| @param hvparams: hypervisor parameters to be verified against, not used here |
| |
| @return: Problem description if something is wrong, C{None} otherwise |
| |
| """ |
| msgs = [] |
| kvmpath = constants.KVM_PATH |
| if hvparams is not None: |
| kvmpath = hvparams.get(constants.HV_KVM_PATH, constants.KVM_PATH) |
| if not os.path.exists(kvmpath): |
| msgs.append("The KVM binary ('%s') does not exist" % kvmpath) |
| if not os.path.exists(constants.SOCAT_PATH): |
| msgs.append("The socat binary ('%s') does not exist" % |
| constants.SOCAT_PATH) |
| |
| return self._FormatVerifyResults(msgs) |
| |
| @classmethod |
| def CheckParameterSyntax(cls, hvparams): |
| """Check the given parameters for validity. |
| |
| @type hvparams: dict of strings |
| @param hvparams: hypervisor parameters |
| @raise errors.HypervisorError: when a parameter is not valid |
| |
| """ |
| super(KVMHypervisor, cls).CheckParameterSyntax(hvparams) |
| |
| kernel_path = hvparams[constants.HV_KERNEL_PATH] |
| if kernel_path: |
| if not hvparams[constants.HV_ROOT_PATH]: |
| raise errors.HypervisorError("Need a root partition for the instance," |
| " if a kernel is defined") |
| |
| if (hvparams[constants.HV_VNC_X509_VERIFY] and |
| not hvparams[constants.HV_VNC_X509]): |
| raise errors.HypervisorError("%s must be defined, if %s is" % |
| (constants.HV_VNC_X509, |
| constants.HV_VNC_X509_VERIFY)) |
| |
| if hvparams[constants.HV_SERIAL_CONSOLE]: |
| serial_speed = hvparams[constants.HV_SERIAL_SPEED] |
| valid_speeds = constants.VALID_SERIAL_SPEEDS |
| if not serial_speed or serial_speed not in valid_speeds: |
| raise errors.HypervisorError("Invalid serial console speed, must be" |
| " one of: %s" % |
| utils.CommaJoin(valid_speeds)) |
| |
| boot_order = hvparams[constants.HV_BOOT_ORDER] |
| if (boot_order == constants.HT_BO_CDROM and |
| not hvparams[constants.HV_CDROM_IMAGE_PATH]): |
| raise errors.HypervisorError("Cannot boot from cdrom without an" |
| " ISO path") |
| |
| security_model = hvparams[constants.HV_SECURITY_MODEL] |
| if security_model == constants.HT_SM_USER: |
| if not hvparams[constants.HV_SECURITY_DOMAIN]: |
| raise errors.HypervisorError("A security domain (user to run kvm as)" |
| " must be specified") |
| elif (security_model == constants.HT_SM_NONE or |
| security_model == constants.HT_SM_POOL): |
| if hvparams[constants.HV_SECURITY_DOMAIN]: |
| raise errors.HypervisorError("Cannot have a security domain when the" |
| " security model is 'none' or 'pool'") |
| |
| spice_bind = hvparams[constants.HV_KVM_SPICE_BIND] |
| spice_ip_version = hvparams[constants.HV_KVM_SPICE_IP_VERSION] |
| if spice_bind: |
| 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)) |
| else: |
| # All the other SPICE parameters depend on spice_bind being set. Raise an |
| # error if any of them is set without it. |
| for param in _SPICE_ADDITIONAL_PARAMS: |
| if hvparams[param]: |
| raise errors.HypervisorError("SPICE: %s requires %s to be set" % |
| (param, constants.HV_KVM_SPICE_BIND)) |
| |
| @classmethod |
| def ValidateParameters(cls, hvparams): |
| """Check the given parameters for validity. |
| |
| @type hvparams: dict of strings |
| @param hvparams: hypervisor parameters |
| @raise errors.HypervisorError: when a parameter is not valid |
| |
| """ |
| super(KVMHypervisor, cls).ValidateParameters(hvparams) |
| |
| kvm_path = hvparams[constants.HV_KVM_PATH] |
| |
| security_model = hvparams[constants.HV_SECURITY_MODEL] |
| if security_model == constants.HT_SM_USER: |
| username = hvparams[constants.HV_SECURITY_DOMAIN] |
| try: |
| pwd.getpwnam(username) |
| except KeyError: |
| raise errors.HypervisorError("Unknown security domain user %s" |
| % username) |
| vnc_bind_address = hvparams[constants.HV_VNC_BIND_ADDRESS] |
| if vnc_bind_address: |
| bound_to_addr = netutils.IP4Address.IsValid(vnc_bind_address) |
| is_interface = netutils.IsValidInterface(vnc_bind_address) |
| is_path = utils.IsNormAbsPath(vnc_bind_address) |
| if not bound_to_addr and not is_interface and not is_path: |
| raise errors.HypervisorError("VNC: The %s parameter must be either" |
| " a valid IP address, an interface name," |
| " or an absolute path" % |
| constants.HV_KVM_SPICE_BIND) |
| |
| 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") |
| |
| # check that KVM supports SPICE |
| kvmhelp = cls._GetKVMOutput(kvm_path, cls._KVMOPT_HELP) |
| if not cls._SPICE_RE.search(kvmhelp): |
| raise errors.HypervisorError("SPICE is configured, but it is not" |
| " supported according to 'kvm --help'") |
| |
| # 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) |
| |
| machine_version = hvparams[constants.HV_KVM_MACHINE_VERSION] |
| if machine_version: |
| output = cls._GetKVMOutput(kvm_path, cls._KVMOPT_MLIST) |
| if not cls._CHECK_MACHINE_VERSION_RE(machine_version).search(output): |
| raise errors.HypervisorError("Unsupported machine version: %s" % |
| machine_version) |
| |
| @classmethod |
| def PowercycleNode(cls, hvparams=None): |
| """KVM powercycle, just a wrapper over Linux powercycle. |
| |
| @type hvparams: dict of strings |
| @param hvparams: hypervisor parameters to be used on this node |
| |
| """ |
| cls.LinuxPowercycle() |