
Signed-off-by: Christy Perez <christy@linux.vnet.ibm.com> --- docs/API.md | 28 ++++++ src/kimchi/API.json | 11 +-- src/kimchi/control/cpuinfo.py | 37 ++++++++ src/kimchi/control/host.py | 2 + src/kimchi/i18n.py | 6 +- src/kimchi/model/cpuinfo.py | 215 ++++++++++++++++++++++++++++++++++++++++++ src/kimchi/model/host.py | 2 +- src/kimchi/model/templates.py | 25 +++-- 8 files changed, 309 insertions(+), 17 deletions(-) create mode 100644 src/kimchi/control/cpuinfo.py create mode 100644 src/kimchi/model/cpuinfo.py diff --git a/docs/API.md b/docs/API.md index 6c36bb1..9f627ac 100644 --- a/docs/API.md +++ b/docs/API.md @@ -260,6 +260,10 @@ Represents a snapshot of the Virtual Machine's primary monitor. * threads - The number of threads per core. If specifying both cpus and CPU topology, make sure cpus is equal to the product of sockets, cores, and threads. + Only threads is required. '0' should be passed in for users to + take advantage of this auto-sizing feature. Then, kimchi will create + a topology based on vcpus (if specified) and the host's capabilities. + To find the host's capabilties, see the /host/cpuinfo documentation. ### Sub-Collection: Virtual Machine Network Interfaces @@ -896,6 +900,30 @@ Contains the host sample data. *No actions defined* +### Resource: HostStats + +**URI:** /host/cpuinfo + +The cores and sockets of a hosts's CPU. Useful when sizing VMs to take +advantages of the perforamance benefits of SMT (Power) or Hyper-Threading (Intel). + +**Methods:** + +* **GET**: Retreives the sockets, cores, and threads values. + * threading_enabled: Whether CPU topology is supported on this system. + * sockets: The number of total sockets on a system. + * cores: The total number of cores per socket. + * threads_per_core: The threads per core. + +**Actions (PUT):** + +*No actions defined* + +**Actions (POST):** + +*No actions defined* + + ### Resource: HostStatsHistory **URI:** /host/stats/history diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 0ad36ab..fb28723 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -37,20 +37,15 @@ "properties": { "sockets": { "type": "integer", - "required": true, - "minimum": 1, - "error": "KCHTMPL0026E" + "minimum": 1 }, "cores": { "type": "integer", - "required": true, - "minimum": 1, - "error": "KCHTMPL0026E" + "minimum": 1 }, "threads": { - "type": "integer", "required": true, - "minimum": 1, + "type": "integer", "error": "KCHTMPL0026E" } } diff --git a/src/kimchi/control/cpuinfo.py b/src/kimchi/control/cpuinfo.py new file mode 100644 index 0000000..415dd3d --- /dev/null +++ b/src/kimchi/control/cpuinfo.py @@ -0,0 +1,37 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2014 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +from kimchi.control.base import Resource + + +class CPUInfo(Resource): + def __init__(self, model): + super(CPUInfo, self).__init__(model) + self.admin_methods = ['GET'] + self.role_key = 'host' + self.uri_fmt = "/host/cpuinfo" + + @property + def data(self): + return {'threading_enabled': self.info['guest_threads_enabled'], + 'sockets': self.info['sockets'], + 'cores': self.info['cores_available'], + 'threads_per_core': self.info['threads_per_core'] + } diff --git a/src/kimchi/control/host.py b/src/kimchi/control/host.py index 4362da7..9f73653 100644 --- a/src/kimchi/control/host.py +++ b/src/kimchi/control/host.py @@ -17,6 +17,7 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +from kimchi.control.cpuinfo import CPUInfo from kimchi.control.base import Collection, Resource, SimpleCollection from kimchi.control.utils import UrlSubNode from kimchi.exception import NotFoundError @@ -39,6 +40,7 @@ def __init__(self, model, id=None): self.users = Users(self.model) self.groups = Groups(self.model) self.swupdate = self.generate_action_handler_task('swupdate') + self.cpuinfo = CPUInfo(self.model) @property def data(self): diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 8f6b67e..6b9b95a 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -148,8 +148,9 @@ "KCHTMPL0023E": _("Template base image must be a valid local image file"), "KCHTMPL0024E": _("Cannot identify base image %(path)s format"), "KCHTMPL0025E": _("When specifying CPU topology, VCPUs must be a product of sockets, cores, and threads."), - "KCHTMPL0026E": _("When specifying CPU topology, each element must be an integer greater than zero."), + "KCHTMPL0026E": _("When specifying CPU topology, threads is required."), "KCHTMPL0027E": _("Invalid disk image format. Valid formats: bochs, cloop, cow, dmg, qcow, qcow2, qed, raw, vmdk, vpc."), + "KCHTMPL0029E": _("This host (or current configuration) does not allow CPU topology."), "KCHPOOL0001E": _("Storage pool %(name)s already exists"), "KCHPOOL0002E": _("Storage pool %(name)s does not exist"), @@ -318,4 +319,7 @@ "KCHSNAP0006E": _("Unable to delete snapshot '%(name)s' on virtual machine '%(vm)s'. Details: %(err)s"), "KCHSNAP0008E": _("Unable to retrieve current snapshot on virtual machine '%(vm)s'. Details: %(err)s"), "KCHSNAP0009E": _("Unable to revert virtual machine '%(vm)s' to snapshot '%(name)s'. Details: %(err)s"), + + "KCHCPUINF0001E": _("The number of vCPUs is too large for this system."), + "KCHCPUINF0002E": _("Invalid vCPU/topology combination."), } diff --git a/src/kimchi/model/cpuinfo.py b/src/kimchi/model/cpuinfo.py new file mode 100644 index 0000000..5e60ca5 --- /dev/null +++ b/src/kimchi/model/cpuinfo.py @@ -0,0 +1,215 @@ +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +import platform + +from distutils.version import LooseVersion +from math import sqrt, log +from xml.etree import ElementTree as ET + +from kimchi.exception import InvalidParameter, InvalidOperation +from kimchi.exception import IsoFormatError +from kimchi.isoinfo import IsoImage +from kimchi.osinfo import modern_version_bases +from kimchi.utils import kimchi_log, run_command + +ARCH = 'power' if platform.machine().startswith('ppc') else 'x86' +LEGACY_CPU_MAX = 4 + +class CPUInfoModel(object): + """ + Get information about a CPU for hyperthreading (on x86) + or SMT (on POWER) for logic when creating templates and VMs. + """ + + def __init__(self, **kargs): + self.conn = kargs['conn'] + + """ + Since there are so many similar-seeming variables: + - guest_threads_enabled = Can a user specify topology? + - sockets = total number of sockets in the system. While nothing + uses this value, it's part of topology so we'll track it. + - cores_present = total number of cores in the system + Note: Cores is often synonymous with CPUs. + - cores_available = cores online (in case some were offlined) + - cores_per_socket = max cores value for topology + - threads_per_core = max threads value for topology + """ + + self.conn = kargs['conn'] + + libvirt_topology = None + try: + connect = self.conn.get() + except Exception as e: + raise Exception("Unable to get qemu connection: %s" % e.message) + try: + xml = connect.getCapabilities() + capabilities = ET.fromstring(xml) + libvirt_topology = capabilities.find('host').find('cpu').\ + find('topology') + if libvirt_topology is None: + kimchi_log.info("cpu_info topology not supported.") + self.guest_threads_enabled = False + self.sockets = 0 + self.cores_present = 0 + self.cores_available = 0 + self.cores_per_socket = 0 + self.threads_per_core = 0 + self.max_threads = 0 + return + except Exception as e: + raise("Unable to get CPU topology capabilities: %s" % e.message) + + if ARCH == 'power': + # IBM PowerPC + self.guest_threads_enabled = True + out, error, rc = run_command(['ppc64_cpu', '--smt']) + if rc or 'on' in out: + # SMT has to be disabled for guest to use threads as CPUs. + # rc is always zero, whether SMT is off or on. + self.guest_threads_enabled = False + out, error, rc = run_command(['ppc64_cpu', '--cores-present']) + if not rc: + self.cores_present = int(out.split()[-1]) + out, error, rc = run_command(['ppc64_cpu', '--cores-on']) + if not rc: + self.cores_available = int(out.split()[-1]) + out, error, rc = run_command(['ppc64_cpu', '--threads-per-core']) + if not rc: + self.threads_per_core = int(out.split()[-1]) + self.sockets = self.cores_present/self.threads_per_core + self.cores_per_socket = self.cores_present/self.sockets + else: + # Intel or AMD + self.guest_threads_enabled = True + self.sockets = int(libvirt_topology.get('sockets')) + self.cores_per_socket = int(libvirt_topology.get('cores')) + self.cores_present = self.cores_per_socket * self.sockets + self.cores_available = self.cores_present + self.threads_per_core = int(libvirt_topology.get('threads')) + + def lookup(self, ident): + return { + 'guest_threads_enabled': self.guest_threads_enabled, + 'sockets': self.sockets, + 'cores_per_socket': self.cores_per_socket, + 'cores_present': self.cores_present, + 'cores_available': self.cores_available, + 'threads_per_core': self.threads_per_core, + } + + def get_rec_topology(self, iso_path, vcpus, req_threads): + """ + Kimchi will provide a recommended topoology based on the + number of virtual CPUs desired and guest OS. + + param vcpus: should be an integer + param iso_path: the path of the guest ISO + param thread_pref: a power of 2 (0 if x86). + return: topology numbers. None, if SMT/HT not enabled. + """ + # Adapted from http://stackoverflow.com/questions/6800193/ + # what-is-the-most-efficient-way-of-finding-all-the-factors- + # of-a-number-in-python + def valid_factors(n): + f_list = set(reduce(list.__add__, + ([i, n//i] for i in range(1, int(sqrt(n)) + 1) + if n % i == 0))) + return [x for x in f_list if x <= self.threads_per_core] + + def best_threads_per_core(): + fac = valid_factors(self.threads_per_core) + if log(self.threads_per_core, 2).is_integer(): + return [x for x in fac if log(x, 2).is_integer()] + else: + return fac + + def is_modern_distro(iso_path): + modern = False + try: + # Since some processors can have such large + # threads/core, there is the potential that a guest + # will not be able to support something like 8 vCPUS. + # @TODO: Are there any modern guests that can't + # do SMT8, or any older ones that can? + distro, version = IsoImage(iso_path).probe() + if distro in modern_version_bases[ARCH] and \ + LooseVersion(version) >= LooseVersion( + modern_version_bases[ARCH][distro]): + modern = True + except IsoFormatError: + pass + return modern + + if not self.guest_threads_enabled: + return None + if vcpus > self.cores_available * self.threads_per_core: + # This check should go into template create too? + raise InvalidParameter("KCHCPUINF0001E") + if req_threads > self.threads_per_core: + raise InvalidParameter("KCHCPUINF0002E") + # For an odd vCPU value, the only valid topology is + # (1, vcpus, 1), IOW, one thread on each core. + if (vcpus % 2) and vcpus > self.cores_available: + raise InvalidParameter("KCHCPUINF0002E") + + sockets = 1 + cores = 1 + threads = 1 + modern = is_modern_distro(iso_path) + if not modern and vcpus > LEGACY_CPU_MAX: + # We weren't preventing this scenario before, so don't + # make using topology more restrictive. Just log this. + kimchi_log.info('A vCPU count of %s may not be supported by' + ' the OS of guest %s' % (vcpus, iso_path)) + if vcpus % 2: + # odd vcpu num (has to be spread among cores) + cores = vcpus + return {'topology': {'sockets': sockets, 'cores': cores, + 'threads': threads}, + 'vcpus': vcpus} + + valid_tpcs = valid_factors(vcpus) + best_tpcs = reversed(sorted(best_threads_per_core())) + # If Power, and automatic was requested, set the SMT value first. + if req_threads == 0 and ARCH == 'power': + # For Power, better to use a factor of the tpc and + # pack threads into one core + for best_tpc in best_tpcs: + req_threads = best_tpc + if best_tpc in valid_tpcs: + break + # An SMT (or threads/core) value was requested. + elif req_threads != 0 and req_threads not in valid_tpcs: + raise InvalidOperation("KCHCPUINF0002E") + + # req_threads is no longer 0 if it was passed in as 0 + if ARCH is 'power': + threads = req_threads + cores = vcpus/threads + else: # x86 + # Because of the check above, we know that more vcpus + # than will fit on one core were requested. Spread + # them over as many cores as needed, as opposed to packing + # threads onto fewer cores. + # cores= vcpus/self.cores_available + # threads = self.threads_per_core + cores = min(self.cores_available, vcpus) + threads = vcpus / cores + + return {'topology': {'sockets': sockets, 'cores': cores, + 'threads': threads}, + 'vcpus': vcpus} diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py index 3b43b95..de57640 100644 --- a/src/kimchi/model/host.py +++ b/src/kimchi/model/host.py @@ -43,8 +43,8 @@ from kimchi.xmlutils.utils import xpath_get_text -HOST_STATS_INTERVAL = 1 +HOST_STATS_INTERVAL = 1 class HostModel(object): def __init__(self, **kargs): diff --git a/src/kimchi/model/templates.py b/src/kimchi/model/templates.py index 6e1a571..9f1b4b8 100644 --- a/src/kimchi/model/templates.py +++ b/src/kimchi/model/templates.py @@ -25,6 +25,8 @@ from kimchi.exception import InvalidOperation, InvalidParameter from kimchi.exception import NotFoundError, OperationFailed from kimchi.kvmusertests import UserTests +from kimchi.model.cpuinfo import CPUInfoModel +from kimchi.osinfo import common_spec from kimchi.utils import pool_name_from_uri from kimchi.utils import probe_file_permission_as_user from kimchi.vmtemplate import VMTemplate @@ -50,15 +52,24 @@ def create(self, params): cpu_info = params.get('cpu_info') if cpu_info: + vcpus = params.get('cpus') topology = cpu_info.get('topology') - # Check, even though currently only topology - # is supported. if topology: - sockets = topology['sockets'] - cores = topology['cores'] - threads = topology['threads'] - vcpus = params.get('cpus') - if vcpus is None: + sockets = topology.get('sockets') + cores = topology.get('cores') + threads = topology.get('threads') + if sockets is None and cores is None: + # The user wants kimchi to decide + if vcpus is None: + vcpus = max(common_spec['cpus'], threads) + rec_topology = CPUInfoModel( + conn=self.conn).get_rec_topology(iso, vcpus, threads) + if rec_topology: + params['cpus'] = rec_topology['vcpus'] + params['cpu_info']['topology'] = rec_topology['topology'] + else: + raise InvalidOperation("KCHTMPL0029E") + elif vcpus is None: params['cpus'] = sockets * cores * threads elif vcpus != sockets * cores * threads: raise InvalidParameter("KCHTMPL0025E") -- 1.9.3