Skip to content

Commit d00a848

Browse files
committed
Fix rescue volume-based instance
As of now, when attempting to rescue a volume-based instance using an image without the hw_rescue_device and/or hw_rescue_bus properties set, the rescue api call fails (as non-stable rescue for volume-based instances are not supported) leaving the instance in error state. This change checks for hw_rescue_device/hw_rescue_bus image properties before attempting to rescue and if the property is not set, then fail with proper error message, without changing instance state. Related-Bug: #1978958 Closes-Bug: #1926601 Change-Id: Id4c8c5f3b32985ac7d3d7c833b82e0876f7367c1 (cherry picked from commit 6eed55b)
1 parent 9bca7f3 commit d00a848

File tree

4 files changed

+220
-11
lines changed

4 files changed

+220
-11
lines changed

nova/compute/api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4697,6 +4697,7 @@ def rescue(self, context, instance, rescue_password=None,
46974697
allow_bfv_rescue=False):
46984698
"""Rescue the given instance."""
46994699

4700+
image_meta = None
47004701
if rescue_image_ref:
47014702
try:
47024703
image_meta = image_meta_obj.ImageMeta.from_image_ref(
@@ -4717,6 +4718,8 @@ def rescue(self, context, instance, rescue_password=None,
47174718
"image properties set")
47184719
raise exception.UnsupportedRescueImage(
47194720
image=rescue_image_ref)
4721+
else:
4722+
image_meta = instance.image_meta
47204723

47214724
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
47224725
context, instance.uuid)
@@ -4725,6 +4728,9 @@ def rescue(self, context, instance, rescue_password=None,
47254728
volume_backed = compute_utils.is_volume_backed_instance(
47264729
context, instance, bdms)
47274730

4731+
allow_bfv_rescue &= 'hw_rescue_bus' in image_meta.properties and \
4732+
'hw_rescue_device' in image_meta.properties
4733+
47284734
if volume_backed and allow_bfv_rescue:
47294735
cn = objects.ComputeNode.get_by_host_and_nodename(
47304736
context, instance.host, instance.node)

nova/tests/functional/test_server_rescue.py

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
# License for the specific language governing permissions and limitations
1111
# under the License.
1212

13+
import datetime
14+
15+
from oslo_utils.fixture import uuidsentinel as uuids
16+
1317
from nova.tests import fixtures as nova_fixtures
1418
from nova.tests.functional.api import client
1519
from nova.tests.functional import integrated_helpers
@@ -23,7 +27,37 @@ def setUp(self):
2327
self.useFixture(nova_fixtures.CinderFixture(self))
2428
self._start_compute(host='host1')
2529

26-
def _create_bfv_server(self):
30+
def _create_image(self, metadata=None):
31+
image = {
32+
'id': uuids.stable_rescue_image,
33+
'name': 'fake-image-rescue-property',
34+
'created_at': datetime.datetime(2011, 1, 1, 1, 2, 3),
35+
'updated_at': datetime.datetime(2011, 1, 1, 1, 2, 3),
36+
'deleted_at': None,
37+
'deleted': False,
38+
'status': 'active',
39+
'is_public': False,
40+
'container_format': 'raw',
41+
'disk_format': 'raw',
42+
'size': '25165824',
43+
'min_ram': 0,
44+
'min_disk': 0,
45+
'protected': False,
46+
'visibility': 'public',
47+
'tags': ['tag1', 'tag2'],
48+
'properties': {
49+
'kernel_id': 'nokernel',
50+
'ramdisk_id': 'nokernel',
51+
'hw_rescue_device': 'disk',
52+
'hw_rescue_bus': 'scsi',
53+
},
54+
}
55+
if metadata:
56+
image['properties'].update(metadata)
57+
return self.glance.create(None, image)
58+
59+
def _create_bfv_server(self, metadata=None):
60+
image = self._create_image(metadata=metadata)
2761
server_request = self._build_server(networks=[])
2862
server_request.pop('imageRef')
2963
server_request['block_device_mapping_v2'] = [{
@@ -33,7 +67,7 @@ def _create_bfv_server(self):
3367
'destination_type': 'volume'}]
3468
server = self.api.post_server({'server': server_request})
3569
self._wait_for_state_change(server, 'ACTIVE')
36-
return server
70+
return server, image
3771

3872

3973
class DisallowBFVRescuev286(BFVRescue):
@@ -43,10 +77,10 @@ class DisallowBFVRescuev286(BFVRescue):
4377
microversion = '2.86'
4478

4579
def test_bfv_rescue_not_supported(self):
46-
server = self._create_bfv_server()
80+
server, image = self._create_bfv_server()
4781
ex = self.assertRaises(client.OpenStackApiException,
4882
self.api.post_server_action, server['id'], {'rescue': {
49-
'rescue_image_ref': '155d900f-4e14-4e4c-a73d-069cbf4541e6'}})
83+
'rescue_image_ref': image['id']}})
5084
self.assertEqual(400, ex.response.status_code)
5185
self.assertIn('Cannot rescue a volume-backed instance',
5286
ex.response.text)
@@ -60,10 +94,10 @@ class DisallowBFVRescuev286WithTrait(BFVRescue):
6094
microversion = '2.86'
6195

