from ganeti.utils import wrapper as utils_wrapper
-_KVM_NETWORK_SCRIPT = pathutils.CONF_DIR + "/kvm-vif-bridge"
+_KVM_NETWORK_SCRIPT = pathutils.CONF_DIR + "/kvm-ifup-custom"
_KVM_START_PAUSED_FLAG = "-S"
# TUN/TAP driver constants, taken from <linux/if_tun.h>
}
-def _GenerateDeviceKVMId(dev_type, dev):
+def _GenerateDeviceKVMId(dev_type, dev, idx=None):
"""Helper function to generate a unique device name used by KVM
QEMU monitor commands use names to identify devices. Here we use their pci
"""
- if not dev.pci:
- raise errors.HotplugError("Hotplug is not supported for %s with UUID %s" %
- (dev_type, dev.uuid))
+ # proper device id - available in latest Ganeti versions
+ if dev.pci and dev.uuid:
+ return "%s-%s-pci-%d" % (dev_type.lower(), dev.uuid.split("-")[0], dev.pci)
- return "%s-%s-pci-%d" % (dev_type.lower(), dev.uuid.split("-")[0], dev.pci)
+ # dummy device id - returned only to _GenerateKVMBlockDevicesOptions
+ # This enables -device option for paravirtual disk_type
+ if idx is not None:
+ return "%s-%d" % (dev_type.lower(), idx)
+
+ raise errors.HotplugError("Hotplug is not supported for devices"
+ " without UUID or PCI info")
def _UpdatePCISlots(dev, pci_reservations):
return found[0]
-def _AnalyzeSerializedRuntime(serialized_runtime):
- """Return runtime entries for a serialized runtime file
+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, nics, hvparams, bdevs)
- @rtype: list
+ @return: (cmd, nic dicts, hvparams, bdev dicts)
+ @rtype: tuple
"""
loaded_runtime = serializer.Load(serialized_runtime)
- if len(loaded_runtime) == 3:
- serialized_disks = []
- kvm_cmd, serialized_nics, hvparams = loaded_runtime
+ kvm_cmd, serialized_nics, hvparams = loaded_runtime[:3]
+ if len(loaded_runtime) >= 4:
+ serialized_disks = loaded_runtime[3]
else:
- kvm_cmd, serialized_nics, hvparams, serialized_disks = loaded_runtime
+ serialized_disks = []
+
+ 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()
+
+ 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)
for sdisk, link in serialized_disks]
_RETURN_KEY = RETURN_KEY = "return"
_ACTUAL_KEY = ACTUAL_KEY = "actual"
_ERROR_CLASS_KEY = "class"
- _ERROR_DATA_KEY = "data"
_ERROR_DESC_KEY = "desc"
_EXECUTE_KEY = "execute"
_ARGUMENTS_KEY = "arguments"
raise errors.HypervisorError("kvm: QMP communication error (wrong"
" server greeting")
+ # This is needed because QMP can return more than one greetings
+ # see https://groups.google.com/d/msg/ganeti-devel/gZYcvHKDooU/SnukC8dgS5AJ
+ self._buf = ""
+
# Let's put the monitor in command mode using the qmp_capabilities
# command, or else no command will be executable.
# (As per the QEMU Protocol Specification 0.1 - section 4)
err = response[self._ERROR_KEY]
if err:
raise errors.HypervisorError("kvm: error executing the %s"
- " command: %s (%s, %s):" %
+ " command: %s (%s):" %
(command,
err[self._ERROR_DESC_KEY],
- err[self._ERROR_CLASS_KEY],
- err[self._ERROR_DATA_KEY]))
+ err[self._ERROR_CLASS_KEY]))
elif not response[self._EVENT_KEY]:
return response
_INFO_PCI_RE = re.compile(r'Bus.*device[ ]*(\d+).*')
_INFO_PCI_CMD = "info pci"
+ _FIND_PCI_DEVICE_RE = \
+ staticmethod(lambda pci, devid:
+ re.compile(r'Bus.*device[ ]*%d,(.*\n){5,6}.*id "%s"' % (pci, devid),
+ re.M))
+
_INFO_VERSION_RE = \
re.compile(r'^QEMU (\d+)\.(\d+)(\.(\d+))?.*monitor.*', re.M)
_INFO_VERSION_CMD = "info version"
return utils.PathJoin(cls._NICS_DIR, instance_name)
@classmethod
- def _InstanceNICFile(cls, instance_name, seq):
+ def _InstanceNICFile(cls, instance_name, seq_or_uuid):
"""Returns the name of the file containing the tap device for a given NIC
"""
- return utils.PathJoin(cls._InstanceNICDir(instance_name), str(seq))
+ return utils.PathJoin(cls._InstanceNICDir(instance_name), str(seq_or_uuid))
+
+ @classmethod
+ def _GetInstanceNICTap(cls, instance_name, nic):
+ """Returns the tap for the corresponding nic
+
+ Search for tap file named after NIC's uuid.
+ For old instances without uuid indexed tap files returns nothing.
+
+ """
+ try:
+ return utils.ReadFile(cls._InstanceNICFile(instance_name, nic.uuid))
+ except EnvironmentError:
+ pass
+
+ @classmethod
+ def _WriteInstanceNICFiles(cls, instance_name, seq, nic, tap):
+ """Write tap name to both instance NIC files
+
+ """
+ for ident in [seq, nic.uuid]:
+ utils.WriteFile(cls._InstanceNICFile(instance_name, ident), data=tap)
+
+ @classmethod
+ def _RemoveInstanceNICFiles(cls, instance_name, seq, nic):
+ """Write tap name to both instance NIC files
+
+ """
+ for ident in [seq, nic.uuid]:
+ utils.RemoveFile(cls._InstanceNICFile(instance_name, ident))
@classmethod
def _InstanceKeymapFile(cls, instance_name):
@type tap: str
"""
- if instance.tags:
- tags = " ".join(instance.tags)
- else:
- tags = ""
-
env = {
"PATH": "%s:/sbin:/usr/sbin" % os.environ["PATH"],
"INSTANCE": instance.name,
"MODE": nic.nicparams[constants.NIC_MODE],
"INTERFACE": tap,
"INTERFACE_INDEX": str(seq),
- "TAGS": tags,
+ "INTERFACE_UUID": nic.uuid,
+ "TAGS": " ".join(instance.GetTags()),
}
if nic.ip:
env["IP"] = nic.ip
+ if nic.name:
+ env["INTERFACE_NAME"] = nic.name
+
if nic.nicparams[constants.NIC_LINK]:
env["LINK"] = nic.nicparams[constants.NIC_LINK]
cache_val = ",cache=%s" % disk_cache
else:
cache_val = ""
- for cfdev, link_name in kvm_disks:
+ for idx, (cfdev, link_name) in enumerate(kvm_disks):
if cfdev.mode != constants.DISK_RDWR:
raise errors.HypervisorError("Instance has read-only disks which"
" are not supported by KVM")
boot_disk = False
if needs_boot_flag and disk_type != constants.HT_DISK_IDE:
boot_val = ",boot=on"
+
+ # For ext we allow overriding disk_cache hypervisor params per disk
+ disk_cache = cfdev.params.get("cache", None)
+ if disk_cache:
+ cache_val = ",cache=%s" % disk_cache
drive_val = "file=%s,format=raw%s%s%s" % \
- (dev_path, if_val, boot_val, cache_val)
+ (link_name, if_val, boot_val, cache_val)
if device_driver:
# kvm_disks are the 4th entry of runtime file that did not exist in
# the past. That means that cfdev should always have pci slot and
# _GenerateDeviceKVMId() will not raise a exception.
- kvm_devid = _GenerateDeviceKVMId(constants.HOTPLUG_TARGET_DISK, cfdev)
+ kvm_devid = _GenerateDeviceKVMId(constants.HOTPLUG_TARGET_DISK,
+ cfdev, idx)
drive_val += (",id=%s" % kvm_devid)
- drive_val += (",bus=0,unit=%d" % cfdev.pci)
+ if cfdev.pci:
+ drive_val += (",bus=0,unit=%d" % cfdev.pci)
dev_val = ("%s,drive=%s,id=%s" %
(device_driver, kvm_devid, kvm_devid))
- dev_val += ",bus=pci.0,addr=%s" % hex(cfdev.pci)
+ if cfdev.pci:
+ dev_val += ",bus=pci.0,addr=%s" % hex(cfdev.pci)
dev_opts.extend(["-device", dev_val])
+ # TODO: export disk geometry in IDISK_PARAMS
+ heads = cfdev.params.get('heads', None)
+ secs = cfdev.params.get('secs', None)
+ if heads and secs:
+ nr_sectors = cfdev.size * 1024 * 1024 / 512
+ cyls = nr_sectors / (int(heads) * int(secs))
+ if cyls > 16383:
+ cyls = 16383
+ elif cyls < 2:
+ cyls = 2
+ if cyls and heads and secs:
+ drive_val += (",cyls=%d,heads=%d,secs=%d" %
+ (cyls, int(heads), int(secs)))
+
dev_opts.extend(["-drive", drive_val])
return dev_opts
tapfds = []
taps = []
devlist = self._GetKVMOutput(kvm_path, self._KVMOPT_DEVICELIST)
+
+ bdev_opts = self._GenerateKVMBlockDevicesOptions(instance,
+ kvm_disks,
+ kvmhelp,
+ devlist)
+ kvm_cmd.extend(bdev_opts)
+
if not kvm_nics:
kvm_cmd.extend(["-net", "none"])
else:
continue
self._ConfigureNIC(instance, nic_seq, nic, taps[nic_seq])
- bdev_opts = self._GenerateKVMBlockDevicesOptions(instance,
- 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.
"""Verifies that hotplug is supported.
Hotplug is *not* supported in case of:
- - qemu versions < 1.0
- security models and chroot (disk hotplug)
- fdsend module is missing (nic hot-add)
@raise errors.HypervisorError: in one of the previous cases
"""
- output = self._CallMonitorCommand(instance.name, self._INFO_VERSION_CMD)
- # TODO: search for netdev_add, drive_add, device_add.....
- match = self._INFO_VERSION_RE.search(output.stdout)
- if not match:
- raise errors.HotplugError("Try hotplug only in running instances.")
- v_major, v_min, _, _ = match.groups()
- if (int(v_major), int(v_min)) < (1, 0):
- raise errors.HotplugError("Hotplug not supported for qemu versions < 1.0")
-
if dev_type == constants.HOTPLUG_TARGET_DISK:
hvp = instance.hvparams
security_model = hvp[constants.HV_SECURITY_MODEL]
raise errors.HotplugError("Cannot hot-add NIC."
" fdsend python module is missing.")
- def _CallHotplugCommand(self, name, cmd):
- output = self._CallMonitorCommand(name, cmd)
- # TODO: parse output and check if succeeded
- for line in output.stdout.splitlines():
- logging.info("%s", line)
+ def HotplugSupported(self, instance):
+ """Checks if hotplug is generally supported.
+
+ Hotplug is *not* supported in case of:
+ - qemu versions < 1.0
+ - 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")
+
+ # TODO: search for netdev_add, drive_add, device_add.....
+ match = self._INFO_VERSION_RE.search(output.stdout)
+ if not match:
+ raise errors.HotplugError("Cannot parse qemu version via monitor")
+
+ v_major, v_min, _, _ = match.groups()
+ if (int(v_major), int(v_min)) < (1, 0):
+ raise errors.HotplugError("Hotplug not supported for qemu versions < 1.0")
+
+ def _CallHotplugCommands(self, name, cmds):
+ for c in cmds:
+ self._CallMonitorCommand(name, c)
+ time.sleep(1)
+
+ def _VerifyHotplugCommand(self, instance_name, device, dev_type,
+ should_exist):
+ """Checks if a previous hotplug command has succeeded.
+
+ It issues info pci monitor command and checks depending on should_exist
+ value if an entry with PCI slot and device ID is found or not.
+
+ @raise errors.HypervisorError: if result is not the expected one
+
+ """
+ output = self._CallMonitorCommand(instance_name, self._INFO_PCI_CMD)
+ kvm_devid = _GenerateDeviceKVMId(dev_type, device)
+ match = \
+ self._FIND_PCI_DEVICE_RE(device.pci, kvm_devid).search(output.stdout)
+ if match and not should_exist:
+ msg = "Device %s should have been removed but is still there" % kvm_devid
+ raise errors.HypervisorError(msg)
+
+ if not match 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)
def HotAddDevice(self, instance, dev_type, device, extra, seq):
""" Helper method to hot-add a new device
kvm_devid = _GenerateDeviceKVMId(dev_type, device)
runtime = self._LoadKVMRuntime(instance)
if dev_type == constants.HOTPLUG_TARGET_DISK:
- command = "drive_add dummy file=%s,if=none,id=%s,format=raw\n" % \
- (extra, kvm_devid)
- command += ("device_add virtio-blk-pci,bus=pci.0,addr=%s,drive=%s,id=%s" %
- (hex(device.pci), kvm_devid, kvm_devid))
+ cmds = ["drive_add dummy file=%s,if=none,id=%s,format=raw" %
+ (extra, kvm_devid)]
+ cmds += ["device_add virtio-blk-pci,bus=pci.0,addr=%s,drive=%s,id=%s" %
+ (hex(device.pci), kvm_devid, kvm_devid)]
elif dev_type == constants.HOTPLUG_TARGET_NIC:
(tap, fd) = _OpenTap()
self._ConfigureNIC(instance, seq, device, tap)
self._PassTapFd(instance, fd, device)
- command = "netdev_add tap,id=%s,fd=%s\n" % (kvm_devid, kvm_devid)
+ cmds = ["netdev_add tap,id=%s,fd=%s" % (kvm_devid, kvm_devid)]
args = "virtio-net-pci,bus=pci.0,addr=%s,mac=%s,netdev=%s,id=%s" % \
(hex(device.pci), device.mac, kvm_devid, kvm_devid)
- command += "device_add %s" % args
+ cmds += ["device_add %s" % args]
utils.WriteFile(self._InstanceNICFile(instance.name, seq), data=tap)
- self._CallHotplugCommand(instance.name, command)
+ self._CallHotplugCommands(instance.name, cmds)
+ self._VerifyHotplugCommand(instance.name, device, dev_type, True)
# update relevant entries in runtime file
index = _DEVICE_RUNTIME_INDEX[dev_type]
entry = _RUNTIME_ENTRY[dev_type](device, extra)
kvm_device = _RUNTIME_DEVICE[dev_type](entry)
kvm_devid = _GenerateDeviceKVMId(dev_type, kvm_device)
if dev_type == constants.HOTPLUG_TARGET_DISK:
- command = "device_del %s\n" % kvm_devid
- command += "drive_del %s" % kvm_devid
+ cmds = ["device_del %s" % kvm_devid]
+ cmds += ["drive_del %s" % kvm_devid]
elif dev_type == constants.HOTPLUG_TARGET_NIC:
- command = "device_del %s\n" % kvm_devid
- command += "netdev_del %s" % kvm_devid
+ cmds = ["device_del %s" % kvm_devid]
+ cmds += ["netdev_del %s" % kvm_devid]
utils.RemoveFile(self._InstanceNICFile(instance.name, seq))
- self._CallHotplugCommand(instance.name, command)
+ self._CallHotplugCommands(instance.name, cmds)
+ self._VerifyHotplugCommand(instance.name, kvm_device, dev_type, False)
index = _DEVICE_RUNTIME_INDEX[dev_type]
runtime[index].remove(entry)
self._SaveKVMRuntime(instance, runtime)
# putting it back in the same pci slot
device.pci = self.HotDelDevice(instance, dev_type, device, _, seq)
# TODO: remove sleep when socat gets removed
- time.sleep(2)
self.HotAddDevice(instance, dev_type, device, _, seq)
def _PassTapFd(self, instance, fd, nic):