Merge "compute: Do not allow rescue attempts using volume snapshot images"
This commit is contained in:
commit
0cdce3e8b5
@ -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())
|
||||
|
||||
|
@ -68,6 +68,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
|
||||
@ -4239,6 +4240,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:
|
||||
|
@ -1406,6 +1406,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")
|
||||
|
||||
|
@ -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"}}
|
||||
|
@ -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
|
||||
@ -5131,6 +5132,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,
|
||||
@ -5140,10 +5142,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,
|
||||
@ -5164,6 +5168,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()
|
||||
@ -5204,6 +5211,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',
|
||||
@ -5211,8 +5219,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(
|
||||
@ -5220,6 +5227,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'),
|
||||
@ -5233,8 +5242,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
|
||||
|
||||
@ -5360,6 +5370,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})
|
||||
|
Loading…
Reference in New Issue
Block a user