Merge "compute: Do not allow rescue attempts using volume snapshot images" into stable/ussuri

This commit is contained in:
Zuul 2020-08-26 03:38:38 +00:00 committed by Gerrit Code Review
commit 87b7c6685c
5 changed files with 102 additions and 6 deletions

View File

@ -70,7 +70,10 @@ class RescueController(wsgi.Controller):
'rescue', id)
except exception.InvalidVolume as volume_error:
raise exc.HTTPConflict(explanation=volume_error.format_message())
except exception.InstanceNotRescuable as non_rescuable:
except (
exception.InstanceNotRescuable,
exception.UnsupportedRescueImage,
) as non_rescuable:
raise exc.HTTPBadRequest(
explanation=non_rescuable.format_message())

View File

@ -69,6 +69,7 @@ from nova import objects
from nova.objects import block_device as block_device_obj
from nova.objects import external_event as external_event_obj
from nova.objects import fields as fields_obj
from nova.objects import image_meta as image_meta_obj
from nova.objects import keypair as keypair_obj
from nova.objects import quotas as quotas_obj
from nova.pci import request as pci_request
@ -4205,6 +4206,27 @@ class API(base.Base):
allow_bfv_rescue=False):
"""Rescue the given instance."""
if rescue_image_ref:
try:
image_meta = image_meta_obj.ImageMeta.from_image_ref(
context, self.image_api, rescue_image_ref)
except (exception.ImageNotFound, exception.ImageBadRequest):
LOG.warning("Failed to fetch rescue image metadata using "
"image_ref %(image_ref)s",
{'image_ref': rescue_image_ref})
raise exception.UnsupportedRescueImage(
image=rescue_image_ref)
# FIXME(lyarwood): There is currently no support for rescuing
# instances using a volume snapshot so fail here before we cast to
# the compute.
if image_meta.properties.get('img_block_device_mapping'):
LOG.warning("Unable to rescue an instance using a volume "
"snapshot image with img_block_device_mapping "
"image properties set")
raise exception.UnsupportedRescueImage(
image=rescue_image_ref)
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
context, instance.uuid)
for bdm in bdms:

View File

@ -1402,6 +1402,10 @@ class UnsupportedRescueDevice(Invalid):
msg_fmt = _("Requested rescue device '%(device)s' is not supported")
class UnsupportedRescueImage(Invalid):
msg_fmt = _("Requested rescue image '%(image)s' is not supported")
class Base64Exception(NovaException):
msg_fmt = _("Invalid Base 64 data for file %(path)s")

View File

@ -140,6 +140,16 @@ class RescueTestV21(test.NoDBTestCase):
self.fake_req, UUID, body=body)
self.assertTrue(mock_rescue.called)
@mock.patch.object(compute.api.API, "rescue",
side_effect=exception.UnsupportedRescueImage(image='fake'))
def test_rescue_raises_unsupported_image(self, mock_rescue):
body = dict(rescue=None)
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._rescue,
self.fake_req, UUID, body=body)
self.assertTrue(mock_rescue.called)
def test_rescue_with_bad_image_specified(self):
body = {"rescue": {"adminPass": "ABC123",
"rescue_image_ref": "img-id"}}

View File

