[Kimchi-devel] [PATCH] [Kimchi 2/4] Add maxvcpus attribute to guests

Lucio Correia luciojhc at linux.vnet.ibm.com
Fri Feb 5 17:42:30 UTC 2016


- 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 at 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




More information about the Kimchi-devel mailing list