
This patch changes the backend in order to allow user to edit max memory tag. It changes the memory API to return and receive: 'memory': {'current': XXX, 'maxmemory': YYY}. Other changes include: - maxMemory xml tag is not created by default anymore; - maxMemory xml tag is only created if user passes maxmemory; - if memory and maxmemory are equal, than maxMemory tag is removed; - keeps the limit to max memory and memory to 1TiB; - adds tests to json and modify documentation; Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- API.json | 7 +- docs/API.md | 11 ++- i18n.py | 3 +- model/vms.py | 246 ++++++++++++++++++++++++++++++++--------------------------- 4 files changed, 146 insertions(+), 121 deletions(-) diff --git a/API.json b/API.json index f9a4ced..3c56c93 100644 --- a/API.json +++ b/API.json @@ -329,12 +329,7 @@ } }, "cpu_info": { "$ref": "#/kimchitype/cpu_info" }, - "memory": { - "description": "The new amount (MB) of memory for the VM", - "type": "integer", - "minimum": 512, - "error": "KCHTMPL0013E" - } + "memory": { "$ref": "#/kimchitype/memory" } }, "additionalProperties": false }, diff --git a/docs/API.md b/docs/API.md index 33d51ac..7cb7952 100644 --- a/docs/API.md +++ b/docs/API.md @@ -116,7 +116,10 @@ server. writes across all virtual disks (kb/s). * 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) + * memory: The memory parameters of the VM in the unit of MiB. + * current: The amount of memory that is assigned to the VM. + * maxmemory: The maximum total of memory that the VM can have. Amount + over current will be used exclusively for memory hotplug * 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 @@ -148,8 +151,10 @@ server. * name: New name for this VM (only applied for shutoff VM) * users: New list of system users. * groups: New list of system groups. - * memory: New amount of memory (MB) for this VM (if VM is running, new - value will take effect in next reboot) + * memory: New memory parameters of the VM in the unit of MiB. + Provide one or both. + * current: New amount of memory that will be assigned to the VM. + * maxmemory: New maximum total of memory that the VM can have. * graphics: A dict to show detail of VM graphics. * passwd *(optional)*: console password. When omitted a random password willbe generated. diff --git a/i18n.py b/i18n.py index 36d4310..33b9c9d 100644 --- a/i18n.py +++ b/i18n.py @@ -100,7 +100,7 @@ messages = { "KCHVM0039E": _("Cannot resume VM '%(name)s' because it is not paused."), "KCHVM0040E": _("Unable to resume VM '%(name)s'. Details: %(err)s"), "KCHVM0041E": _("Memory assigned is higher then the maximum allowed in the host: %(maxmem)sMib."), - "KCHVM0042E": _("VM '%(name)s' does not support live memory update. Update the memory with the machine offline to enable this feature."), + "KCHVM0042E": _("Guest '%(name)s' does not support live memory update. Please, with the guest offline, set Maximum Memory with a value greater then Memory to enable this feature."), "KCHVM0043E": _("Only increase memory is allowed in active VMs"), "KCHVM0044E": _("For live memory update, new memory value must be equal old memory value plus multiples of 1024 Mib"), "KCHVM0045E": _("There are not enough free slots of 1024 Mib in the guest."), @@ -134,6 +134,7 @@ messages = { "KCHVM0077E": _("Impossible to get the serial console of %(name)s"), "KCHVM0078E": _("Memory or Maximum Memory value is higher than amount supported by the host: %(memHost)sMiB."), "KCHVM0079E": _("Memory or Maximum Memory value is higher than maximum amount recommended: 1TiB"), + "KCHVM0080E": _("Cannot update Maximum Memory when guest is running."), "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 5fb27d3..5702f80 100644 --- a/model/vms.py +++ b/model/vms.py @@ -52,8 +52,8 @@ from wok.plugins.kimchi.kvmusertests import UserTests from wok.plugins.kimchi.model.config import CapabilitiesModel from wok.plugins.kimchi.model.cpuinfo import CPUInfoModel from wok.plugins.kimchi.model.featuretests import FeatureTests -from wok.plugins.kimchi.model.templates import TemplateModel -from wok.plugins.kimchi.model.templates import MAX_MEM_LIM, PPC_MEM_ALIGN +from wok.plugins.kimchi.model.templates import PPC_MEM_ALIGN +from wok.plugins.kimchi.model.templates import TemplateModel, validate_memory from wok.plugins.kimchi.model.utils import get_ascii_nonascii_name, get_vm_name from wok.plugins.kimchi.model.utils import get_metadata_node from wok.plugins.kimchi.model.utils import remove_metadata_node @@ -97,6 +97,7 @@ XPATH_NAME = './name' XPATH_NUMA_CELL = './cpu/numa/cell' XPATH_TOPOLOGY = './cpu/topology' XPATH_VCPU = './vcpu' +XPATH_MAX_MEMORY = './maxMemory' # key: VM name; value: lock object vm_locks = {} @@ -251,16 +252,12 @@ class VMModel(object): vm_locks[name] = lock with lock: - # make sure memory is alingned in 256MiB in PowerPC - distro, _, _ = platform.linux_distribution() - if 'memory' in params and distro == "IBM_PowerKVM": - if params['memory'] % PPC_MEM_ALIGN != 0: - raise InvalidParameter('KCHVM0071E', - {'param': "Memory", - 'mem': str(params['memory']), - 'alignment': str(PPC_MEM_ALIGN)}) - dom = self.get_vm(name, self.conn) + # You can only change <maxMemory> offline, updating guest XML + if ("memory" in params) and ('maxmemory' in params['memory']) and\ + (DOM_STATE_MAP[dom.info()[0]] != 'shutoff'): + raise InvalidParameter("KCHVM0080E") + if DOM_STATE_MAP[dom.info()[0]] == 'shutoff': ext_params = set(params.keys()) - set(VM_OFFLINE_UPDATE_PARAMS) if len(ext_params) > 0: @@ -725,14 +722,14 @@ class VMModel(object): def _static_vm_update(self, vm_name, dom, params): old_xml = new_xml = dom.XMLDesc(0) - params = copy.deepcopy(params) # Update name name = params.get('name') nonascii_name = None + state = DOM_STATE_MAP[dom.info()[0]] + if name is not None: - state = DOM_STATE_MAP[dom.info()[0]] if state != 'shutoff': msg_args = {'name': vm_name, 'new_name': params['name']} raise InvalidParameter("KCHVM0003E", msg_args) @@ -771,7 +768,8 @@ class VMModel(object): new_xml = xml_item_remove(new_xml, XPATH_TOPOLOGY) # Updating memory and NUMA if necessary, if vm is offline - if 'memory' in params and not dom.isActive(): + if (not dom.isActive() and 'memory' in params and + params['memory'] != {}): new_xml = self._update_memory_config(new_xml, params) if 'graphics' in params: @@ -811,103 +809,113 @@ class VMModel(object): return (nonascii_name if nonascii_name is not None else vm_name, dom) def _update_memory_config(self, xml, params): - # Checks if NUMA memory is already configured, if not, checks if CPU - # element is already configured (topology). Then add NUMA element as - # apropriated root = ET.fromstring(xml) - numa_mem = xpath_get_text(xml, XPATH_NUMA_CELL + '/@memory') - maxvcpus = params.get('cpu_info', {}).get('maxvcpus') - if numa_mem == []: - 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) - root.insert(0, ET.fromstring(cpu)) - else: - numa_element = get_numa_xml(0, params['memory'] << 10) - cpu.insert(0, ET.fromstring(numa_element)) + # MiB to KiB + hasMem = 'current' in params['memory'] + hasMaxMem = 'maxmemory' in params['memory'] + oldMem = int(xpath_get_text(xml, XPATH_DOMAIN_MEMORY)[0]) >> 10 + maxMemTag = root.find(XPATH_MAX_MEMORY) + if maxMemTag is not None: + oldMaxMem = int(xpath_get_text(xml, XPATH_MAX_MEMORY)[0]) >> 10 else: - if maxvcpus is not None: - xml = xml_item_update( - xml, - XPATH_NUMA_CELL, - value='0', - attr='cpus') - root = ET.fromstring(xml_item_update(xml, XPATH_NUMA_CELL, - str(params['memory'] << 10), - attr='memory')) - - # Remove currentMemory, automatically set later by libvirt - currentMem = root.find('.currentMemory') - if currentMem is not None: - root.remove(currentMem) - - memory = root.find('.memory') - # Update/Adds maxMemory accordingly - if not self.caps.mem_hotplug_support: - if memory is not None: - memory.text = str(params['memory'] << 10) + oldMaxMem = oldMem + newMem = (params['memory'].get('current', oldMem)) << 10 + newMaxMem = (params['memory'].get('maxmemory', oldMaxMem)) << 10 + + validate_memory({'current': newMem >> 10, + 'maxmemory': newMaxMem >> 10}) + + def _get_slots(mem, maxMem): + slots = (maxMem - mem) >> 10 >> 10 + # Libvirt does not accepts slots <= 1 + if slots < 0: + raise InvalidParameter("KCHTMPL0031E", + {'mem': str(mem >> 10), + 'maxmem': str(maxMem >> 10)}) + elif slots == 0: + slots = 1 + + # max 32 slots on Power + distro, _, _ = platform.linux_distribution() + if distro == "IBM_PowerKVM" and slots > 32: + slots = 32 + return slots + # End of _get_slots + + # There is an issue in Libvirt/Qemu, where Guest does not start if + # memory and max memory are the same. So we decided to remove max + # memory and only add it if user explicitly provides it, willing to + # do memory hotplug + if hasMaxMem: + # Conditions: + if (maxMemTag is None) and (newMem != newMaxMem): + # Creates the maxMemory tag + max_mem_xml = E.maxMemory( + str(newMaxMem), + unit='Kib', + slots=str(_get_slots(newMem, newMaxMem))) + root.insert(0, max_mem_xml) + elif (maxMemTag is None) and (newMem == newMaxMem): + # Nothing to do + pass + elif (maxMemTag is not None) and (newMem != newMaxMem): + # Just update value in max memory tag + maxMemTag.text = str(newMaxMem) + maxMemTag.set('slots', str(_get_slots(newMem, newMaxMem))) + elif (maxMemTag is not None) and (newMem == newMaxMem): + # Remove the tag + root.remove(maxMemTag) + + # Update memory, if necessary + if hasMem: + # Remove currentMemory, automatically set later by libvirt, with + # memory value + currentMem = root.find('.currentMemory') + if currentMem is not None: + root.remove(currentMem) + + memory = root.find('.memory') + # If host/guest does not support memory hot plug, then there is + # NUMA configure, we must update the tag directly + if not self.caps.mem_hotplug_support: + if memory is not None: + memory.text = str(newMem) + else: + if memory is not None: + # Libvirt is going to set the value automatically with + # the value configured in NUMA tag + root.remove(memory) + + if (maxMemTag is not None) and (not hasMaxMem): + if newMem == newMaxMem: + root.remove(maxMemTag) + else: + maxMemTag.set('slots', str(_get_slots(newMem, newMaxMem))) + + # Set NUMA parameterers and create it if does not exist + # CPU tag DO NOT exist? Create it + cpu = root.find(XPATH_CPU) + if cpu is None: + cpu = get_cpu_xml(0, newMem) + root.insert(0, ET.fromstring(cpu)) else: - if memory is not None: - root.remove(memory) - - def _get_slots(maxMem): - slots = (maxMem - params['memory']) >> 10 - # Libvirt does not accepts slots <= 1 - if slots < 0: - raise OperationFailed("KCHVM0041E", - {'maxmem': str(maxMem)}) - elif slots == 0: - slots = 1 - - distro, _, _ = platform.linux_distribution() - if distro == "IBM_PowerKVM": - # max 32 slots on Power - if slots > 32: - slots = 32 - return slots - # End of _get_slots - - def _get_newMaxMem(): - # Setting max memory to 4x memory requested, host total memory, - # or 1 TB. This should avoid problems with live migration - newMaxMem = MAX_MEM_LIM - hostMem = self.conn.get().getInfo()[1] << 10 - if hostMem < newMaxMem: - newMaxMem = hostMem - mem = params.get('memory', 0) - if (mem != 0) and (((mem * 4) << 10) < newMaxMem): - newMaxMem = (mem * 4) << 10 - - distro, _, _ = platform.linux_distribution() - if distro == "IBM_PowerKVM": - # max memory 256MiB alignment - newMaxMem -= (newMaxMem % 256) - return newMaxMem - - maxMem = root.find('.maxMemory') - if maxMem is not None: - root.remove(maxMem) - - # Setting maxMemory - newMaxMem = _get_newMaxMem() - slots = _get_slots(newMaxMem >> 10) - max_mem_xml = E.maxMemory( - str(newMaxMem), - unit='Kib', - slots=str(slots)) - root.insert(0, max_mem_xml) - - # Setting memory hard limit to max_memory + 1GiB - memtune = root.find('memtune') - if memtune is not None: - hl = memtune.find('hard_limit') - if hl is not None: - memtune.remove(hl) - memtune.insert(0, E.hard_limit(str(newMaxMem + 1048576), - unit='Kib')) - + # Does NUMA tag exist ? + numaTag = cpu.find('./numa') + if numaTag is None: + numa_element = get_numa_xml(0, newMem) + cpu.insert(0, ET.fromstring(numa_element)) + else: + cellTag = cpu.find('./numa/cell') + cellTag.set('memory', str(newMem)) + + # Setting memory hard limit to max_memory + 1GiB + memtune = root.find('memtune') + if memtune is not None: + hl = memtune.find('hard_limit') + if hl is not None: + memtune.remove(hl) + memtune.insert(0, E.hard_limit(str(newMaxMem + 1048576), + unit='Kib')) return ET.tostring(root, encoding="utf-8") def _update_cpu_info(self, new_xml, dom, new_info): @@ -943,7 +951,8 @@ class VMModel(object): def _live_vm_update(self, dom, params): self._vm_update_access_metadata(dom, params) - if 'memory' in params and dom.isActive(): + if (('memory' in params) and ('current' in params['memory']) and + dom.isActive()): self._update_memory_live(dom, params) def _update_memory_live(self, dom, params): @@ -960,13 +969,21 @@ class VMModel(object): raise OperationFailed('KCHVM0042E', {'name': dom.name()}) # Memory live update must be done in chunks of 1024 Mib or 1Gib - new_mem = params['memory'] + new_mem = params['memory']['current'] old_mem = int(xpath_get_text(xml, XPATH_DOMAIN_MEMORY)[0]) >> 10 if new_mem < old_mem: raise OperationFailed('KCHVM0043E') if (new_mem - old_mem) % 1024 != 0: raise OperationFailed('KCHVM0044E') + # make sure memory is alingned in 256MiB in PowerPC + distro, _, _ = platform.linux_distribution() + if (distro == "IBM_PowerKVM" and new_mem % PPC_MEM_ALIGN != 0): + raise InvalidParameter('KCHVM0071E', + {'param': "Memory", + 'mem': str(new_mem), + 'alignment': str(PPC_MEM_ALIGN)}) + # Check slot spaces: total_slots = int(xpath_get_text(xml, './maxMemory/@slots')[0]) needed_slots = (new_mem - old_mem) >> 10 @@ -1190,11 +1207,18 @@ class VMModel(object): proc.join(1) self._serial_procs.remove(proc) + # Get max memory, or return "memory" if not set + maxmemory = xpath_get_text(xml, XPATH_MAX_MEMORY) + if len(maxmemory) > 0: + maxmemory = convert_data_size(maxmemory[0], 'KiB', 'MiB') + else: + maxmemory = memory + return {'name': name, 'state': state, 'stats': res, 'uuid': dom.UUIDString(), - 'memory': memory, + 'memory': {'current': memory, 'maxmemory': maxmemory}, 'cpu_info': cpu_info, 'screenshot': screenshot, 'icon': icon, -- 2.1.0