6296
def test_bfv_rescue_not_supported(self):
63-
server = self._create_bfv_server()
97+
server, image = self._create_bfv_server()
6498
ex = self.assertRaises(client.OpenStackApiException,
6599
self.api.post_server_action, server['id'], {'rescue': {
66-
'rescue_image_ref': '155d900f-4e14-4e4c-a73d-069cbf4541e6'}})
100+
'rescue_image_ref': image['id']}})
67101
self.assertEqual(400, ex.response.status_code)
68102
self.assertIn('Cannot rescue a volume-backed instance',
69103
ex.response.text)
@@ -77,10 +111,10 @@ class DisallowBFVRescuev287WithoutTrait(BFVRescue):
77111
microversion = '2.87'
78112

79113
def test_bfv_rescue_not_supported(self):
80-
server = self._create_bfv_server()
114+
server, image = self._create_bfv_server()
81115
ex = self.assertRaises(client.OpenStackApiException,
82116
self.api.post_server_action, server['id'], {'rescue': {
83-
'rescue_image_ref': '155d900f-4e14-4e4c-a73d-069cbf4541e6'}})
117+
'rescue_image_ref': image['id']}})
84118
self.assertEqual(400, ex.response.status_code)
85119
self.assertIn('Host unable to rescue a volume-backed instance',
86120
ex.response.text)
@@ -94,7 +128,41 @@ class AllowBFVRescuev287WithTrait(BFVRescue):
94128
microversion = '2.87'
95129

96130
def test_bfv_rescue_supported(self):
97-
server = self._create_bfv_server()
131+
server, image = self._create_bfv_server()
98132
self.api.post_server_action(server['id'], {'rescue': {
133+
'rescue_image_ref': image['id']}})
134+
self._wait_for_state_change(server, 'RESCUE')
135+
136+
137+
class DisallowBFVRescuev287WithoutRescueImageProperties(BFVRescue):
138+
"""Asserts that BFV rescue requests fail with microversion 2.87 (or later)
139+
when the required hw_rescue_device and hw_rescue_bus image properties
140+
are not set on the image.
141+
"""
142+
compute_driver = 'fake.MediumFakeDriver'
143+
microversion = '2.87'
144+
145+
def test_bfv_rescue_failed(self):
146+
server, image = self._create_bfv_server()
147+
# try rescue without hw_rescue_device and hw_rescue_bus properties set
148+
ex = self.assertRaises(client.OpenStackApiException,
149+
self.api.post_server_action, server['id'], {'rescue': {
99150
'rescue_image_ref': '155d900f-4e14-4e4c-a73d-069cbf4541e6'}})
151+
self.assertEqual(400, ex.response.status_code)
152+
self.assertIn('Cannot rescue a volume-backed instance',
153+
ex.response.text)
154+
155+
156+
class AllowBFVRescuev287WithRescueImageProperties(BFVRescue):
157+
"""Asserts that BFV rescue requests pass with microversion 2.87 (or later)
158+
when the required hw_rescue_device and hw_rescue_bus image properties
159+
are set on the image.
160+
"""
161+
compute_driver = 'fake.RescueBFVDriver'
162+
microversion = '2.87'
163+
164+
def test_bfv_rescue_done(self):
165+
server, image = self._create_bfv_server()
166+
self.api.post_server_action(server['id'], {'rescue': {
167+
'rescue_image_ref': image['id']}})
100168
self._wait_for_state_change(server, 'RESCUE')

nova/tests/unit/compute/test_api.py

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5790,7 +5790,10 @@ def test_rescue_bfv_with_required_trait(self, mock_get_bdms,
57905790
destination_type='volume', volume_type=None,
57915791
snapshot_id=None, volume_id=uuids.volume_id,
57925792
volume_size=None)])
5793-
rescue_image_meta_obj = image_meta_obj.ImageMeta.from_dict({})
5793+
rescue_image_meta_obj = image_meta_obj.ImageMeta.from_dict({
5794+
'properties': {'hw_rescue_device': 'disk',
5795+
'hw_rescue_bus': 'scsi'}
5796+
})
57945797

