From f31f79249b1af0b5ccc1b8c0a1e6764056657aca Mon Sep 17 00:00:00 2001 From: Yaniv Kaul Date: Thu, 22 Feb 2018 11:02:43 +0200 Subject: [PATCH] Add NUMA awareness to Lago. Logic: If there's >1 vCPU: 2 vCPUs - 2 NUMA nodes, each has 1 vCPU 4 vCPUS - 2 NUMA nodes, each has 1 vCPU Otherwise: If num of vCPUs (N) divisible by 4: N/4 NUMA nodes, each has 4 vCPUs If num of vCPUs divisible by 2: N/2 NUMA nodes, each has 2 vCPUs Otherwise, N NUMA nodes, each has 1 vCPU. Tested with 1 - 6 vCPUs. --- lago/providers/libvirt/cpu.py | 88 +++++++++++++++-- tests/unit/lago/providers/libvirt/test_cpu.py | 95 ++++++++++++++----- 2 files changed, 152 insertions(+), 31 deletions(-) diff --git a/lago/providers/libvirt/cpu.py b/lago/providers/libvirt/cpu.py index 75f8177c..0674e5bc 100644 --- a/lago/providers/libvirt/cpu.py +++ b/lago/providers/libvirt/cpu.py @@ -44,9 +44,10 @@ def __init__(self, spec, host_cpu): if spec.get('vcpu'): self.vcpu_set = True self.vcpu_num = spec['vcpu'] - self.cpu = spec.get('cpu_custom') + 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() @@ -61,10 +62,10 @@ def validate(self): Validate CPU-related VM spec are compatible Raises: - :exc:`~LagoInitException`: if both 'cpu_model' and 'cpu' are + :exc:`~LagoInitException`: if both 'cpu_model' and 'cpu_custom' are defined. """ - if self.cpu is not None and self.cpu_model: + if self.cpu_custom is not None and self.cpu_model: raise LagoInitException( 'Defining both cpu_model and cpu_custom is ' 'not supported.' @@ -82,7 +83,7 @@ def vcpu_xml(self): def model(self): if self.cpu_model: return self.cpu_model - elif not self.cpu: + elif not self.cpu_custom: return self.host_cpu.xpath('model')[0].text else: return self._cpu_xml.xpath('model')[0].text @@ -91,7 +92,7 @@ def model(self): def vendor(self): if self.cpu_model: return LibvirtCPU.get_cpu_vendor(self.cpu_model) - elif not self.cpu: + elif not self.cpu_custom: return self.host_cpu.xpath('vendor')[0].text else: return LibvirtCPU.get_cpu_vendor(self.model) @@ -103,7 +104,7 @@ def generate_cpu_xml(self): Returns: lxml.etree.Element: cpu node """ - if self.cpu: + if self.cpu_custom: return self.generate_custom( cpu=self.cpu, vcpu_num=self.vcpu_num, @@ -134,7 +135,7 @@ def generate_host_passthrough(self, vcpu_num): Generate host-passthrough XML cpu node Args: - vcpu_num(int): number of virtual CPUs + vcpu_num(str): number of virtual CPUs Returns: lxml.etree.Element: CPU XML node @@ -142,6 +143,8 @@ def generate_host_passthrough(self, vcpu_num): cpu = ET.Element('cpu', mode='host-passthrough') cpu.append(self.generate_topology(vcpu_num)) + if vcpu_num > 1: + cpu.append(self.generate_numa(vcpu_num)) return cpu def generate_custom(self, cpu, vcpu_num, fill_topology): @@ -229,7 +232,7 @@ def generate_topology(self, vcpu_num, cores=1, threads=1): Generate CPU XML child Args: - vcpu_num(int): number of virtual CPUs + vcpu_num(str): number of virtual CPUs cores(int): number of cores threads(int): number of threads @@ -244,12 +247,79 @@ def generate_topology(self, vcpu_num, cores=1, threads=1): threads=str(threads), ) + def generate_numa(self, vcpu_num): + """ + Generate guest CPU 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 xrange(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 + def generate_vcpu(self, vcpu_num): """ Generate domain XML child Args: - vcpu_num(int): number of virtual cpus + vcpu_num(str): number of virtual cpus Returns: lxml.etree.Element: vcpu XML element diff --git a/tests/unit/lago/providers/libvirt/test_cpu.py b/tests/unit/lago/providers/libvirt/test_cpu.py index fc7dd967..13cb2eda 100644 --- a/tests/unit/lago/providers/libvirt/test_cpu.py +++ b/tests/unit/lago/providers/libvirt/test_cpu.py @@ -52,7 +52,7 @@ def test_generate_topology(self): } for tup in permutations(range(1, 4), 3) ] - empty_cpu = cpu.CPU(spec={}, host_cpu=None) + empty_cpu = cpu.CPU(spec={'memory': 2048}, host_cpu=None) for comb in combs: self.assertXmlEquivalentOutputs( ET.tostring(empty_cpu.generate_topology(**comb)), @@ -62,15 +62,54 @@ def test_generate_topology(self): def test_generate_host_passthrough(self): _xml = """ - + {1} """ - empty_cpu = cpu.CPU(spec={}, host_cpu=None) - for vcpu_num in [1, 9, 11, 120]: - self.assertXmlEquivalentOutputs( - ET.tostring(empty_cpu.generate_host_passthrough(vcpu_num)), - _xml.format(vcpu_num) - ) + empty_cpu = cpu.CPU(spec={'memory': 2048}, host_cpu=None) + vcpu_num = 1 + self.assertXmlEquivalentOutputs( + ET.tostring(empty_cpu.generate_host_passthrough(vcpu_num)), + _xml.format(vcpu_num, '') + ) + + _numa2 = """ + + + + + """ + empty_cpu = cpu.CPU(spec={'memory': 2047}, host_cpu=None) + vcpu_num = 2 + self.assertXmlEquivalentOutputs( + ET.tostring(empty_cpu.generate_host_passthrough(vcpu_num)), + _xml.format(vcpu_num, _numa2) + ) + + _numa3 = """ + + + + + + """ + vcpu_num = 3 + self.assertXmlEquivalentOutputs( + ET.tostring(empty_cpu.generate_host_passthrough(vcpu_num)), + _xml.format(vcpu_num, _numa3) + ) + + _numa8 = """ + + + + + """ + empty_cpu = cpu.CPU(spec={'memory': 4096}, host_cpu=None) + vcpu_num = 8 + self.assertXmlEquivalentOutputs( + ET.tostring(empty_cpu.generate_host_passthrough(vcpu_num)), + _xml.format(vcpu_num, _numa8) + ) def test_generate_exact_intel_vmx_intel_vmx(self, vcpu=2, model='Penryn'): @@ -93,11 +132,12 @@ def test_generate_exact_intel_vmx_intel_vmx(self, vcpu=2, model='Penryn'): """ ) - empty_cpu = cpu.CPU(spec={}, host_cpu=None) + empty_cpu = cpu.CPU(spec={'memory': 2048}, host_cpu=None) self.assertXmlEquivalentOutputs( ET.tostring( - empty_cpu. - generate_exact(model=model, vcpu_num=vcpu, host_cpu=host) + empty_cpu.generate_exact( + model=model, vcpu_num=vcpu, host_cpu=host + ) ), _xml ) @@ -120,11 +160,12 @@ def test_generate_exact_intel_novmx(self, vcpu=2, model='Penryn'): """ ) - empty_cpu = cpu.CPU(spec={}, host_cpu=None) + empty_cpu = cpu.CPU(spec={'memory': 2048}, host_cpu=None) self.assertXmlEquivalentOutputs( ET.tostring( - empty_cpu. - generate_exact(model=model, vcpu_num=vcpu, host_cpu=host) + empty_cpu.generate_exact( + model=model, vcpu_num=vcpu, host_cpu=host + ) ), _xml ) @@ -147,11 +188,12 @@ def test_generate_exact_vendor_mismatch(self, vcpu=2, model='Opteron_G2'): """ ) - empty_cpu = cpu.CPU(spec={}, host_cpu=None) + empty_cpu = cpu.CPU(spec={'memory': 2048}, host_cpu=None) self.assertXmlEquivalentOutputs( ET.tostring( - empty_cpu. - generate_exact(model=model, vcpu_num=vcpu, host_cpu=host) + empty_cpu.generate_exact( + model=model, vcpu_num=vcpu, host_cpu=host + ) ), _xml ) @@ -174,25 +216,34 @@ def test_generate_exact_unknown_vendor(self, vcpu=2, model='Westmere'): """ ) - empty_cpu = cpu.CPU(spec={}, host_cpu=None) + empty_cpu = cpu.CPU(spec={'memory': 2048}, host_cpu=None) self.assertXmlEquivalentOutputs( ET.tostring( - empty_cpu. - generate_exact(model=model, vcpu_num=vcpu, host_cpu=host) + empty_cpu.generate_exact( + model=model, vcpu_num=vcpu, host_cpu=host + ) ), _xml ) def test_init_default(self): - spec = {} + spec = {'memory': 2048} _xml = """ + + + + """ def_cpu = cpu.CPU(spec=spec, host_cpu=self.get_host_cpu()) self.assertXmlEquivalentOutputs(ET.tostring(def_cpu.cpu_xml), _xml) def test_init_custom_and_model_not_allowed(self): - spec = {'cpu_custom': 'custom', 'cpu_model': 'DummyModel'} + spec = { + 'cpu_custom': 'custom', + 'cpu_model': 'DummyModel', + 'memory': 2048 + } with pytest.raises(LagoInitException): cpu.CPU(spec=spec, host_cpu=self.get_host_cpu())