
- Enable update of maxvcpus, vcpus and CPU topology - Enable vCPU cold plug when topology is defined - Add cpu_info validation to guest update - Simplify _get_cpu_xml() code - Remove change_numa verification - Update docs and APIs with maxvcpus attribute Signed-off-by: Lucio Correia <luciojhc@linux.vnet.ibm.com> --- API.json | 7 +-- docs/API.md | 25 +++++++-- i18n.py | 1 - model/vms.py | 171 +++++++++++++++++++++++++++++++++----------------------- vmtemplate.py | 37 ++++-------- xmlutils/cpu.py | 6 +- 6 files changed, 136 insertions(+), 111 deletions(-) diff --git a/API.json b/API.json index d97a810..294be64 100644 --- a/API.json +++ b/API.json @@ -308,12 +308,7 @@ } } }, - "cpus": { - "description": "The new number of virtual CPUs for the VM", - "type": "integer", - "minimum": 1, - "error": "KCHTMPL0012E" - }, + "cpu_info": { "$ref": "#/kimchitype/cpu_info" }, "memory": { "description": "The new amount (MB) of memory for the VM", "type": "integer", diff --git a/docs/API.md b/docs/API.md index 070f938..a46e80e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -117,7 +117,13 @@ server. * io_throughput_peak: The highest recent value of 'io_throughput'. * uuid: UUID of the VM. * memory: The amount of memory assigned to the VM (in MB) - * cpus: The number of CPUs assigned to the VM + * cpu_info: CPU-specific information. + * vcpus: The number of CPUs assigned to the VM + * maxvcpus: The maximum number of CPUs that can be assigned to the VM + * topology: Processor topology, includes: + * sockets - The maximum number of sockets to use. + * cores - The number of cores per socket. + * threads - The number of threads per core. * screenshot: A link to a recent capture of the screen in PNG format * icon: A link to an icon that represents the VM * graphics: A dict to show detail of VM graphics. @@ -138,12 +144,10 @@ server. * groups: A list of system groups whose users have permission to access the VM. Default is: empty (i.e. no groups given access). * **DELETE**: Remove the Virtual Machine -* **PUT**: update the parameters of existed VM +* **PUT**: update the parameters of existing VM * name: New name for this VM (only applied for shutoff VM) * users: New list of system users. * groups: New list of system groups. - * cpus: New number of virtual cpus for this VM (if VM is running, new value - will take effect in next reboot) * memory: New amount of memory (MB) for this VM (if VM is running, new value will take effect in next reboot) * graphics: A dict to show detail of VM graphics. @@ -152,6 +156,19 @@ server. * passwdValidTo *(optional)*: lifetime for the console password. When omitted the password will be valid just for 30 seconds. + * cpu_info *(optional)*: CPU-specific information. + * maxvcpus *(optional)*: The maximum number of vCPUs that can be + assigned to the VM. If topology is specified, maxvcpus must be a + product of sockets, cores and threads. + * vcpus *(optional)*: The number of vCPUs assigned to the VM. Default + is 1, unless a CPU topology is specified. In that case, vcpus + must be a multiple of a product of cores and threads, and will + default to maxvcpus value. + * topology *(optional)*: Specify sockets, threads, and cores to run the + virtual CPU threads on. All three are required. + * sockets - The maximum number of sockets to use. + * cores - The number of cores per socket. + * threads - The number of threads per core. * **POST**: *See Virtual Machine Actions* diff --git a/i18n.py b/i18n.py index bbe68b6..0b9fa39 100644 --- a/i18n.py +++ b/i18n.py @@ -130,7 +130,6 @@ messages = { "KCHVM0071E": _("Memory value %(mem)s must be aligned to %(alignment)sMiB."), "KCHVM0073E": _("Unable to update the following parameters while the VM is offline: %(params)s"), "KCHVM0074E": _("Unable to update the following parameters while the VM is online: %(params)s"), - "KCHVM0075E": _("Cannot change VCPU value because '%(vm)s' has a topology defined - sockets: %(sockets)s, cores: %(cores)s, threads: %(threads)s."), "KCHVMHDEV0001E": _("VM %(vmid)s does not contain directly assigned host device %(dev_name)s."), "KCHVMHDEV0002E": _("The host device %(dev_name)s is not allowed to directly assign to VM."), diff --git a/model/vms.py b/model/vms.py index 9aaf530..593d73b 100644 --- a/model/vms.py +++ b/model/vms.py @@ -41,8 +41,8 @@ from wok.model.tasks import TaskModel from wok.rollbackcontext import RollbackContext from wok.utils import add_task, convert_data_size, get_next_clone_name from wok.utils import import_class, run_setfacl_set_attr, run_command, wok_log -from wok.xmlutils.utils import xpath_get_text, xml_item_update -from wok.xmlutils.utils import dictize +from wok.xmlutils.utils import dictize, xpath_get_text, xml_item_insert +from wok.xmlutils.utils import xml_item_remove, xml_item_update from wok.plugins.kimchi import model from wok.plugins.kimchi import vnc @@ -60,6 +60,7 @@ from wok.plugins.kimchi.screenshot import VMScreenshot from wok.plugins.kimchi.utils import template_name_from_uri from wok.plugins.kimchi.vmtemplate import MAX_MEM_LIM, PPC_MEM_ALIGN from wok.plugins.kimchi.xmlutils.cpu import get_cpu_xml, get_numa_xml +from wok.plugins.kimchi.xmlutils.cpu import get_topology_xml from wok.plugins.kimchi.xmlutils.disk import get_vm_disk_info, get_vm_disks @@ -72,15 +73,12 @@ DOM_STATE_MAP = {0: 'nostate', 6: 'crashed', 7: 'pmsuspended'} -VM_STATIC_UPDATE_PARAMS = {'name': './name', 'cpus': './vcpu'} - -VM_LIVE_UPDATE_PARAMS = {} - # update parameters which are updatable when the VM is online VM_ONLINE_UPDATE_PARAMS = ['graphics', 'groups', 'memory', 'users'] + # update parameters which are updatable when the VM is offline -VM_OFFLINE_UPDATE_PARAMS = ['cpus', 'graphics', 'groups', 'memory', 'name', - 'users'] +VM_OFFLINE_UPDATE_PARAMS = ['cpu_info', 'graphics', 'groups', 'memory', + 'name', 'users'] XPATH_DOMAIN_DISK = "/domain/devices/disk[@device='disk']/source/@file" XPATH_DOMAIN_DISK_BY_FILE = "./devices/disk[@device='disk']/source[@file='%s']" @@ -93,8 +91,11 @@ XPATH_DOMAIN_MEMORY_UNIT = '/domain/memory/@unit' XPATH_DOMAIN_UUID = '/domain/uuid' XPATH_DOMAIN_DEV_CPU_ID = '/domain/devices/spapr-cpu-socket/@id' +XPATH_CPU = './cpu' +XPATH_NAME = './name' XPATH_NUMA_CELL = './cpu/numa/cell' XPATH_TOPOLOGY = './cpu/topology' +XPATH_VCPU = './vcpu' # key: VM name; value: lock object vm_locks = {} @@ -241,23 +242,6 @@ class VMModel(object): threads = xpath_get_text(xml, XPATH_TOPOLOGY + '/@threads') return sockets and cores and threads - def get_vm_max_sockets(self, dom): - return int(xpath_get_text(dom.XMLDesc(0), - XPATH_TOPOLOGY + '/@sockets')[0]) - - def get_vm_sockets(self, dom): - current_vcpu = dom.vcpusFlags(libvirt.VIR_DOMAIN_AFFECT_CURRENT) - return (current_vcpu / self.get_vm_cores(dom) / - self.get_vm_threads(dom)) - - def get_vm_cores(self, dom): - return int(xpath_get_text(dom.XMLDesc(0), - XPATH_TOPOLOGY + '/@cores')[0]) - - def get_vm_threads(self, dom): - return int(xpath_get_text(dom.XMLDesc(0), - XPATH_TOPOLOGY + '/@threads')[0]) - def update(self, name, params): lock = vm_locks.get(name) if lock is None: @@ -285,17 +269,6 @@ class VMModel(object): raise InvalidParameter('KCHVM0074E', {'params': ', '.join(ext_params)}) - if 'cpus' in params and DOM_STATE_MAP[dom.info()[0]] == 'shutoff': - # user cannot change vcpu if topology is defined. - curr_vcpu = dom.vcpusFlags(libvirt.VIR_DOMAIN_AFFECT_CURRENT) - if self.has_topology(dom) and curr_vcpu != params['cpus']: - raise InvalidOperation( - 'KCHVM0075E', - {'vm': dom.name(), - 'sockets': self.get_vm_sockets(dom), - 'cores': self.get_vm_cores(dom), - 'threads': self.get_vm_threads(dom)}) - self._live_vm_update(dom, params) vm_name, dom = self._static_vm_update(name, dom, params) return vm_name @@ -751,6 +724,8 @@ class VMModel(object): old_xml = new_xml = dom.XMLDesc(0) params = copy.deepcopy(params) + + # Update name name = params.get('name') nonascii_name = None if name is not None: @@ -760,37 +735,41 @@ class VMModel(object): raise InvalidParameter("KCHVM0003E", msg_args) params['name'], nonascii_name = get_ascii_nonascii_name(name) - - for key, val in params.items(): - change_numa = True - if key in VM_STATIC_UPDATE_PARAMS: - if type(val) == int: - val = str(val) - xpath = VM_STATIC_UPDATE_PARAMS[key] - attrib = None - if key == 'cpus': - if self.has_topology(dom) or dom.isActive(): - change_numa = False - continue - # Update maxvcpu firstly - new_xml = xml_item_update(new_xml, xpath, - str(self._get_host_maxcpu()), - attrib) - # Update current vcpu - attrib = 'current' - new_xml = xml_item_update(new_xml, xpath, val, attrib) + new_xml = xml_item_update(new_xml, XPATH_NAME, name, None) + + # Update CPU info + cpu_info = params.get('cpu_info', {}) + cpu_info = self._update_cpu_info(new_xml, dom, cpu_info) + + vcpus = str(cpu_info['vcpus']) + new_xml = xml_item_update(new_xml, XPATH_VCPU, vcpus, 'current') + + maxvcpus = str(cpu_info['maxvcpus']) + new_xml = xml_item_update(new_xml, XPATH_VCPU, maxvcpus, None) + + topology = cpu_info['topology'] + if topology: + sockets = str(topology['sockets']) + cores = str(topology['cores']) + threads = str(topology['threads']) + + if self.has_topology(dom): + # topology is being updated + xpath = XPATH_TOPOLOGY + new_xml = xml_item_update(new_xml, xpath, sockets, 'sockets') + new_xml = xml_item_update(new_xml, xpath, cores, 'cores') + new_xml = xml_item_update(new_xml, xpath, threads, 'threads') + else: + # topology is being added + new_xml = xml_item_insert(new_xml, XPATH_CPU, + get_topology_xml(topology)) + elif self.has_topology(dom): + # topology is being undefined: remove it + new_xml = xml_item_remove(new_xml, XPATH_TOPOLOGY) # Updating memory and NUMA if necessary, if vm is offline - if not dom.isActive(): - if 'memory' in params: - new_xml = self._update_memory_config(new_xml, params) - elif 'cpus' in params and change_numa and \ - (xpath_get_text(new_xml, XPATH_NUMA_CELL + '/@memory') != []): - new_xml = xml_item_update( - new_xml, - XPATH_NUMA_CELL, - value='0', - attr='cpus') + if 'memory' in params and not dom.isActive(): + new_xml = self._update_memory_config(new_xml, params) if 'graphics' in params: new_xml = self._update_graphics(dom, new_xml, params) @@ -834,10 +813,10 @@ class VMModel(object): # apropriated root = ET.fromstring(xml) numa_mem = xpath_get_text(xml, XPATH_NUMA_CELL + '/@memory') - vcpus = params.get('cpus') + maxvcpus = params.get('cpu_info', {}).get('maxvcpus') if numa_mem == []: - if vcpus is None: - vcpus = int(xpath_get_text(xml, 'vcpu')[0]) + if maxvcpus is None: + maxvcpus = int(xpath_get_text(xml, XPATH_VCPU)[0]) cpu = root.find('./cpu') if cpu is None: cpu = get_cpu_xml(0, params['memory'] << 10) @@ -846,7 +825,7 @@ class VMModel(object): numa_element = get_numa_xml(0, params['memory'] << 10) cpu.insert(0, ET.fromstring(numa_element)) else: - if vcpus is not None: + if maxvcpus is not None: xml = xml_item_update( xml, XPATH_NUMA_CELL, @@ -928,6 +907,37 @@ class VMModel(object): return ET.tostring(root, encoding="utf-8") + def _update_cpu_info(self, new_xml, dom, new_info): + topology = {} + if self.has_topology(dom): + sockets = xpath_get_text(new_xml, XPATH_TOPOLOGY + '/@sockets')[0] + cores = xpath_get_text(new_xml, XPATH_TOPOLOGY + '/@cores')[0] + threads = xpath_get_text(new_xml, XPATH_TOPOLOGY + '/@threads')[0] + topology = { + 'sockets': int(sockets), + 'cores': int(cores), + 'threads': int(threads), + } + + # if current is not defined in vcpu, vcpus is equal to maxvcpus + xml_maxvcpus = xpath_get_text(new_xml, 'vcpu') + maxvcpus = int(xml_maxvcpus[0]) + xml_vcpus = xpath_get_text(new_xml, './vcpu/@current') + vcpus = int(xml_vcpus[0]) if xml_vcpus else maxvcpus + + cpu_info = { + 'maxvcpus': maxvcpus, + 'vcpus': vcpus, + 'topology': topology, + } + cpu_info.update(new_info) + + # Revalidate cpu info - may raise CPUInfo exceptions + cpu_model = CPUInfoModel(conn=self.conn) + cpu_model.check_cpu_info(cpu_info) + + return cpu_info + def _live_vm_update(self, dom, params): self._vm_update_access_metadata(dom, params) if 'memory' in params and dom.isActive(): @@ -1140,8 +1150,27 @@ class VMModel(object): res['io_throughput_peak'] = vm_stats.get('max_disk_io', 100) users, groups = self._get_access_info(dom) + xml = dom.XMLDesc(0) + maxvcpus = int(xpath_get_text(xml, XPATH_VCPU)[0]) + + cpu_info = { + 'vcpus': info[3], + 'maxvcpus': maxvcpus, + 'topology': {}, + } + + if self.has_topology(dom): + sockets = int(xpath_get_text(xml, XPATH_TOPOLOGY + '/@sockets')[0]) + cores = int(xpath_get_text(xml, XPATH_TOPOLOGY + '/@cores')[0]) + threads = int(xpath_get_text(xml, XPATH_TOPOLOGY + '/@threads')[0]) + + cpu_info['topology'] = { + 'sockets': sockets, + 'cores': cores, + 'threads': threads, + } + if state == 'shutoff': - xml = dom.XMLDesc(0) val = xpath_get_text(xml, XPATH_DOMAIN_MEMORY)[0] unit_list = xpath_get_text(xml, XPATH_DOMAIN_MEMORY_UNIT) if len(unit_list) > 0: @@ -1157,7 +1186,7 @@ class VMModel(object): 'stats': res, 'uuid': dom.UUIDString(), 'memory': memory, - 'cpus': info[3], + 'cpu_info': cpu_info, 'screenshot': screenshot, 'icon': icon, # (type, listen, port, passwd, passwdValidTo) diff --git a/vmtemplate.py b/vmtemplate.py index 2f524a7..ef17ff6 100644 --- a/vmtemplate.py +++ b/vmtemplate.py @@ -324,12 +324,9 @@ class VMTemplate(object): def _get_cpu_xml(self): # Include CPU topology, if provided - cpu_info = self.info.get('cpu_info') - if cpu_info is not None: - cpu_topo = cpu_info.get('topology') - return get_cpu_xml(0, - self.info.get('memory') << 10, - cpu_topo) + cpu_topo = self.info.get('cpu_info', {}).get('topology', {}) + + return get_cpu_xml(0, self.info.get('memory') << 10, cpu_topo) def _get_max_memory(self, guest_memory): # Setting maxMemory of the VM, which will be lesser value between: @@ -371,8 +368,6 @@ class VMTemplate(object): libvirt_stream_protocols = kwargs.get('libvirt_stream_protocols', []) cdrom_xml = self._get_cdrom_xml(libvirt_stream_protocols) - max_vcpus = kwargs.get('max_vcpus', 1) - if not urlparse.urlparse(self.info.get('cdrom', "")).scheme in \ libvirt_stream_protocols and \ params.get('iso_stream', False): @@ -402,23 +397,13 @@ class VMTemplate(object): # set a hard limit using max_memory + 1GiB params['hard_limit'] = params['max_memory'] + (1024 << 10) - cpu_topo = self.info.get('cpu_info').get('topology') - if (cpu_topo is not None): - sockets = int(max_vcpus / (cpu_topo['cores'] * - cpu_topo['threads'])) - self.info['cpu_info']['topology']['sockets'] = sockets + # vcpu element + cpus = params['cpu_info']['vcpus'] + maxvcpus = params['cpu_info']['maxvcpus'] + params['vcpus_xml'] = "<vcpu current='%d'>%d</vcpu>" % (cpus, maxvcpus) - # Reduce maxvcpu to fit number of sockets if necessary - total_max_vcpu = sockets * cpu_topo['cores'] * cpu_topo['threads'] - if total_max_vcpu != max_vcpus: - max_vcpus = total_max_vcpu - - params['vcpus'] = "<vcpu current='%s'>%d</vcpu>" % \ - (params['cpus'], max_vcpus) - else: - params['vcpus'] = "<vcpu current='%s'>%d</vcpu>" % \ - (params['cpus'], max_vcpus) - params['cpu_info'] = self._get_cpu_xml() + # cpu_info element + params['cpu_info_xml'] = self._get_cpu_xml() xml = """ <domain type='%(domain)s'> @@ -430,8 +415,8 @@ class VMTemplate(object): </memtune> <maxMemory slots='%(slots)s' unit='KiB'>%(max_memory)s</maxMemory> <memory unit='MiB'>%(memory)s</memory> - %(vcpus)s - %(cpu_info)s + %(vcpus_xml)s + %(cpu_info_xml)s <os> <type arch='%(arch)s'>hvm</type> <boot dev='hd'/> diff --git a/xmlutils/cpu.py b/xmlutils/cpu.py index 32c01a4..b3eaaae 100644 --- a/xmlutils/cpu.py +++ b/xmlutils/cpu.py @@ -1,7 +1,7 @@ # # Project Kimchi # -# Copyright IBM, Corp. 2015 +# Copyright IBM, Corp. 2015-2016 # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -45,7 +45,7 @@ def get_topology_xml(cpu_topo): return ET.tostring(xml) -def get_cpu_xml(cpus, memory, cpu_topo=None): +def get_cpu_xml(cpus, memory, cpu_topo={}): # Returns the libvirt CPU element based on given numa and topology # CPU element will always have numa element # <cpu> @@ -55,6 +55,6 @@ def get_cpu_xml(cpus, memory, cpu_topo=None): # <topology sockets='1' cores='2' threads='1'/> # </cpu> xml = E.cpu(ET.fromstring(get_numa_xml(cpus, memory))) - if cpu_topo is not None: + if cpu_topo: xml.insert(0, ET.fromstring(get_topology_xml(cpu_topo))) return ET.tostring(xml) -- 1.9.1