From 509820f156e339e825f799d84b06cb11ac6b6096 Mon Sep 17 00:00:00 2001 From: Masahito Muroi Date: Tue, 21 Jan 2025 10:03:24 +0900 Subject: [PATCH] Use dict object for request_specs_dict in the _list_view The request_specs_dict in the _list_view is initialized as a defaultdict object in order to return empty string as default. But the request_spec_dict is replaced with a normal dict object in the v2.96 microversion, then if server list and RequestSpec missmatch happens by any reason, the List Server API and the List Server Detail API hit 500 Internal server error because of key error. This commit updates the req_spec_dict to use normal dict object, then it returns sentinel object if there is no appropriate request_spec object. Closes-Bug: #2095364 Change-Id: If282b8709954f276cb5d48114437809d771a9958 --- api-ref/source/parameters.yaml | 1 + nova/api/openstack/compute/views/servers.py | 23 +++--- .../api/openstack/compute/test_servers.py | 70 ++++++++++++++++++- .../notes/bug-2095364-ffbf67c0ae3f53b5.yaml | 15 ++++ 4 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/bug-2095364-ffbf67c0ae3f53b5.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 9a530cffbe7d..f644c84f9ea2 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -5721,6 +5721,7 @@ pinned_availability_zone: Also when default_schedule_zone config option set to specific AZ, in that case, instance would be pinned to that specific AZ, and instance will be scheduled on host belonging to pinned AZ. + In case of no pinned availability zone, this value is set to `null`. in: body type: string min_version: 2.96 diff --git a/nova/api/openstack/compute/views/servers.py b/nova/api/openstack/compute/views/servers.py index 8452d6bb1027..023669406eb5 100644 --- a/nova/api/openstack/compute/views/servers.py +++ b/nova/api/openstack/compute/views/servers.py @@ -14,8 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. -import collections - from oslo_log import log as logging from oslo_serialization import jsonutils @@ -41,6 +39,7 @@ from nova import utils LOG = logging.getLogger(__name__) +AZ_NOT_IN_REQUEST_SPEC = object() SCHED_HINTS_NOT_IN_REQUEST_SPEC = object() @@ -225,16 +224,20 @@ class ViewBuilder(common.ViewBuilder): return unknown_only def _get_pinned_az(self, context, instance, provided_az): - pinned_az = '' - if provided_az is not None: + if provided_az is AZ_NOT_IN_REQUEST_SPEC: + # Case the provided_az is pre fetched, but not specified + pinned_az = None + elif provided_az is not None: + # Case the provided_az is pre fetched, and specified pinned_az = provided_az else: + # Case the provided_az is not pre fethed. try: req_spec = objects.RequestSpec.get_by_instance_uuid( context, instance.uuid) pinned_az = req_spec.availability_zone except exception.RequestSpecNotFound: - pinned_az = '' + pinned_az = None return pinned_az def _get_scheduler_hints(self, context, instance, provided_sched_hints): @@ -543,15 +546,16 @@ class ViewBuilder(common.ViewBuilder): :returns: Server data in dictionary format """ req_specs = None - req_specs_dict = collections.defaultdict(str) + req_specs_dict = {} sched_hints_dict = {} if api_version_request.is_supported(request, min_version='2.96'): context = request.environ['nova.context'] instance_uuids = [s.uuid for s in servers] req_specs = objects.RequestSpec.get_by_instance_uuids( context, instance_uuids) - req_specs_dict = {req.instance_uuid: req.availability_zone - for req in req_specs} + req_specs_dict.update({req.instance_uuid: req.availability_zone + for req in req_specs + if req.availability_zone is not None}) if api_version_request.is_supported(request, min_version='2.100'): sched_hints_dict.update({ req.instance_uuid: req.scheduler_hints @@ -565,7 +569,8 @@ class ViewBuilder(common.ViewBuilder): show_host_status=show_host_status, show_sec_grp=show_sec_grp, bdms=bdms, cell_down_support=cell_down_support, - provided_az=req_specs_dict[server.uuid], + provided_az=req_specs_dict.get( + server.uuid, AZ_NOT_IN_REQUEST_SPEC), provided_sched_hints=sched_hints_dict.get( server.uuid, SCHED_HINTS_NOT_IN_REQUEST_SPEC) )["server"] diff --git a/nova/tests/unit/api/openstack/compute/test_servers.py b/nova/tests/unit/api/openstack/compute/test_servers.py index cf165fde2504..745bc6f891b9 100644 --- a/nova/tests/unit/api/openstack/compute/test_servers.py +++ b/nova/tests/unit/api/openstack/compute/test_servers.py @@ -8466,8 +8466,74 @@ class ServersViewBuilderTestV296(_ServersViewBuilderTest): availability_zone=self.instance.availability_zone)] req = self.req('/%s/servers' % self.project_id) - self.assertRaises(KeyError, self.view_builder.index, - req, self.instances, False) + output = self.view_builder.index(req, self.instances, False) + + self.assertEqual(2, len(output['servers'])) + + @mock.patch('nova.objects.RequestSpec.get_by_instance_uuids') + def test_list_detail_view_with_missing_request_specs(self, m_rs): + + self.instances = [ + self.instance, + self.create_instance(2, uuids.fake1, 'fake-server'), + self.create_instance(3, uuids.fake2, 'fake-server2') + ] + # First instance's request spec has pinned availability zone + # Second instance's request spec has no pinned availability zone + m_rs.return_value = [ + objects.RequestSpec( + instance_uuid=self.instance.uuid, + availability_zone=self.instance.availability_zone), + objects.RequestSpec( + instance_uuid=self.instance.uuid, + availability_zone=None) + ] + + req = self.req('/%s/servers/detail' % self.project_id) + output = self.view_builder.detail(req, self.instances, False) + + self.assertEqual(3, len(output['servers'])) + # first instance has pinned az + self.assertEqual('nova', + output['servers'][0]['pinned_availability_zone']) + # second or later has no pinned az + for s in output['servers'][1:]: + self.assertIsNone(s['pinned_availability_zone']) + + @mock.patch('nova.objects.RequestSpec.get_by_instance_uuid') + def test_show_view_with_missing_request_specs(self, m_rs): + + self.instances = [ + self.instance, + self.create_instance(2, uuids.fake1, 'fake-server'), + self.create_instance(3, uuids.fake2, 'fake-server2') + ] + # First instance's request spec has pinned availability zone + # Second instance's request spec has no pinned availability zone + m_rs.side_effect = [ + objects.RequestSpec( + instance_uuid=self.instance.uuid, + availability_zone=self.instance.availability_zone), + objects.RequestSpec( + instance_uuid=self.instance.uuid, + availability_zone=None), + exception.RequestSpecNotFound(instance_uuid='3') + ] + + # Instance show with request spec and pinned az + req = self.req('/%s/servers/1' % self.project_id) + output = self.view_builder.show(req, self.instances[0]) + self.assertEqual('nova', output['server']['pinned_availability_zone']) + + # Instance show with request spec and no pinned az + req = self.req('/%s/servers/2' % self.project_id) + output = self.view_builder.show(req, self.instances[1]) + self.assertIsNone(output['server']['pinned_availability_zone']) + + # Instance show without request spec + req = self.req('/%s/servers/3' % self.project_id) + output = self.view_builder.show(req, self.instances[2]) + self.assertIsNone(output['server']['pinned_availability_zone']) class ServersViewBuilderTestV2100(_ServersViewBuilderTest): diff --git a/releasenotes/notes/bug-2095364-ffbf67c0ae3f53b5.yaml b/releasenotes/notes/bug-2095364-ffbf67c0ae3f53b5.yaml new file mode 100644 index 000000000000..672863666d5f --- /dev/null +++ b/releasenotes/notes/bug-2095364-ffbf67c0ae3f53b5.yaml @@ -0,0 +1,15 @@ +--- +fixes: + - | + `Bug #2095364`_: Fixed the List Server API and the List Server Detail API + 500 Internal Server Error issue in v2.96 or later API microversion if + one or more instance has no request spec object. One usecase was when cloud + user tried to create instance which exceeded their quota, the request does + not create instance request spec. Once the no request spec instance is + created in cloud user project, the server list API and the list server + details API return 500 Internal Server Error for the project until the + cloud user deletes the no request spec object instance. + After this fix, the v2.96 or later returns `null` at the + `pinned_availability_zone` value if not specified. + + .. _Bug #2095364: https://launchpad.net/bugs/2095364