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
This commit is contained in:
Masahito Muroi
2025-01-21 10:03:24 +09:00
parent 29d17552a7
commit 509820f156
4 changed files with 98 additions and 11 deletions

View File

@@ -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

View File

@@ -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"]

View File

@@ -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):

View File

@@ -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