Signed-off-by: Christy Perez <christy(a)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