diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 9a530cffbe..f644c84f9e 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 8452d6bb10..023669406e 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 cf165fde25..745bc6f891 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 0000000000..672863666d --- /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