diff --git a/nova/exception.py b/nova/exception.py index 879dd92732b2..b917d3a13933 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1827,3 +1827,8 @@ class MemoryPageSizeNotSupported(Invalid): class CPUPinningInvalid(Invalid): msg_fmt = _("Cannot pin/unpin cpus %(requested)s from the following " "pinned set %(pinned)s") + + +class ImageCPUPinningForbidden(Invalid): + msg_fmt = _("Image property 'hw_cpu_policy' is not permitted to override " + "CPU pinning policy set against the flavor") diff --git a/nova/tests/unit/virt/test_hardware.py b/nova/tests/unit/virt/test_hardware.py index dfcb7d7cbe22..492a02ac25bf 100644 --- a/nova/tests/unit/virt/test_hardware.py +++ b/nova/tests/unit/virt/test_hardware.py @@ -971,6 +971,82 @@ class NUMATopologyTest(test.NoDBTestCase): }, "expect": exception.ImageNUMATopologyForbidden, }, + { + # NUMA + CPU pinning requested in the flavor + "flavor": objects.Flavor(vcpus=4, memory_mb=2048, + extra_specs={ + "hw:numa_nodes": 2, "hw:cpu_policy": "dedicated" + }), + "image": { + }, + "expect": objects.InstanceNUMATopology(cells= + [ + objects.InstanceNUMACell( + id=0, cpuset=set([0, 1]), memory=1024, + cpu_pinning={}), + objects.InstanceNUMACell( + id=1, cpuset=set([2, 3]), memory=1024, + cpu_pinning={})]) + }, + { + # no NUMA + CPU pinning requested in the flavor + "flavor": objects.Flavor(vcpus=4, memory_mb=2048, + extra_specs={ + "hw:cpu_policy": "dedicated" + }), + "image": { + }, + "expect": objects.InstanceNUMATopology(cells= + [ + objects.InstanceNUMACell( + id=0, cpuset=set([0, 1, 2, 3]), memory=2048, + cpu_pinning={})]) + }, + { + # NUMA + CPU pinning requested in the image + "flavor": objects.Flavor(vcpus=4, memory_mb=2048, + extra_specs={ + "hw:numa_nodes": 2 + }), + "image": { + "properties": { + "hw_cpu_policy": "dedicated"} + }, + "expect": objects.InstanceNUMATopology(cells= + [ + objects.InstanceNUMACell( + id=0, cpuset=set([0, 1]), memory=1024, + cpu_pinning={}), + objects.InstanceNUMACell( + id=1, cpuset=set([2, 3]), memory=1024, + cpu_pinning={})]) + }, + { + # no NUMA + CPU pinning requested in the image + "flavor": objects.Flavor(vcpus=4, memory_mb=2048, + extra_specs={}), + "image": { + "properties": { + "hw_cpu_policy": "dedicated"} + }, + "expect": objects.InstanceNUMATopology(cells= + [ + objects.InstanceNUMACell( + id=0, cpuset=set([0, 1, 2, 3]), memory=2048, + cpu_pinning={})]) + }, + { + # Invalid CPU pinning override + "flavor": objects.Flavor(vcpus=4, memory_mb=2048, + extra_specs={ + "hw:numa_nodes": 2, "hw:cpu_policy": "shared" + }), + "image": { + "properties": { + "hw_cpu_policy": "dedicated"} + }, + "expect": exception.ImageCPUPinningForbidden, + }, ] for testitem in testdata: @@ -989,12 +1065,16 @@ class NUMATopologyTest(test.NoDBTestCase): self.assertEqual(len(testitem["expect"].cells), len(topology.cells)) for i in range(len(topology.cells)): + self.assertEqual(testitem["expect"].cells[i].id, + topology.cells[i].id) self.assertEqual(testitem["expect"].cells[i].cpuset, topology.cells[i].cpuset) self.assertEqual(testitem["expect"].cells[i].memory, topology.cells[i].memory) self.assertEqual(testitem["expect"].cells[i].pagesize, topology.cells[i].pagesize) + self.assertEqual(testitem["expect"].cells[i].cpu_pinning, + topology.cells[i].cpu_pinning) def test_host_usage_contiguous(self): hpages0_4K = objects.NUMAPagesTopology(size_kb=4, total=256, used=0) diff --git a/nova/virt/hardware.py b/nova/virt/hardware.py index aaaf6f9bac52..80860b872922 100644 --- a/nova/virt/hardware.py +++ b/nova/virt/hardware.py @@ -1050,6 +1050,37 @@ def _numa_get_constraints_auto(nodes, flavor, image_meta): return objects.InstanceNUMATopology(cells=cells) +def _add_cpu_pinning_constraint(flavor, image_meta, numa_topology): + flavor_pinning = flavor.get('extra_specs', {}).get("hw:cpu_policy") + image_pinning = image_meta.get('properties', {}).get("hw_cpu_policy") + if flavor_pinning == "dedicated": + requested = True + elif flavor_pinning == "shared": + if image_pinning == "dedicated": + raise exception.ImageCPUPinningForbidden() + requested = False + else: + requested = image_pinning == "dedicated" + + if not requested: + return numa_topology + + if numa_topology: + # NOTE(ndipanov) Setting the cpu_pinning attribute to a non-None value + # means CPU pinning was requested + for cell in numa_topology.cells: + cell.cpu_pinning = {} + return numa_topology + else: + single_cell = objects.InstanceNUMACell( + id=0, + cpuset=set(range(flavor['vcpus'])), + memory=flavor['memory_mb'], + cpu_pinning={}) + numa_topology = objects.InstanceNUMATopology(cells=[single_cell]) + return numa_topology + + # TODO(sahid): Move numa related to hardward/numa.py def numa_get_constraints(flavor, image_meta): """Return topology related to input request @@ -1064,7 +1095,7 @@ def numa_get_constraints(flavor, image_meta): pagesize = _numa_get_pagesize_constraints( flavor, image_meta) - topology = None + numa_topology = None if nodes or pagesize: nodes = nodes and int(nodes) or 1 # We'll pick what path to go down based on whether @@ -1074,16 +1105,16 @@ def numa_get_constraints(flavor, image_meta): flavor, image_meta, "numa_cpus.0") is None if auto: - topology = _numa_get_constraints_auto( + numa_topology = _numa_get_constraints_auto( nodes, flavor, image_meta) else: - topology = _numa_get_constraints_manual( + numa_topology = _numa_get_constraints_manual( nodes, flavor, image_meta) # We currently support same pagesize for all cells. - [setattr(c, 'pagesize', pagesize) for c in topology.cells] + [setattr(c, 'pagesize', pagesize) for c in numa_topology.cells] - return topology + return _add_cpu_pinning_constraint(flavor, image_meta, numa_topology) class VirtNUMALimitTopology(VirtNUMATopology):