diff --git a/lib/backend.py b/lib/backend.py index b5bb87e2ed056c24b24f740fdf8a62a05db1caed..362be1f7429b26994988b79372e32e3e258ddf72 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -1519,6 +1519,34 @@ def GetMigrationStatus(instance): except Exception, err: # pylint: disable=W0703 _Fail("Failed to get migration status: %s", err, exc=True) +def HotAddDisk(instance, disk, dev_path, seq): + """Hot add a nic + + """ + hyper = hypervisor.GetHypervisor(instance.hypervisor) + return hyper.HotAddDisk(instance, disk, dev_path, seq) + +def HotDelDisk(instance, disk, seq): + """Hot add a nic + + """ + hyper = hypervisor.GetHypervisor(instance.hypervisor) + return hyper.HotDelDisk(instance, disk, seq) + +def HotAddNic(instance, nic, seq): + """Hot add a nic + + """ + hyper = hypervisor.GetHypervisor(instance.hypervisor) + return hyper.HotAddNic(instance, nic, seq) + +def HotDelNic(instance, nic, seq): + """Hot add a nic + + """ + hyper = hypervisor.GetHypervisor(instance.hypervisor) + return hyper.HotDelNic(instance, nic, seq) + def BlockdevCreate(disk, size, owner, on_primary, info): """Creates a block device for an instance. diff --git a/lib/cli.py b/lib/cli.py index e930afa945dda821223565a6f47b3d8837718cf3..6a707acc7e4eba8b0b5c07fbd17498aec66e7640 100644 --- a/lib/cli.py +++ b/lib/cli.py @@ -92,6 +92,7 @@ __all__ = [ "GLOBAL_FILEDIR_OPT", "HID_OS_OPT", "GLOBAL_SHARED_FILEDIR_OPT", + "HOTPLUG_OPT", "HVLIST_OPT", "HVOPTS_OPT", "HYPERVISOR_OPT", @@ -1491,6 +1492,10 @@ NOCONFLICTSCHECK_OPT = cli_option("--no-conflicts-check", action="store_false", help="Don't check for conflicting IPs") +HOTPLUG_OPT = cli_option("--hotplug", dest="hotplug", + action="store_true", default=False, + help="Enable disk/nic hotplug") + #: Options provided by all commands COMMON_OPTS = [DEBUG_OPT] @@ -2451,6 +2456,11 @@ def GenericInstanceCreate(mode, opts, args): else: raise errors.ProgrammerError("Invalid creation mode %s" % mode) + if opts.hotplug: + hotplug = True + else: + hotplug = False + op = opcodes.OpInstanceCreate(instance_name=instance, disks=disks, disk_template=opts.disk_template, @@ -2474,6 +2484,7 @@ def GenericInstanceCreate(mode, opts, args): src_node=src_node, src_path=src_path, tags=tags, + hotplug=hotplug, no_install=no_install, identify_defaults=identify_defaults, ignore_ipolicy=opts.ignore_ipolicy) diff --git a/lib/client/gnt_instance.py b/lib/client/gnt_instance.py index 296b4413e35c135d35f7010e69300324a3e05b39..04c750873bb5f5402542af133f4f14649c079907 100644 --- a/lib/client/gnt_instance.py +++ b/lib/client/gnt_instance.py @@ -1405,9 +1405,15 @@ def SetInstanceParams(opts, args): else: offline = None + if opts.hotplug: + hotplug = True + else: + hotplug = False + op = opcodes.OpInstanceSetParams(instance_name=args[0], nics=nics, disks=disks, + hotplug=hotplug, disk_template=opts.disk_template, remote_node=opts.node, hvparams=opts.hvparams, @@ -1520,6 +1526,7 @@ add_opts = [ FORCE_VARIANT_OPT, NO_INSTALL_OPT, IGNORE_IPOLICY_OPT, + HOTPLUG_OPT, ] commands = { @@ -1607,7 +1614,7 @@ commands = { DISK_TEMPLATE_OPT, SINGLE_NODE_OPT, OS_OPT, FORCE_VARIANT_OPT, OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT, NWSYNC_OPT, OFFLINE_INST_OPT, ONLINE_INST_OPT, IGNORE_IPOLICY_OPT, RUNTIME_MEM_OPT, - NOCONFLICTSCHECK_OPT], + NOCONFLICTSCHECK_OPT, HOTPLUG_OPT], "<instance>", "Alters the parameters of an instance"), "shutdown": ( GenericManyOps("shutdown", _ShutdownInstance), [ArgInstance()], diff --git a/lib/cmdlib.py b/lib/cmdlib.py index 9c10c8e9e18a8f170fa5b565cb646df7dd505a63..0a973aada1cd1a6d75ec420caff95104fd42b375 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -6401,7 +6401,7 @@ class LUInstanceActivateDisks(NoHooksLU): def _AssembleInstanceDisks(lu, instance, disks=None, ignore_secondaries=False, - ignore_size=False): + ignore_size=False, check=True): """Prepare the block devices for an instance. This sets up the block devices on all nodes. @@ -6427,7 +6427,8 @@ def _AssembleInstanceDisks(lu, instance, disks=None, ignore_secondaries=False, device_info = [] disks_ok = True iname = instance.name - disks = _ExpandCheckDisks(instance, disks) + if check: + disks = _ExpandCheckDisks(instance, disks) # With the two passes mechanism we try to reduce the window of # opportunity for the race condition of switching DRBD to primary @@ -8785,6 +8786,22 @@ def _GenerateUniqueNames(lu, exts): results.append("%s%s" % (new_id, val)) return results +def _GetPCIInfo(lu, dev_type): + + if lu.op.hotplug: + if hasattr(lu, 'hotplug_info'): + info = lu.hotplug_info + elif hasattr(lu, 'instance') and hasattr(lu.instance, 'hotplug_info'): + return lu.cfg.GetPCIInfo(lu.instance.name, dev_type) + + if info: + idx = getattr(info, dev_type) + setattr(info, dev_type, idx+1) + pci = info.pci_pool.pop() + return idx, pci + + return None, None + def _GenerateDRBD8Branch(lu, primary, secondary, size, vgnames, names, iv_name, p_minor, s_minor): @@ -8801,7 +8818,10 @@ def _GenerateDRBD8Branch(lu, primary, secondary, size, vgnames, names, dev_meta = objects.Disk(dev_type=constants.LD_LV, size=DRBD_META_SIZE, logical_id=(vgnames[1], names[1]), params={}) - drbd_dev = objects.Disk(dev_type=constants.LD_DRBD8, size=size, + + disk_idx, pci = _GetPCIInfo(lu, 'disks') + drbd_dev = objects.Disk(idx=disk_idx, pci=pci, + dev_type=constants.LD_DRBD8, size=size, logical_id=(primary, secondary, port, p_minor, s_minor, shared_secret), @@ -8910,11 +8930,14 @@ def _GenerateDiskTemplate(lu, template_name, instance_name, primary_node, size = disk[constants.IDISK_SIZE] feedback_fn("* disk %s, size %s" % (disk_index, utils.FormatUnit(size, "h"))) + + disk_idx, pci = _GetPCIInfo(lu, 'disks') + disks.append(objects.Disk(dev_type=dev_type, size=size, logical_id=logical_id_fn(idx, disk_index, disk), iv_name="disk/%d" % disk_index, mode=disk[constants.IDISK_MODE], - params={})) + params={}, idx=disk_idx, pci=pci)) return disks @@ -9805,6 +9828,10 @@ class LUInstanceCreate(LogicalUnit): if self.op.identify_defaults: self._RevertToDefaults(cluster) + self.hotplug_info = None + if self.op.hotplug: + self.hotplug_info = objects.HotplugInfo(disks=0, nics=0, + pci_pool=list(range(16,32))) # NIC buildup self.nics = [] for idx, nic in enumerate(self.op.nics): @@ -9875,8 +9902,10 @@ class LUInstanceCreate(LogicalUnit): check_params = cluster.SimpleFillNIC(nicparams) objects.NIC.CheckParameterSyntax(check_params) - self.nics.append(objects.NIC(mac=mac, ip=nic_ip, - network=net, nicparams=check_params)) + nic_idx, pci = _GetPCIInfo(self, 'nics') + self.nics.append(objects.NIC(idx=nic_idx, pci=pci, + mac=mac, ip=nic_ip, network=net, + nicparams=check_params)) # disk checks/pre-build default_vg = self.cfg.GetVGName() @@ -10199,6 +10228,7 @@ class LUInstanceCreate(LogicalUnit): hvparams=self.op.hvparams, hypervisor=self.op.hypervisor, osparams=self.op.osparams, + hotplug_info=self.hotplug_info, ) if self.op.tags: @@ -12164,13 +12194,16 @@ def ApplyContainerMods(kind, container, chgdesc, mods, if remove_fn is not None: remove_fn(absidx, item, private) + #TODO: include a hotplugged msg in changes changes = [("%s/%s" % (kind, absidx), "remove")] assert container[absidx] == item del container[absidx] elif op == constants.DDM_MODIFY: if modify_fn is not None: + #TODO: include a hotplugged msg in changes changes = modify_fn(absidx, item, params, private) + else: raise errors.ProgrammerError("Unhandled operation '%s'" % op) @@ -12549,6 +12582,8 @@ class LUInstanceSetParams(LogicalUnit): " a NIC that is connected to a network.", errors.ECODE_INVAL) + logging.info("new_params %s", new_params) + logging.info("new_filled_params %s", new_filled_params) private.params = new_params private.filled = new_filled_params @@ -12572,6 +12607,7 @@ class LUInstanceSetParams(LogicalUnit): # Prepare disk/NIC modifications self.diskmod = PrepareContainerMods(self.op.disks, None) self.nicmod = PrepareContainerMods(self.op.nics, _InstNicModPrivate) + logging.info("nicmod %s", self.nicmod) # OS change if self.op.os_name and not self.op.force: @@ -12810,9 +12846,11 @@ class LUInstanceSetParams(LogicalUnit): " (%d), cannot add more" % constants.MAX_NICS, errors.ECODE_STATE) + # Verify disk changes (operating on a copy) disks = instance.disks[:] - ApplyContainerMods("disk", disks, None, self.diskmod, None, None, None) + ApplyContainerMods("disk", disks, None, self.diskmod, + None, None, None) if len(disks) > constants.MAX_DISKS: raise errors.OpPrereqError("Instance has too many disks (%d), cannot add" " more" % constants.MAX_DISKS, @@ -12831,11 +12869,13 @@ class LUInstanceSetParams(LogicalUnit): # Operate on copies as this is still in prereq nics = [nic.Copy() for nic in instance.nics] ApplyContainerMods("NIC", nics, self._nic_chgdesc, self.nicmod, - self._CreateNewNic, self._ApplyNicMods, None) + self._CreateNewNic, self._ApplyNicMods, + self._RemoveNic) self._new_nics = nics else: self._new_nics = None + def _ConvertPlainToDrbd(self, feedback_fn): """Converts an instance from plain to drbd. @@ -12983,6 +13023,12 @@ class LUInstanceSetParams(LogicalUnit): self.LogWarning("Failed to create volume %s (%s) on node '%s': %s", disk.iv_name, disk, node, err) + if self.op.hotplug and disk.pci: + disk_ok, device_info = _AssembleInstanceDisks(self, self.instance, + [disk], check=False) + _, _, dev_path = device_info[0] + result = self.rpc.call_hot_add_disk(self.instance.primary_node, + self.instance, disk, dev_path, idx) return (disk, [ ("disk/%d" % idx, "add:size=%s,mode=%s" % (disk.size, disk.mode)), ]) @@ -13002,6 +13048,19 @@ class LUInstanceSetParams(LogicalUnit): """Removes a disk. """ + #TODO: log warning in case hotplug is not possible + # handle errors + if root.pci and not self.op.hotplug: + raise errors.OpPrereqError("Cannot remove a disk that has" + " been hotplugged" + " without removing it with hotplug", + errors.ECODE_INVAL) + if self.op.hotplug and root.pci: + self.rpc.call_hot_del_disk(self.instance.primary_node, + self.instance, root, idx) + _ShutdownInstanceDisks(self, self.instance, [root]) + self.cfg.UpdatePCIInfo(self.instance.name, root.pci) + (anno_disk,) = _AnnotateDiskParams(self.instance, [root], self.cfg) for node, disk in anno_disk.ComputeNodeTree(self.instance.primary_node): self.cfg.SetDiskID(disk, node) @@ -13014,8 +13073,7 @@ class LUInstanceSetParams(LogicalUnit): if root.dev_type in constants.LDS_DRBD: self.cfg.AddTcpUdpPort(root.logical_id[2]) - @staticmethod - def _CreateNewNic(idx, params, private): + def _CreateNewNic(self, idx, params, private): """Creates data structure for a new network interface. """ @@ -13025,16 +13083,27 @@ class LUInstanceSetParams(LogicalUnit): #TODO: not private.filled?? can a nic have no nicparams?? nicparams = private.filled - return (objects.NIC(mac=mac, ip=ip, network=network, nicparams=nicparams), [ + nic = objects.NIC(mac=mac, ip=ip, network=network, nicparams=nicparams) + + #TODO: log warning in case hotplug is not possible + # handle errors + # return changes + if self.op.hotplug: + nic_idx, pci = _GetPCIInfo(self, 'nics') + nic.idx = nic_idx + nic.pci = pci + result = self.rpc.call_hot_add_nic(self.instance.primary_node, + self.instance, nic, idx) + desc = [ ("nic.%d" % idx, "add:mac=%s,ip=%s,mode=%s,link=%s,network=%s" % (mac, ip, private.filled[constants.NIC_MODE], private.filled[constants.NIC_LINK], network)), - ]) + ] + return (nic, desc) - @staticmethod - def _ApplyNicMods(idx, nic, params, private): + def _ApplyNicMods(self, idx, nic, params, private): """Modifies a network interface. """ @@ -13051,8 +13120,28 @@ class LUInstanceSetParams(LogicalUnit): for (key, val) in nic.nicparams.items(): changes.append(("nic.%s/%d" % (key, idx), val)) + #TODO: log warning in case hotplug is not possible + # handle errors + if self.op.hotplug and nic.pci: + self.rpc.call_hot_del_nic(self.instance.primary_node, + self.instance, nic, idx) + result = self.rpc.call_hot_add_nic(self.instance.primary_node, + self.instance, nic, idx) return changes + def _RemoveNic(self, idx, nic, private): + if nic.pci and not self.op.hotplug: + raise errors.OpPrereqError("Cannot remove a nic that has been hotplugged" + " without removing it with hotplug", + errors.ECODE_INVAL) + #TODO: log warning in case hotplug is not possible + # handle errors + if self.op.hotplug and nic.pci: + self.rpc.call_hot_del_nic(self.instance.primary_node, + self.instance, nic, idx) + self.cfg.UpdatePCIInfo(self.instance.name, nic.pci) + + def Exec(self, feedback_fn): """Modifies an instance. diff --git a/lib/config.py b/lib/config.py index cf4ad81d322497fb008c0f01d53c7e807ddb6e56..d7a6c8addbae5fc5881469f9fbdd8bc2b47b5d39 100644 --- a/lib/config.py +++ b/lib/config.py @@ -402,6 +402,27 @@ class ConfigWriter: if net_uuid: return self._UnlockedReserveIp(net_uuid, address, ec_id) + @locking.ssynchronized(_config_lock, shared=1) + def GetPCIInfo(self, instance_name, dev_type): + + instance = self._UnlockedGetInstanceInfo(instance_name) + if not instance.hotplug_info: + return None, None + idx = getattr(instance.hotplug_info, dev_type) + setattr(instance.hotplug_info, dev_type, idx+1) + pci = instance.hotplug_info.pci_pool.pop() + self._WriteConfig() + + return idx, pci + + @locking.ssynchronized(_config_lock, shared=1) + def UpdatePCIInfo(self, instance_name, pci_slot): + + instance = self._UnlockedGetInstanceInfo(instance_name) + if instance.hotplug_info: + instance.hotplug_info.pci_pool.append(pci_slot) + self._WriteConfig() + @locking.ssynchronized(_config_lock, shared=1) def ReserveLV(self, lv_name, ec_id): """Reserve an VG/LV pair for an instance. diff --git a/lib/hypervisor/hv_kvm.py b/lib/hypervisor/hv_kvm.py index 5fba7635bc7126d563470ae79e528d7183bc8a99..25f1ddd3707d2719665e3dd3546746411de4ca4e 100644 --- a/lib/hypervisor/hv_kvm.py +++ b/lib/hypervisor/hv_kvm.py @@ -37,6 +37,7 @@ import shutil import socket import stat import StringIO +import fdsend try: import affinity # pylint: disable=F0401 except ImportError: @@ -1028,38 +1029,6 @@ class KVMHypervisor(hv_base.BaseHypervisor): needs_boot_flag = (v_major, v_min) < (0, 14) disk_type = hvp[constants.HV_DISK_TYPE] - if disk_type == constants.HT_DISK_PARAVIRTUAL: - if_val = ",if=virtio" - else: - if_val = ",if=%s" % disk_type - # Cache mode - disk_cache = hvp[constants.HV_DISK_CACHE] - if instance.disk_template in constants.DTS_EXT_MIRROR: - if disk_cache != "none": - # TODO: make this a hard error, instead of a silent overwrite - logging.warning("KVM: overriding disk_cache setting '%s' with 'none'" - " to prevent shared storage corruption on migration", - disk_cache) - cache_val = ",cache=none" - elif disk_cache != constants.HT_CACHE_DEFAULT: - cache_val = ",cache=%s" % disk_cache - else: - cache_val = "" - for cfdev, dev_path in block_devices: - 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: - kvm_cmd.extend(["-boot", "c"]) - boot_disk = False - if needs_boot_flag and disk_type != constants.HT_DISK_IDE: - boot_val = ",boot=on" - - drive_val = "file=%s,format=raw%s%s%s" % (dev_path, if_val, boot_val, - cache_val) - kvm_cmd.extend(["-drive", drive_val]) #Now we can specify a different device type for CDROM devices. cdrom_disk_type = hvp[constants.HV_KVM_CDROM_DISK_TYPE] @@ -1285,7 +1254,7 @@ class KVMHypervisor(hv_base.BaseHypervisor): kvm_nics = instance.nics hvparams = hvp - return (kvm_cmd, kvm_nics, hvparams) + return (kvm_cmd, kvm_nics, hvparams, block_devices) def _WriteKVMRuntime(self, instance_name, data): """Write an instance's KVM runtime @@ -1311,9 +1280,11 @@ class KVMHypervisor(hv_base.BaseHypervisor): """Save an instance's KVM runtime """ - kvm_cmd, kvm_nics, hvparams = kvm_runtime + kvm_cmd, kvm_nics, hvparams, block_devices = kvm_runtime serialized_nics = [nic.ToDict() for nic in kvm_nics] - serialized_form = serializer.Dump((kvm_cmd, serialized_nics, hvparams)) + serialized_blockdevs = [(blk.ToDict(), link) for blk,link in block_devices] + serialized_form = serializer.Dump((kvm_cmd, serialized_nics, + hvparams, serialized_blockdevs)) self._WriteKVMRuntime(instance.name, serialized_form) def _LoadKVMRuntime(self, instance, serialized_runtime=None): @@ -1323,9 +1294,11 @@ class KVMHypervisor(hv_base.BaseHypervisor): if not serialized_runtime: serialized_runtime = self._ReadKVMRuntime(instance.name) loaded_runtime = serializer.Load(serialized_runtime) - kvm_cmd, serialized_nics, hvparams = loaded_runtime + kvm_cmd, serialized_nics, hvparams, serialized_blockdevs = loaded_runtime kvm_nics = [objects.NIC.FromDict(snic) for snic in serialized_nics] - return (kvm_cmd, kvm_nics, hvparams) + block_devices = [(objects.Disk.FromDict(sdisk), link) + for sdisk, link in serialized_blockdevs] + return (kvm_cmd, kvm_nics, hvparams, block_devices) def _RunKVMCmd(self, name, kvm_cmd, tap_fds=None): """Run the KVM cmd and check for errors @@ -1368,10 +1341,11 @@ class KVMHypervisor(hv_base.BaseHypervisor): conf_hvp = instance.hvparams name = instance.name self._CheckDown(name) + boot_disk = conf_hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_DISK temp_files = [] - kvm_cmd, kvm_nics, up_hvp = kvm_runtime + kvm_cmd, kvm_nics, up_hvp, block_devices = kvm_runtime up_hvp = objects.FillDict(conf_hvp, up_hvp) _, v_major, v_min, _ = self._GetKVMVersion() @@ -1392,6 +1366,60 @@ class KVMHypervisor(hv_base.BaseHypervisor): utils.WriteFile(keymap_path, data="include en-us\ninclude %s\n" % keymap) kvm_cmd.extend(["-k", keymap_path]) + # whether this is an older KVM version that uses the boot=on flag + # on devices + needs_boot_flag = (v_major, v_min) < (0, 14) + + disk_type = up_hvp[constants.HV_DISK_TYPE] + if disk_type == constants.HT_DISK_PARAVIRTUAL: + if_val = ",if=virtio" + if (v_major, v_min) >= (0, 12): + disk_model = "virtio-blk-pci" + else: + disk_model = "virtio" + else: + if_val = ",if=%s" % disk_type + disk_model = disk_type + # Cache mode + disk_cache = up_hvp[constants.HV_DISK_CACHE] + if instance.disk_template in constants.DTS_EXT_MIRROR: + if disk_cache != "none": + # TODO: make this a hard error, instead of a silent overwrite + logging.warning("KVM: overriding disk_cache setting '%s' with 'none'" + " to prevent shared storage corruption on migration", + disk_cache) + cache_val = ",cache=none" + elif disk_cache != constants.HT_CACHE_DEFAULT: + cache_val = ",cache=%s" % disk_cache + else: + cache_val = "" + for cfdev, dev_path in block_devices: + 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: + kvm_cmd.extend(["-boot", "c"]) + boot_disk = False + if needs_boot_flag and disk_type != constants.HT_DISK_IDE: + boot_val = ",boot=on" + drive_val = "file=%s,format=raw%s%s" % \ + (dev_path, boot_val, cache_val) + if cfdev.pci: + #TODO: name id after model + drive_val += (",bus=0,unit=%d,if=none,id=drive%d" % + (cfdev.pci, cfdev.idx)) + else: + drive_val += if_val + + kvm_cmd.extend(["-drive", drive_val]) + + if cfdev.pci: + dev_val = ("%s,bus=pci.0,addr=%s,drive=drive%d,id=virtio-blk-pci.%d" % + (disk_model, hex(cfdev.pci), cfdev.idx, cfdev.idx)) + kvm_cmd.extend(["-device", dev_val]) + # 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 @@ -1426,8 +1454,16 @@ class KVMHypervisor(hv_base.BaseHypervisor): tapfds.append(tapfd) taps.append(tapname) if (v_major, v_min) >= (0, 12): - nic_val = "%s,mac=%s,netdev=netdev%s" % (nic_model, nic.mac, nic_seq) - tap_val = "type=tap,id=netdev%s,fd=%d%s" % (nic_seq, tapfd, tap_extra) + if nic.pci: + nic_idx = nic.idx + else: + nic_idx = nic_seq + nic_val = ("%s,mac=%s,netdev=netdev%d" % + (nic_model, nic.mac, nic_idx)) + if nic.pci: + nic_val += (",bus=pci.0,addr=%s,id=virtio-net-pci.%d" % + (hex(nic.pci), nic_idx)) + tap_val = "type=tap,id=netdev%d,fd=%d%s" % (nic_idx, tapfd, tap_extra) kvm_cmd.extend(["-netdev", tap_val, "-device", nic_val]) else: nic_val = "nic,vlan=%s,macaddr=%s,model=%s" % (nic_seq, @@ -1578,6 +1614,167 @@ class KVMHypervisor(hv_base.BaseHypervisor): return result + def HotAddDisk(self, instance, disk, dev_path, seq): + """Hotadd new disk to the VM + + """ + if not self._InstancePidAlive(instance.name)[2]: + logging.info("Cannot hotplug. Instance %s not alive" % instance.name) + return disk.ToDict() + + _, v_major, v_min, _ = self._GetKVMVersion() + if (v_major, v_min) >= (1, 0) and disk.pci: + idx = disk.idx + command = ("drive_add dummy file=%s,if=none,id=drive%d,format=raw" % + (dev_path, idx)) + + logging.info("%s" % command) + output = self._CallMonitorCommand(instance.name, command) + + command = ("device_add virtio-blk-pci,bus=pci.0,addr=%s," + "drive=drive%d,id=virtio-blk-pci.%d" + % (hex(disk.pci), idx, idx)) + logging.info("%s" % command) + output = self._CallMonitorCommand(instance.name, command) + for line in output.stdout.splitlines(): + logging.info("%s" % line) + + (kvm_cmd, kvm_nics, + hvparams, block_devices) = self._LoadKVMRuntime(instance) + block_devices.append((disk, dev_path)) + new_kvm_runtime = (kvm_cmd, kvm_nics, hvparams, block_devices) + self._SaveKVMRuntime(instance, new_kvm_runtime) + + return disk.ToDict() + + def HotDelDisk(self, instance, disk, seq): + """Hotdel disk to the VM + + """ + if not self._InstancePidAlive(instance.name)[2]: + logging.info("Cannot hotplug. Instance %s not alive" % instance.name) + return disk.ToDict() + + _, v_major, v_min, _ = self._GetKVMVersion() + if (v_major, v_min) >= (1, 0) and disk.pci: + idx = disk.idx + + command = "device_del virtio-blk-pci.%d" % idx + logging.info("%s" % command) + output = self._CallMonitorCommand(instance.name, command) + for line in output.stdout.splitlines(): + logging.info("%s" % line) + + command = "drive_del drive%d" % idx + logging.info("%s" % command) + #output = self._CallMonitorCommand(instance.name, command) + #for line in output.stdout.splitlines(): + # logging.info("%s" % line) + + (kvm_cmd, kvm_nics, + hvparams, block_devices) = self._LoadKVMRuntime(instance) + rem = [(d, p) for d, p in block_devices + if d.idx is not None and d.idx == idx] + try: + block_devices.remove(rem[0]) + except (ValueError, IndexError): + logging.info("Disk with %d idx disappeared from runtime file", idx) + new_kvm_runtime = (kvm_cmd, kvm_nics, hvparams, block_devices) + self._SaveKVMRuntime(instance, new_kvm_runtime) + + return disk.ToDict() + + def HotAddNic(self, instance, nic, seq): + """Hotadd new nic to the VM + + """ + if not self._InstancePidAlive(instance.name)[2]: + logging.info("Cannot hotplug. Instance %s not alive" % instance.name) + return nic.ToDict() + + _, v_major, v_min, _ = self._GetKVMVersion() + if (v_major, v_min) >= (1, 0) and nic.pci: + mac = nic.mac + idx = nic.idx + + (tap, fd) = _OpenTap() + logging.info("%s %d", tap, fd) + + self._PassTapFd(instance, fd, nic) + + command = ("netdev_add tap,id=netdev%d,fd=netdev%d" + % (idx, idx)) + logging.info("%s" % command) + output = self._CallMonitorCommand(instance.name, command) + for line in output.stdout.splitlines(): + logging.info("%s" % line) + + command = ("device_add virtio-net-pci,bus=pci.0,addr=%s,mac=%s," + "netdev=netdev%d,id=virtio-net-pci.%d" + % (hex(nic.pci), mac, idx, idx)) + logging.info("%s" % command) + output = self._CallMonitorCommand(instance.name, command) + for line in output.stdout.splitlines(): + logging.info("%s" % line) + + self._ConfigureNIC(instance, seq, nic, tap) + + (kvm_cmd, kvm_nics, + hvparams, block_devices) = self._LoadKVMRuntime(instance) + kvm_nics.append(nic) + new_kvm_runtime = (kvm_cmd, kvm_nics, hvparams, block_devices) + self._SaveKVMRuntime(instance, new_kvm_runtime) + + return nic.ToDict() + + def HotDelNic(self, instance, nic, seq): + """Hotadd new nic to the VM + + """ + if not self._InstancePidAlive(instance.name)[2]: + logging.info("Cannot hotplug. Instance %s not alive" % instance.name) + return nic.ToDict() + + _, v_major, v_min, _ = self._GetKVMVersion() + if (v_major, v_min) >= (1, 0) and nic.pci: + mac = nic.mac + idx = nic.idx + + command = "device_del virtio-net-pci.%d" % idx + logging.info("%s" % command) + output = self._CallMonitorCommand(instance.name, command) + for line in output.stdout.splitlines(): + logging.info("%s" % line) + + command = "netdev_del netdev%d" % idx + logging.info("%s" % command) + output = self._CallMonitorCommand(instance.name, command) + for line in output.stdout.splitlines(): + logging.info("%s" % line) + + (kvm_cmd, kvm_nics, + hvparams, block_devices) = self._LoadKVMRuntime(instance) + rem = [n for n in kvm_nics if n.idx is not None and n.idx == nic.idx] + try: + kvm_nics.remove(rem[0]) + except (ValueError, IndexError): + logging.info("NIC with %d idx disappeared from runtime file", nic.idx) + new_kvm_runtime = (kvm_cmd, kvm_nics, hvparams, block_devices) + self._SaveKVMRuntime(instance, new_kvm_runtime) + + return nic.ToDict() + + def _PassTapFd(self, instance, fd, nic): + monsock = utils.ShellQuote(self._InstanceMonitor(instance.name)) + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(monsock) + idx = nic.idx + command = "getfd netdev%d\n" % idx + fds = [fd] + logging.info("%s", fds) + fdsend.sendfds(s, command, fds = fds) + s.close() + @classmethod def _ParseKVMVersion(cls, text): """Parse the KVM version from the --help output. diff --git a/lib/objects.py b/lib/objects.py index f29e82122b7b9337d7dee2220e7b35390e560d28..3b6ee49c95d639435302863dc9f23bf28792b374 100644 --- a/lib/objects.py +++ b/lib/objects.py @@ -507,10 +507,13 @@ class ConfigData(ConfigObject): if self.networks is None: self.networks = {} +class HotplugInfo(ConfigObject): + __slots__ = ["nics", "disks", "pci_pool"] + class NIC(ConfigObject): """Config object representing a network card.""" - __slots__ = ["mac", "ip", "network", "nicparams", "netinfo"] + __slots__ = ["idx", "pci", "mac", "ip", "network", "nicparams", "netinfo"] @classmethod def CheckParameterSyntax(cls, nicparams): @@ -534,7 +537,7 @@ class NIC(ConfigObject): class Disk(ConfigObject): """Config object representing a block device.""" - __slots__ = ["dev_type", "logical_id", "physical_id", + __slots__ = ["idx", "pci", "dev_type", "logical_id", "physical_id", "children", "iv_name", "size", "mode", "params"] def CreateOnSecondary(self): @@ -1037,6 +1040,7 @@ class Instance(TaggableObject): "admin_state", "nics", "disks", + "hotplug_info", "disk_template", "network_port", "serial_no", @@ -1167,6 +1171,8 @@ class Instance(TaggableObject): else: nlist = [] bo[attr] = nlist + if self.hotplug_info: + bo['hotplug_info'] = self.hotplug_info.ToDict() return bo @classmethod @@ -1184,6 +1190,8 @@ class Instance(TaggableObject): obj = super(Instance, cls).FromDict(val) obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC) obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk) + if "hotplug_info" in val: + obj.hotplug_info = HotplugInfo.FromDict(val["hotplug_info"]) return obj def UpgradeConfig(self): diff --git a/lib/opcodes.py b/lib/opcodes.py index 9626075b0cc86a4deaa4cc282dd98db6046d9bc7..766140aca3f65abd53f0b97e48650edaa272b86d 100644 --- a/lib/opcodes.py +++ b/lib/opcodes.py @@ -1272,6 +1272,7 @@ class OpInstanceCreate(OpCode): ("src_path", None, ht.TMaybeString, "Source directory for import"), ("start", True, ht.TBool, "Whether to start instance after creation"), ("tags", ht.EmptyList, ht.TListOf(ht.TNonEmptyString), "Instance tags"), + ("hotplug", None, ht.TMaybeBool, "Whether to hotplug devices"), ] OP_RESULT = ht.Comment("instance nodes")(ht.TListOf(ht.TNonEmptyString)) @@ -1576,6 +1577,7 @@ class OpInstanceSetParams(OpCode): "Whether to wait for the disk to synchronize, when changing template"), ("offline", None, ht.TMaybeBool, "Whether to mark instance as offline"), ("conflicts_check", True, ht.TBool, "Check for conflicting IPs"), + ("hotplug", None, ht.TMaybeBool, "Whether to hotplug devices"), ] OP_RESULT = _TSetParamsResult diff --git a/lib/rpc_defs.py b/lib/rpc_defs.py index 93644d6278842baf33e98dfe3c58a872c53eaf5c..965d968d5610d567c179b81f72c061c4b9a8d6e9 100644 --- a/lib/rpc_defs.py +++ b/lib/rpc_defs.py @@ -275,6 +275,27 @@ _INSTANCE_CALLS = [ ("reinstall", None, None), ("debug", None, None), ], None, None, "Starts an instance"), + ("hot_add_nic", SINGLE, None, TMO_NORMAL, [ + ("instance", ED_INST_DICT, "Instance object"), + ("nic", ED_NIC_DICT, "Nic dict to hotplug"), + ("seq", None, "Nic seq to hotplug"), + ], None, None, "Adds a nic to a running instance"), + ("hot_del_nic", SINGLE, None, TMO_NORMAL, [ + ("instance", ED_INST_DICT, "Instance object"), + ("nic", ED_NIC_DICT, "nic dict to remove"), + ("seq", None, "Nic seq to hotplug"), + ], None, None, "Removes a nic to a running instance"), + ("hot_add_disk", SINGLE, None, TMO_NORMAL, [ + ("instance", ED_INST_DICT, "Instance object"), + ("disk", ED_OBJECT_DICT, "Disk dict to hotplug"), + ("dev_path", None, "Device path"), + ("seq", None, "Disk seq to hotplug"), + ], None, None, "Adds a nic to a running instance"), + ("hot_del_disk", SINGLE, None, TMO_NORMAL, [ + ("instance", ED_INST_DICT, "Instance object"), + ("disk", ED_OBJECT_DICT, "Disk dict to remove"), + ("seq", None, "Disk seq to hotplug"), + ], None, None, "Removes a nic to a running instance"), ] _IMPEXP_CALLS = [ diff --git a/lib/server/noded.py b/lib/server/noded.py index d95680a55655827b119abd7b0bc1887ed75cd375..1f3de35b8ead9e5fb675d71aad080d3ed5f81a6a 100644 --- a/lib/server/noded.py +++ b/lib/server/noded.py @@ -558,6 +558,50 @@ class NodeRequestHandler(http.server.HttpServerHandler): instance = objects.Instance.FromDict(instance_name) return backend.StartInstance(instance, startup_paused) + @staticmethod + def perspective_hot_add_disk(params): + """Hotplugs a nic to a running instance. + + """ + (idict, ddict, dev_path, seq) = params + logging.info("%s %s", idict, ddict) + instance = objects.Instance.FromDict(idict) + disk = objects.Disk.FromDict(ddict) + return backend.HotAddDisk(instance, disk, dev_path, seq) + + @staticmethod + def perspective_hot_del_disk(params): + """Hotplugs a nic to a running instance. + + """ + (idict, ddict, seq) = params + logging.info("%s %s", idict, ddict) + instance = objects.Instance.FromDict(idict) + disk = objects.Disk.FromDict(ddict) + return backend.HotDelDisk(instance, disk, seq) + + @staticmethod + def perspective_hot_add_nic(params): + """Hotplugs a nic to a running instance. + + """ + (idict, ndict, seq) = params + logging.info("%s %s", idict, ndict) + instance = objects.Instance.FromDict(idict) + nic = objects.NIC.FromDict(ndict) + return backend.HotAddNic(instance, nic, seq) + + @staticmethod + def perspective_hot_del_nic(params): + """Hotplugs a nic to a running instance. + + """ + (idict, ndict, seq) = params + logging.info("%s %s", idict, ndict) + instance = objects.Instance.FromDict(idict) + nic = objects.NIC.FromDict(ndict) + return backend.HotDelNic(instance, nic, seq) + @staticmethod def perspective_migration_info(params): """Gather information about an instance to be migrated.