Fix incomplete instance data returned after build failure

This change fixes a race in _cleanup_build_artifacts. We were updating
the instance mapping to point at the cell in which the instance was
created before the instance record was complete, i.e. before the related
BDMs and tags were created in the cell DB. Updating the instance mapping
exposes the cell's version of the instance to the API. If the API happened
to fetch it before it was finished being created it would return
incomplete data.

Co-Authored-By: Matt Riedemann <mriedem.os@gmail.com>

NOTE(mriedem): The unit test had to be modified slightly due to
not having change Ibc44e3b2261b314bb92062a88ca9ee6b81298dc3 in Pike.

Closes-Bug: #1820337
Change-Id: If966eb1161c842ff49aa530e4482dbca87b61a3e
(cherry picked from commit 64f6cbc912)
(cherry picked from commit eec39762cf)
(cherry picked from commit c8cb120964)
(cherry picked from commit ddc2c9118a)
This commit is contained in:
Matthew Booth 2019-03-22 11:47:32 +00:00 committed by Matt Riedemann
parent 8643ab5dc4
commit 3d85f7e581
2 changed files with 67 additions and 7 deletions

View File

@ -1237,13 +1237,6 @@ class ComputeTaskManager(base.Base):
'build_instances', updates, exc,
legacy_spec)
# TODO(mnaser): The cell mapping should already be populated by
# this point to avoid setting it below here.
inst_mapping = objects.InstanceMapping.get_by_instance_uuid(
context, instance.uuid)
inst_mapping.cell_mapping = cell
inst_mapping.save()
# In order to properly clean-up volumes when deleting a server in
# ERROR status with no host, we need to store BDMs in the same
# cell.
@ -1259,6 +1252,18 @@ class ComputeTaskManager(base.Base):
with nova_context.target_cell(context, cell) as cctxt:
self._create_tags(cctxt, instance.uuid, tags)
# NOTE(mdbooth): To avoid an incomplete instance record being
# returned by the API, the instance mapping must be
# created after the instance record is complete in
# the cell, and before the build request is
# destroyed.
# TODO(mnaser): The cell mapping should already be populated by
# this point to avoid setting it below here.
inst_mapping = objects.InstanceMapping.get_by_instance_uuid(
context, instance.uuid)
inst_mapping.cell_mapping = cell
inst_mapping.save()
# Be paranoid about artifacts being deleted underneath us.
try:
build_request.destroy()

View File

@ -1912,6 +1912,61 @@ class ConductorTaskTestCase(_BaseTaskTestCase, test_compute.BaseTestCase):
self.assertTrue(mock_build.called)
@mock.patch('nova.objects.InstanceMapping.get_by_instance_uuid')
def test_cleanup_build_artifacts(self, inst_map_get):
"""Simple test to ensure the order of operations in the cleanup method
is enforced.
"""
req_spec = fake_request_spec.fake_spec_obj()
build_req = fake_build_request.fake_req_obj(self.context)
instance = build_req.instance
bdms = objects.BlockDeviceMappingList(objects=[
objects.BlockDeviceMapping(instance_uuid=instance.uuid)])
tags = objects.TagList(objects=[objects.Tag(tag='test')])
cell1 = self.cell_mappings['cell1']
cell_mapping_cache = {instance.uuid: cell1}
err = exc.TooManyInstances('test')
# We need to assert that BDMs and tags are created in the cell DB
# before the instance mapping is updated.
def fake_create_block_device_mapping(*args, **kwargs):
inst_map_get.return_value.save.assert_not_called()
def fake_create_tags(*args, **kwargs):
inst_map_get.return_value.save.assert_not_called()
with test.nested(
mock.patch.object(self.conductor_manager,
'_set_vm_state_and_notify'),
mock.patch.object(self.conductor_manager,
'_create_block_device_mapping',
side_effect=fake_create_block_device_mapping),
mock.patch.object(self.conductor_manager, '_create_tags',
side_effect=fake_create_tags),
mock.patch.object(build_req, 'destroy'),
mock.patch.object(req_spec, 'destroy'),
) as (
_set_vm_state_and_notify, _create_block_device_mapping,
_create_tags, build_req_destroy, req_spec_destroy,
):
self.conductor_manager._cleanup_build_artifacts(
self.context, err, [instance], [build_req], [req_spec], bdms,
tags, cell_mapping_cache)
# Assert the various mock calls.
_set_vm_state_and_notify.assert_called_once_with(
test.MatchType(context.RequestContext), instance.uuid,
'build_instances',
{'vm_state': vm_states.ERROR, 'task_state': None}, err,
req_spec.to_legacy_request_spec_dict())
_create_block_device_mapping.assert_called_once_with(
cell1, instance.flavor, instance.uuid, bdms)
_create_tags.assert_called_once_with(
test.MatchType(context.RequestContext), instance.uuid, tags)
inst_map_get.return_value.save.assert_called_once_with()
self.assertEqual(cell1, inst_map_get.return_value.cell_mapping)
build_req_destroy.assert_called_once_with()
req_spec_destroy.assert_called_once_with()
@mock.patch('nova.objects.CellMapping.get_by_uuid')
def test_bury_in_cell0_no_cell0(self, mock_cm_get):
mock_cm_get.side_effect = exc.CellMappingNotFound(uuid='0')