diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index fe249ad48157..6df249e9a018 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -689,6 +689,7 @@ class ServersController(wsgi.Controller): exception.ImageNUMATopologyCPUDuplicates, exception.ImageNUMATopologyCPUsUnassigned, exception.ImageNUMATopologyMemoryOutOfRange, + exception.InvalidNUMANodesNumber, exception.InstanceGroupNotFound, exception.PciRequestAliasNotDefined, exception.SnapshotNotFound, diff --git a/nova/exception.py b/nova/exception.py index b0dcbeff4024..c9e52120557d 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -372,6 +372,11 @@ class InvalidStrTime(Invalid): msg_fmt = _("Invalid datetime string: %(reason)s") +class InvalidNUMANodesNumber(Invalid): + msg_fmt = _("The property 'numa_nodes' cannot be '%(nodes)s'. " + "It must be a number greater than 0") + + class InvalidName(Invalid): msg_fmt = _("An invalid 'name' value was provided. " "The name must be: %(reason)s") diff --git a/nova/tests/unit/api/openstack/compute/test_serversV21.py b/nova/tests/unit/api/openstack/compute/test_serversV21.py index 5b41393a218d..680a2d2828d6 100644 --- a/nova/tests/unit/api/openstack/compute/test_serversV21.py +++ b/nova/tests/unit/api/openstack/compute/test_serversV21.py @@ -3255,6 +3255,14 @@ class ServersControllerCreateTest(test.TestCase): self.controller.create, self.req, body=self.body) + @mock.patch.object(compute_api.API, 'create', + side_effect=exception.InvalidNUMANodesNumber( + details='')) + def test_create_instance_raise_invalid_numa_nodes(self, mock_create): + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.create, + self.req, body=self.body) + @mock.patch.object(compute_api.API, 'create', side_effect=exception.InvalidBDMFormat(details='')) def test_create_instance_raise_invalid_bdm_format(self, mock_create): diff --git a/nova/tests/unit/virt/test_hardware.py b/nova/tests/unit/virt/test_hardware.py index 8f75879af264..336a48ac5735 100644 --- a/nova/tests/unit/virt/test_hardware.py +++ b/nova/tests/unit/virt/test_hardware.py @@ -856,6 +856,36 @@ class NUMATopologyTest(test.NoDBTestCase): memory=2048, pagesize=2048) ]), }, + { + # a nodes number of zero should lead to an + # exception + "flavor": objects.Flavor(vcpus=8, memory_mb=2048, extra_specs={ + "hw:numa_nodes": 0 + }), + "image": { + }, + "expect": exception.InvalidNUMANodesNumber, + }, + { + # a negative nodes number should lead to an + # exception + "flavor": objects.Flavor(vcpus=8, memory_mb=2048, extra_specs={ + "hw:numa_nodes": -1 + }), + "image": { + }, + "expect": exception.InvalidNUMANodesNumber, + }, + { + # a nodes number not numeric should lead to an + # exception + "flavor": objects.Flavor(vcpus=8, memory_mb=2048, extra_specs={ + "hw:numa_nodes": 'x' + }), + "image": { + }, + "expect": exception.InvalidNUMANodesNumber, + }, { # vcpus is not a multiple of nodes, so it # is an error to not provide cpu/mem mapping diff --git a/nova/virt/hardware.py b/nova/virt/hardware.py index d8eff008ad5d..558bb5b4135e 100644 --- a/nova/virt/hardware.py +++ b/nova/virt/hardware.py @@ -1178,6 +1178,18 @@ def _add_cpu_pinning_constraint(flavor, image_meta, numa_topology): return numa_topology +def _validate_numa_nodes(nodes): + """Validate NUMA nodes number + + :param nodes: The number of NUMA nodes + :raises: exception.InvalidNUMANodesNumber if the given + parameter is not a number or less than 1 + """ + if nodes is not None and (not strutils.is_int_like(nodes) or + int(nodes) < 1): + raise exception.InvalidNUMANodesNumber(nodes=nodes) + + # TODO(sahid): Move numa related to hardward/numa.py def numa_get_constraints(flavor, image_meta): """Return topology related to input request @@ -1189,6 +1201,8 @@ def numa_get_constraints(flavor, image_meta): image properties are not correctly specified, or exception.ImageNUMATopologyForbidden if an attempt is made to override flavor settings with image properties. + exception.InvalidNUMANodesNumber if the number of NUMA + nodes is less than 1 (or not an integer). :returns: InstanceNUMATopology or None """ @@ -1196,12 +1210,14 @@ def numa_get_constraints(flavor, image_meta): nodes = flavor.get('extra_specs', {}).get("hw:numa_nodes") props = image_meta.properties if nodes is not None: + _validate_numa_nodes(nodes) if props.obj_attr_is_set("hw_numa_nodes"): raise exception.ImageNUMATopologyForbidden( name='hw_numa_nodes') nodes = int(nodes) else: nodes = props.get("hw_numa_nodes") + _validate_numa_nodes(nodes) pagesize = _numa_get_pagesize_constraints( flavor, image_meta)