@ -50,6 +50,7 @@ from nova import objects
from nova.objects import base as obj_base
from nova.objects import block_device as block_device_obj
from nova.objects import fields as fields_obj
from nova.objects import image_meta as image_meta_obj
from nova.objects import quotas as quotas_obj
from nova.objects import security_group as secgroup_obj
from nova.servicegroup import api as servicegroup_api
@ -5177,6 +5178,7 @@ class _ComputeAPIUnitTestMixIn(object):
def _test_rescue(self, vm_state=vm_states.ACTIVE, rescue_password=None,
rescue_image=None, clean_shutdown=True):
instance = self._create_instance_obj(params={'vm_state': vm_state})
rescue_image_meta_obj = image_meta_obj.ImageMeta.from_dict({})
bdms = []
with test.nested(
mock.patch.object(objects.BlockDeviceMappingList,
@ -5186,10 +5188,12 @@ class _ComputeAPIUnitTestMixIn(object):
mock.patch.object(instance, 'save'),
mock.patch.object(self.compute_api, '_record_action_start'),
mock.patch.object(self.compute_api.compute_rpcapi,
'rescue_instance')
'rescue_instance'),
mock.patch.object(objects.ImageMeta, 'from_image_ref',
return_value=rescue_image_meta_obj),
) as (
bdm_get_by_instance_uuid, volume_backed_inst, instance_save,
record_action_start, rpcapi_rescue_instance
record_action_start, rpcapi_rescue_instance, mock_find_image_ref,
):
self.compute_api.rescue(self.context, instance,
rescue_password=rescue_password,
@ -5210,6 +5214,9 @@ class _ComputeAPIUnitTestMixIn(object):
rescue_password=rescue_password,
rescue_image_ref=rescue_image,
clean_shutdown=clean_shutdown)
if rescue_image:
mock_find_image_ref.assert_called_once_with(
self.context, self.compute_api.image_api, rescue_image)
def test_rescue_active(self):
self._test_rescue()
@ -5250,6 +5257,7 @@ class _ComputeAPIUnitTestMixIn(object):
rpcapi_unrescue_instance.assert_called_once_with(
self.context, instance=instance)
@mock.patch('nova.objects.image_meta.ImageMeta.from_image_ref')
@mock.patch('nova.objects.compute_node.ComputeNode'
'.get_by_host_and_nodename')
@mock.patch('nova.compute.utils.is_volume_backed_instance',
@ -5257,8 +5265,7 @@ class _ComputeAPIUnitTestMixIn(object):
@mock.patch('nova.objects.block_device.BlockDeviceMappingList'
'.get_by_instance_uuid')
def test_rescue_bfv_with_required_trait(self, mock_get_bdms,
mock_is_volume_backed,
mock_get_cn):
mock_is_volume_backed, mock_get_cn, mock_image_meta_obj_from_ref):
instance = self._create_instance_obj()
bdms = objects.BlockDeviceMappingList(objects=[
objects.BlockDeviceMapping(
@ -5266,6 +5273,8 @@ class _ComputeAPIUnitTestMixIn(object):
destination_type='volume', volume_type=None,
snapshot_id=None, volume_id=uuids.volume_id,
volume_size=None)])
rescue_image_meta_obj = image_meta_obj.ImageMeta.from_dict({})
with test.nested(
mock.patch.object(self.compute_api.placementclient,
'get_provider_traits'),
@ -5279,8 +5288,9 @@ class _ComputeAPIUnitTestMixIn(object):
mock_get_traits, mock_get_volume, mock_check_attached,
mock_instance_save, mock_record_start, mock_rpcapi_rescue
):
# Mock out the returned compute node, bdms and volume
# Mock out the returned compute node, image, bdms and volume
mock_get_cn.return_value = mock.Mock(uuid=uuids.cn)
mock_image_meta_obj_from_ref.return_value = rescue_image_meta_obj
mock_get_bdms.return_value = bdms
mock_get_volume.return_value = mock.sentinel.volume
@ -5406,6 +5416,53 @@ class _ComputeAPIUnitTestMixIn(object):
mock_is_volume_backed.assert_called_once_with(
self.context, instance, bdms)
@mock.patch('nova.objects.image_meta.ImageMeta.from_image_ref')
def test_rescue_from_image_ref_failure(self, mock_image_meta_obj_from_ref):
instance = self._create_instance_obj()
mock_image_meta_obj_from_ref.side_effect = [
exception.ImageNotFound(image_id=mock.sentinel.rescue_image_ref),
exception.ImageBadRequest(
image_id=mock.sentinel.rescue_image_ref, response='bar')]
# Assert that UnsupportedRescueImage is raised when from_image_ref
# returns exception.ImageNotFound
self.assertRaises(exception.UnsupportedRescueImage,
self.compute_api.rescue, self.context, instance,
rescue_image_ref=mock.sentinel.rescue_image_ref)
# Assert that UnsupportedRescueImage is raised when from_image_ref
# returns exception.ImageBadRequest
self.assertRaises(exception.UnsupportedRescueImage,
self.compute_api.rescue, self.context, instance,
rescue_image_ref=mock.sentinel.rescue_image_ref)
# Assert that we called from_image_ref using the provided ref
mock_image_meta_obj_from_ref.assert_has_calls([
mock.call(self.context, self.compute_api.image_api,
mock.sentinel.rescue_image_ref),
mock.call(self.context, self.compute_api.image_api,
mock.sentinel.rescue_image_ref)])
@mock.patch('nova.objects.image_meta.ImageMeta.from_image_ref')
def test_rescue_using_volume_backed_snapshot(self,
mock_image_meta_obj_from_ref):
instance = self._create_instance_obj()
rescue_image_obj = image_meta_obj.ImageMeta.from_dict(
{'min_disk': 0, 'min_ram': 0,
'properties': {'bdm_v2': True, 'block_device_mapping': [{}]},
'size': 0, 'status': 'active'})
mock_image_meta_obj_from_ref.return_value = rescue_image_obj
# Assert that UnsupportedRescueImage is raised
self.assertRaises(exception.UnsupportedRescueImage,
self.compute_api.rescue, self.context, instance,
rescue_image_ref=mock.sentinel.rescue_image_ref)
# Assert that we called from_image_ref using the provided ref
mock_image_meta_obj_from_ref.assert_called_once_with(
self.context, self.compute_api.image_api,
mock.sentinel.rescue_image_ref)
def test_set_admin_password_invalid_state(self):
# Tests that InstanceInvalidState is raised when not ACTIVE.
instance = self._create_instance_obj({'vm_state': vm_states.STOPPED})