Source code for lago.providers.libvirt.cpu

#
# Copyright 2017 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
#
# Refer to the README and COPYING files for full details of the license
#

from __future__ import absolute_import

import logging
import os
from lxml import etree as ET

import lago.providers.libvirt.utils as utils
from lago.utils import LagoException, LagoInitException

LOGGER = logging.getLogger(__name__)


[docs]class CPU(object): def __init__(self, spec, host_cpu): """ Generate CPU XML node. Args: spec(dict): VM Lago spec host_cpu(lxml.etree.Element): Host cpu capabilities """ self.vcpu_set = False # TO-DO: deduce recommended defaults for vcpu_num from host CPU caps self.vcpu_num = '2' if spec.get('vcpu'): self.vcpu_set = True self.vcpu_num = spec['vcpu'] self.cpu_custom = spec.get('cpu_custom') self.cpu_model = spec.get('cpu_model') self.host_cpu = host_cpu self.memory = spec.get('memory') self.validate() self._cpu_xml = self.generate_cpu_xml() self._vcpu_xml = self.generate_vcpu_xml(self.vcpu_num) def __iter__(self): for node in (self.cpu_xml, self.vcpu_xml): yield node
[docs] def validate(self): """ Validate CPU-related VM spec are compatible Raises: :exc:`~LagoInitException`: if both 'cpu_model' and 'cpu_custom' are defined. """ if self.cpu_custom is not None and self.cpu_model: raise LagoInitException( 'Defining both cpu_model and cpu_custom is ' 'not supported.' )
@property def cpu_xml(self): return self._cpu_xml @property def vcpu_xml(self): return self._vcpu_xml @property def model(self): if self.cpu_model: return self.cpu_model elif not self.cpu_custom: return self.host_cpu.xpath('model')[0].text else: return self._cpu_xml.xpath('model')[0].text @property def vendor(self): if self.cpu_model: return LibvirtCPU.get_cpu_vendor(self.cpu_model) elif not self.cpu_custom: return self.host_cpu.xpath('vendor')[0].text else: return LibvirtCPU.get_cpu_vendor(self.model)
[docs] def generate_cpu_xml(self): """ Get CPU XML Returns: lxml.etree.Element: cpu node """ if self.cpu_custom: return self.generate_custom( cpu=self.cpu, vcpu_num=self.vcpu_num, fill_topology=self.vcpu_set ) elif self.cpu_model: return self.generate_exact( self.cpu_model, vcpu_num=self.vcpu_num, host_cpu=self.host_cpu ) else: return self.generate_host_passthrough(self.vcpu_num)
[docs] def generate_vcpu_xml(self, vcpu_num): """ generate_vcpu_xml Args: vcpu_num(int): number of virtual cpus Returns: lxml.etree.Element: vcpu XML node """ return self.generate_vcpu(vcpu_num=self.vcpu_num)
[docs] def generate_host_passthrough(self, vcpu_num): """ Generate host-passthrough XML cpu node Args: vcpu_num(str): number of virtual CPUs Returns: lxml.etree.Element: CPU XML node """ cpu = ET.Element('cpu', mode='host-passthrough') cpu.append(self.generate_topology(vcpu_num)) if int(vcpu_num) > 1: cpu.append(self.generate_numa(vcpu_num)) return cpu
[docs] def generate_custom(self, cpu, vcpu_num, fill_topology): """ Generate custom CPU model. This method attempts to convert the dict to XML, as defined by ``xmltodict.unparse`` method. Args: cpu(dict): CPU spec vcpu_num(int): number of virtual cpus fill_topology(bool): if topology is not defined in ``cpu`` and ``vcpu`` was not set, will add CPU topology to the generated CPU. Returns: lxml.etree.Element: CPU XML node Raises: :exc:`~LagoInitException`: when failed to convert dict to XML """ try: cpu = utils.dict_to_xml({'cpu': cpu}) except: # TO-DO: print an example here raise LagoInitException('conversion of \'cpu\' to XML failed') if not cpu.xpath('topology') and fill_topology: cpu.append(self.generate_topology(vcpu_num)) return cpu
[docs] def generate_exact(self, model, vcpu_num, host_cpu): """ Generate exact CPU model with nested virtualization CPU feature. Args: model(str): libvirt supported CPU model vcpu_num(int): number of virtual cpus host_cpu(lxml.etree.Element): the host CPU model Returns: lxml.etree.Element: CPU XML node """ nested = {'Intel': 'vmx', 'AMD': 'svm'} cpu = ET.Element('cpu', match='exact') ET.SubElement(cpu, 'model').text = model cpu.append(self.generate_topology(vcpu_num)) vendor = host_cpu.findtext('vendor') if not nested.get(vendor): LOGGER.debug( 'Unknown vendor: {0}, did not configure nested ' 'virtualization cpu flag on guest.'.format(vendor) ) return cpu model_vendor = LibvirtCPU.get_cpu_vendor(family=model) if vendor != model_vendor: LOGGER.debug( ( 'Not enabling nested virtualization feature, host ' 'vendor is: {0}, guest vendor: ' '{1}'.format(vendor, model_vendor) ) ) return cpu flag = nested[vendor] if host_cpu.find('feature/[@name="{0}"]'.format(flag)) is not None: cpu.append(self.generate_feature(name=flag)) else: LOGGER.debug( ( 'missing {0} cpu flag on host, nested ' 'virtualization will probably not ' 'work.' ).format(flag) ) return cpu
[docs] def generate_topology(self, vcpu_num, cores=1, threads=1): """ Generate CPU <topology> XML child Args: vcpu_num(str): number of virtual CPUs cores(int): number of cores threads(int): number of threads Returns: lxml.etree.Element: topology XML element """ return ET.Element( 'topology', sockets=str(vcpu_num), cores=str(cores), threads=str(threads), )
[docs] def generate_numa(self, vcpu_num): """ Generate guest CPU <numa> XML child Configures 1, 2 or 4 vCPUs per cell. Args: vcpu_num(str): number of virtual CPUs Returns: lxml.etree.Element: numa XML element """ if int(vcpu_num) == 2: # 2 vCPUs is a special case. # We wish to have 2 cells, # with 1 vCPU in each. # This is also the common case. total_cells = 2 cpus_per_cell = 1 elif int(vcpu_num) == 4: # 4 vCPU is a special case. # We wish to have 2 cells, # with 2 vCPUs in each. total_cells = 2 cpus_per_cell = 2 else: cell_info = divmod(int(vcpu_num), 4) if cell_info[1] == 0: # 4 vCPUs in each cell total_cells = cell_info[0] cpus_per_cell = 4 elif cell_info[1] == 2: # 2 vCPUs in each cell total_cells = (cell_info[0] * 2) + 1 cpus_per_cell = 2 else: # 1 vCPU per cell... total_cells = int(vcpu_num) cpus_per_cell = 1 numa = ET.Element('numa') memory_per_cell = divmod(int(self.memory), total_cells) LOGGER.debug( 'numa\n: cpus_per_cell: {0}, total_cells: {1}'.format( cpus_per_cell, total_cells ) ) for cell_id in range(0, total_cells): first_cpu_in_cell = cell_id * cpus_per_cell if cpus_per_cell == 1: cpus_in_cell = str(first_cpu_in_cell) else: cpus_in_cell = '{0}-{1}'.format( first_cpu_in_cell, first_cpu_in_cell + cpus_per_cell - 1 ) cell = ET.Element( 'cell', id=str(cell_id), cpus=cpus_in_cell, memory=str(memory_per_cell[0]), unit='MiB', ) numa.append(cell) LOGGER.debug('numa:\n{}'.format(ET.tostring(numa, pretty_print=True))) return numa
[docs] def generate_vcpu(self, vcpu_num): """ Generate <vcpu> domain XML child Args: vcpu_num(str): number of virtual cpus Returns: lxml.etree.Element: vcpu XML element """ vcpu = ET.Element('vcpu') vcpu.text = str(vcpu_num) return vcpu
[docs] def generate_feature(self, name, policy='require'): """ Generate CPU feature element Args: name(str): feature name policy(str): libvirt feature policy Returns: lxml.etree.Element: feature XML element """ return ET.Element('feature', policy=policy, name=name)
[docs]class LibvirtCPU(object): """Query data from /usr/share/libvirt/cpu_map.xml"""
[docs] @classmethod def get_cpu_vendor(cls, family, arch='x86'): """ Get CPU vendor, if vendor is not available will return 'generic' Args: family(str): CPU family arch(str): CPU arch Returns: str: CPU vendor if found otherwise 'generic' """ props = cls.get_cpu_props(family, arch) vendor = 'generic' try: vendor = props.xpath('vendor/@name')[0] except IndexError: pass return vendor
[docs] @classmethod def get_cpu_props(cls, family, arch='x86'): """ Get CPU info XML Args: family(str): CPU family arch(str): CPU arch Returns: lxml.etree.Element: CPU xml Raises: :exc:`~LagoException`: If no such CPU family exists """ cpus = cls.get_cpus_by_arch(arch) try: return cpus.xpath('model[@name="{0}"]'.format(family))[0] except IndexError: raise LagoException('No such CPU family: {0}'.format(family))
[docs] @classmethod def get_cpus_by_arch(cls, arch): """ Get all CPUs info by arch Args: arch(str): CPU architecture Returns: lxml.etree.element: CPUs by arch XML Raises: :exc:`~LagoException`: If no such ARCH is found """ cpu_map_xml = "/usr/share/libvirt/cpu_map.xml" cpu_map_dir = "/usr/share/libvirt/cpu_map/" cpu_map_index_xml = cpu_map_dir + "index.xml" if not os.path.exists(cpu_map_xml): cpu_xml = ET.ElementTree( ET.fromstring(create_xml_map(cpu_map_index_xml, cpu_map_dir)) ) else: with open(cpu_map_xml, 'r') as cpu_map: cpu_xml = ET.parse(cpu_map) try: return cpu_xml.xpath('/cpus/arch[@name="{0}"]'.format(arch))[0] except IndexError: raise LagoException('No such arch: {0}'.format(arch))
[docs]def create_xml_map(cpu_map_index_xml, cpu_map_dir): xml_list = [] if os.path.exists(cpu_map_index_xml): with open(cpu_map_index_xml) as fp: line = fp.readline() while line: if "include" in line: tree = ET.fromstring(line) for child in tree.getiterator(): if child.tag == "include": filename = child.attrib["filename"] with open( cpu_map_dir + filename, 'r' ) as content_file: for content_line in content_file: if "cpus" not in content_line: xml_list.append(content_line) else: xml_list.append(line) line = fp.readline() return ''.join(xml_list)