57955798
with test.nested(
57965799
mock.patch.object(self.compute_api.placementclient,
@@ -5842,6 +5845,7 @@ def test_rescue_bfv_with_required_trait(self, mock_get_bdms,
58425845
# Assert that the instance task state as set in the compute API
58435846
self.assertEqual(task_states.RESCUING, instance.task_state)
58445847

5848+
@mock.patch('nova.objects.instance.Instance.image_meta')
58455849
@mock.patch('nova.objects.compute_node.ComputeNode'
58465850
'.get_by_host_and_nodename')
58475851
@mock.patch('nova.compute.utils.is_volume_backed_instance',
@@ -5850,14 +5854,21 @@ def test_rescue_bfv_with_required_trait(self, mock_get_bdms,
58505854
'.get_by_instance_uuid')
58515855
def test_rescue_bfv_without_required_trait(self, mock_get_bdms,
58525856
mock_is_volume_backed,
5853-
mock_get_cn):
5857+
mock_get_cn,
5858+
mock_image_meta):
58545859
instance = self._create_instance_obj()
58555860
bdms = objects.BlockDeviceMappingList(objects=[
58565861
objects.BlockDeviceMapping(
58575862
boot_index=0, image_id=uuids.image_id, source_type='image',
58585863
destination_type='volume', volume_type=None,
58595864
snapshot_id=None, volume_id=uuids.volume_id,
58605865
volume_size=None)])
5866+
5867+
instance.image_meta = image_meta_obj.ImageMeta.from_dict({
5868+
'properties': {'hw_rescue_device': 'disk',
5869+
'hw_rescue_bus': 'scsi'}
5870+
})
5871+
58615872
with test.nested(
58625873
mock.patch.object(self.compute_api.placementclient,
58635874
'get_provider_traits'),
@@ -5895,6 +5906,124 @@ def test_rescue_bfv_without_required_trait(self, mock_get_bdms,
58955906
mock_get_traits.assert_called_once_with(
58965907
self.context, uuids.cn)
58975908

5909+
@mock.patch('nova.objects.image_meta.ImageMeta.from_image_ref')
5910+
@mock.patch('nova.objects.compute_node.ComputeNode'
5911+
'.get_by_host_and_nodename')
5912+
@mock.patch('nova.compute.utils.is_volume_backed_instance',
5913+
return_value=True)
5914+
@mock.patch('nova.objects.block_device.BlockDeviceMappingList'
5915+
'.get_by_instance_uuid')
5916+
def test_rescue_bfv_with_required_image_properties(
5917+
self, mock_get_bdms, mock_is_volume_backed, mock_get_cn,
5918+
mock_image_meta_obj_from_ref):
5919+
instance = self._create_instance_obj()
5920+
bdms = objects.BlockDeviceMappingList(objects=[
5921+
objects.BlockDeviceMapping(
5922+
boot_index=0, image_id=uuids.image_id, source_type='image',
5923+
destination_type='volume', volume_type=None,
5924+
snapshot_id=None, volume_id=uuids.volume_id,
5925+
volume_size=None)])
5926+
rescue_image_meta_obj = image_meta_obj.ImageMeta.from_dict({
5927+
'properties': {'hw_rescue_device': 'disk',
5928+
'hw_rescue_bus': 'scsi'}
5929+
})
5930+
5931+
with test.nested(
5932+
mock.patch.object(self.compute_api.placementclient,
5933+
'get_provider_traits'),
5934+
mock.patch.object(self.compute_api.volume_api, 'get'),
5935+
mock.patch.object(self.compute_api.volume_api, 'check_attached'),
5936+
mock.patch.object(instance, 'save'),
5937+
mock.patch.object(self.compute_api, '_record_action_start'),
5938+
mock.patch.object(self.compute_api.compute_rpcapi,
5939+
'rescue_instance')
5940+
) as (
5941+
mock_get_traits, mock_get_volume, mock_check_attached,
5942+
mock_instance_save, mock_record_start, mock_rpcapi_rescue
5943+
):
5944+
# Mock out the returned compute node, image_meta, bdms and volume
5945+
mock_image_meta_obj_from_ref.return_value = rescue_image_meta_obj
5946+
mock_get_bdms.return_value = bdms
5947+
mock_get_volume.return_value = mock.sentinel.volume
5948+
mock_get_cn.return_value = mock.Mock(uuid=uuids.cn)
5949+
5950+
# Ensure the required trait is returned, allowing BFV rescue
5951+
mock_trait_info = mock.Mock(traits=[ot.COMPUTE_RESCUE_BFV])
5952+
mock_get_traits.return_value = mock_trait_info
5953+
5954+
# Try to rescue the instance
5955+
self.compute_api.rescue(self.context, instance,
5956+
rescue_image_ref=uuids.rescue_image_id,
5957+
allow_bfv_rescue=True)
5958+
5959+
# Assert all of the calls made in the compute API
5960+
mock_get_bdms.assert_called_once_with(self.context, instance.uuid)
5961+
mock_get_volume.assert_called_once_with(
5962+
self.context, uuids.volume_id)
5963+
mock_check_attached.assert_called_once_with(
5964+
self.context, mock.sentinel.volume)
5965+
mock_is_volume_backed.assert_called_once_with(
5966+
self.context, instance, bdms)
5967+
mock_get_cn.assert_called_once_with(
5968+
self.context, instance.host, instance.node)
5969+
mock_get_traits.assert_called_once_with(self.context, uuids.cn)
5970+
mock_instance_save.assert_called_once_with(
5971+
expected_task_state=[None])
5972+
mock_record_start.assert_called_once_with(
5973+
self.context, instance, instance_actions.RESCUE)
5974+
mock_rpcapi_rescue.assert_called_once_with(
5975+
self.context, instance=instance, rescue_password=None,
5976+
rescue_image_ref=uuids.rescue_image_id, clean_shutdown=True)
5977+
5978+
# Assert that the instance task state as set in the compute API
5979+
self.assertEqual(task_states.RESCUING, instance.task_state)
5980+
5981+
@mock.patch('nova.objects.image_meta.ImageMeta.from_image_ref')
5982+
@mock.patch('nova.compute.utils.is_volume_backed_instance',
5983+
return_value=True)
5984+
@mock.patch('nova.objects.block_device.BlockDeviceMappingList'
5985+
'.get_by_instance_uuid')
5986+
def test_rescue_bfv_without_required_image_properties(
5987+
self, mock_get_bdms, mock_is_volume_backed,
5988+
mock_image_meta_obj_from_ref):
5989+
instance = self._create_instance_obj()
5990+
bdms = objects.BlockDeviceMappingList(objects=[
5991+
objects.BlockDeviceMapping(
5992+
boot_index=0, image_id=uuids.image_id, source_type='image',
5993+
destination_type='volume', volume_type=None,
5994+
snapshot_id=None, volume_id=uuids.volume_id,
5995+
volume_size=None)])
5996+
rescue_image_meta_obj = image_meta_obj.ImageMeta.from_dict({
5997+
'properties': {}
5998+
})
5999+
6000+
with test.nested(
6001+
mock.patch.object(self.compute_api.volume_api, 'get'),
6002+
mock.patch.object(self.compute_api.volume_api, 'check_attached'),
6003+
) as (
6004+
mock_get_volume, mock_check_attached
6005+
):
6006+
# Mock out the returned bdms, volume and image_meta
6007+
mock_get_bdms.return_value = bdms
6008+
mock_get_volume.return_value = mock.sentinel.volume
6009+
mock_image_meta_obj_from_ref.return_value = rescue_image_meta_obj
6010+
6011+
# Assert that any attempt to rescue a bfv instance on a compute
6012+
# node that does not report the COMPUTE_RESCUE_BFV trait fails and
6013+
# raises InstanceNotRescuable
6014+
self.assertRaises(exception.InstanceNotRescuable,
6015+
self.compute_api.rescue, self.context, instance,
6016+
rescue_image_ref=None, allow_bfv_rescue=True)
6017+
6018+
# Assert the calls made in the compute API prior to the failure
6019+
mock_get_bdms.assert_called_once_with(self.context, instance.uuid)
6020+
mock_get_volume.assert_called_once_with(
6021+
self.context, uuids.volume_id)
6022+
mock_check_attached.assert_called_once_with(
6023+
self.context, mock.sentinel.volume)
6024+
mock_is_volume_backed.assert_called_once_with(
6025+
self.context, instance, bdms)
6026+
58986027
@mock.patch('nova.compute.utils.is_volume_backed_instance',
58996028
return_value=True)
59006029
@mock.patch('nova.objects.block_device.BlockDeviceMappingList'
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
Fix rescuing volume based instance by adding a check for 'hw_rescue_disk'
5+
and 'hw_rescue_device' properties in image metadata before attempting
6+
to rescue instance.

0 commit comments

Comments
 (0)