Skip to content

Commit c19d602

Browse files
committed
Block swap volume on volumes with >1 rw attachment
If we're swapping from a multiattach volume that has more than one read/write attachment, another server on the secondary attachment could be writing to the volume which is not getting copied into the volume to which we're swapping, so we could have data loss during the swap. This change does volume read/write attachment counting for the volume we're swapping from and if there is more than one read/write attachment on the volume, the swap volume operation fails with a 400 BadRequest error. Conflicts: nova/tests/unit/compute/test_compute_api.py NOTE(mriedem): The conflict is due to Stein change I7d5bddc0aa1833cda5f4bcebe5e03bdd447f641a changing the decorators on the _test_snapshot_and_backup method. Depends-On: https://review.openstack.org/573025/ Closes-Bug: #1775418 Change-Id: Icd7fcb87a09c35a13e4e14235feb30a289d22778 (cherry picked from commit 5a1d159) (cherry picked from commit 9b21d10)
1 parent 69dd48d commit c19d602

File tree

7 files changed

+207
-1
lines changed

7 files changed

+207
-1
lines changed

api-ref/source/os-volume-attachments.inc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ Policy defaults enable only users with the administrative role or
149149
the owner of the server to perform this operation. Cloud providers
150150
can change these permissions through the ``policy.json`` file.
151151

152+
Updating, or what is commonly referred to as "swapping", volume attachments
153+
with volumes that have more than one read/write attachment, is not supported.
154+
152155
Normal response codes: 202
153156

154157
Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404), conflict(409)

nova/api/openstack/compute/volumes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,8 @@ def update(self, req, server_id, id, body):
394394
new_volume)
395395
except exception.VolumeBDMNotFound as e:
396396
raise exc.HTTPNotFound(explanation=e.format_message())
397-
except exception.InvalidVolume as e:
397+
except (exception.InvalidVolume,
398+
exception.MultiattachSwapVolumeNotSupported) as e:
398399
raise exc.HTTPBadRequest(explanation=e.format_message())
399400
except exception.InstanceIsLocked as e:
400401
raise exc.HTTPConflict(explanation=e.format_message())

nova/compute/api.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4268,6 +4268,52 @@ def detach_volume(self, context, instance, volume):
42684268
else:
42694269
self._detach_volume(context, instance, volume)
42704270

4271+
def _count_attachments_for_swap(self, ctxt, volume):
4272+
"""Counts the number of attachments for a swap-related volume.
4273+
4274+
Attempts to only count read/write attachments if the volume attachment
4275+
records exist, otherwise simply just counts the number of attachments
4276+
regardless of attach mode.
4277+
4278+
:param ctxt: nova.context.RequestContext - user request context
4279+
:param volume: nova-translated volume dict from nova.volume.cinder.
4280+
:returns: count of attachments for the volume
4281+
"""
4282+
# This is a dict, keyed by server ID, to a dict of attachment_id and
4283+
# mountpoint.
4284+
attachments = volume.get('attachments', {})
4285+
# Multiattach volumes can have more than one attachment, so if there
4286+
# is more than one attachment, attempt to count the read/write
4287+
# attachments.
4288+
if len(attachments) > 1:
4289+
count = 0
4290+
for attachment in attachments.values():
4291+
attachment_id = attachment['attachment_id']
4292+
# Get the attachment record for this attachment so we can
4293+
# get the attach_mode.
4294+
# TODO(mriedem): This could be optimized if we had
4295+
# GET /attachments/detail?volume_id=volume['id'] in Cinder.
4296+
try:
4297+
attachment_record = self.volume_api.attachment_get(
4298+
ctxt, attachment_id)
4299+
# Note that the attachment record from Cinder has
4300+
# attach_mode in the top-level of the resource but the
4301+
# nova.volume.cinder code translates it and puts the
4302+
# attach_mode in the connection_info for some legacy
4303+
# reason...
4304+
if attachment_record.get(
4305+
'connection_info', {}).get(
4306+
# attachments are read/write by default
4307+
'attach_mode', 'rw') == 'rw':
4308+
count += 1
4309+
except exception.VolumeAttachmentNotFound:
4310+
# attachments are read/write by default so count it
4311+
count += 1
4312+
else:
4313+
count = len(attachments)
4314+
4315+
return count
4316+
42714317
@check_instance_lock
42724318
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.PAUSED,
42734319
vm_states.RESIZED])
@@ -4291,6 +4337,20 @@ def swap_volume(self, context, instance, old_volume, new_volume):
42914337
except exception.InvalidInput as exc:
42924338
raise exception.InvalidVolume(reason=exc.format_message())
42934339

4340+
# Disallow swapping from multiattach volumes that have more than one
4341+
# read/write attachment. We know the old_volume has at least one
4342+
# attachment since it's attached to this server. The new_volume
4343+
# can't have any attachments because of the attach_status check above.
4344+
# We do this count after calling "begin_detaching" to lock against
4345+
# concurrent attachments being made while we're counting.
4346+
try:
4347+
if self._count_attachments_for_swap(context, old_volume) > 1:
4348+
raise exception.MultiattachSwapVolumeNotSupported()
4349+
except Exception: # This is generic to handle failures while counting
4350+
# We need to reset the detaching status before raising.
4351+
with excutils.save_and_reraise_exception():
4352+
self.volume_api.roll_detaching(context, old_volume['id'])
4353+
42944354
# Get the BDM for the attached (old) volume so we can tell if it was
42954355
# attached with the new-style Cinder 3.44 API.
42964356
bdm = objects.BlockDeviceMapping.get_by_volume_and_instance(

nova/exception.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,11 @@ class MultiattachToShelvedNotSupported(Invalid):
288288
"shelved-offloaded instances.")
289289

290290

291+
class MultiattachSwapVolumeNotSupported(Invalid):
292+
msg_fmt = _('Swapping multi-attach volumes with more than one read/write '
293+
'attachment is not supported.')
294+
295+
291296
class VolumeNotCreated(NovaException):
292297
msg_fmt = _("Volume %(volume_id)s did not finish being created"
293298
" even after we waited %(seconds)s seconds or %(attempts)s"

nova/tests/unit/api/openstack/compute/test_volumes.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,81 @@ def test_attach_with_multiattach_fails_for_shelved_offloaded_server(self):
971971
'shelved-offloaded instances.', six.text_type(ex))
972972

973973

974+
class SwapVolumeMultiattachTestCase(test.NoDBTestCase):
975+
976+
@mock.patch('nova.api.openstack.common.get_instance')
977+
@mock.patch('nova.volume.cinder.API.begin_detaching')
978+
@mock.patch('nova.volume.cinder.API.roll_detaching')
979+
def test_swap_multiattach_multiple_readonly_attachments_fails(
980+
self, mock_roll_detaching, mock_begin_detaching,
981+
mock_get_instance):
982+
"""Tests that trying to swap from a multiattach volume with
983+
multiple read/write attachments will return an error.
984+
"""
985+
986+
def fake_volume_get(_context, volume_id):
987+
if volume_id == uuids.old_vol_id:
988+
return {
989+
'id': volume_id,
990+
'size': 1,
991+
'multiattach': True,
992+
'attachments': {
993+
uuids.server1: {
994+
'attachment_id': uuids.attachment_id1,
995+
'mountpoint': '/dev/vdb'
996+
},
997+
uuids.server2: {
998+
'attachment_id': uuids.attachment_id2,
999+
'mountpoint': '/dev/vdb'
1000+
}
1001+
}
1002+
}
1003+
if volume_id == uuids.new_vol_id:
1004+
return {
1005+
'id': volume_id,
1006+
'size': 1,
1007+
'attach_status': 'detached'
1008+
}
1009+
raise exception.VolumeNotFound(volume_id=volume_id)
1010+
1011+
def fake_attachment_get(_context, attachment_id):
1012+
return {'connection_info': {'attach_mode': 'rw'}}
1013+
1014+
ctxt = context.get_admin_context()
1015+
instance = fake_instance.fake_instance_obj(
1016+
ctxt, uuid=uuids.server1, vm_state=vm_states.ACTIVE,
1017+
task_state=None, launched_at=datetime.datetime(2018, 6, 6))
1018+
mock_get_instance.return_value = instance
1019+
controller = volumes_v21.VolumeAttachmentController()
1020+
with test.nested(
1021+
mock.patch.object(controller.volume_api, 'get',
1022+
side_effect=fake_volume_get),
1023+
mock.patch.object(controller.compute_api.volume_api,
1024+
'attachment_get',
1025+
side_effect=fake_attachment_get)) as (
1026+
mock_volume_get, mock_attachment_get
1027+
):
1028+
req = fakes.HTTPRequest.blank(
1029+
'/servers/%s/os-volume_attachments/%s' %
1030+
(uuids.server1, uuids.old_vol_id))
1031+
req.headers['content-type'] = 'application/json'
1032+
req.environ['nova.context'] = ctxt
1033+
body = {
1034+
'volumeAttachment': {
1035+
'volumeId': uuids.new_vol_id
1036+
}
1037+
}
1038+
ex = self.assertRaises(
1039+
webob.exc.HTTPBadRequest, controller.update, req,
1040+
uuids.server1, uuids.old_vol_id, body=body)
1041+
self.assertIn('Swapping multi-attach volumes with more than one ',
1042+
six.text_type(ex))
1043+
mock_attachment_get.assert_has_calls([
1044+
mock.call(ctxt, uuids.attachment_id1),
1045+
mock.call(ctxt, uuids.attachment_id2)], any_order=True)
1046+
mock_roll_detaching.assert_called_once_with(ctxt, uuids.old_vol_id)
1047+
1048+
9741049
class CommonBadRequestTestCase(object):
9751050

9761051
resource = None

nova/tests/unit/compute/test_compute_api.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2839,6 +2839,58 @@ def _do_test(mock_begin_detaching, mock_get_by_volume_and_instance,
28392839

28402840
_do_test()
28412841

2842+
def test_count_attachments_for_swap_not_found_and_readonly(self):
2843+
"""Tests that attachment records that aren't found are considered
2844+
read/write by default. Also tests that read-only attachments are
2845+
not counted.
2846+
"""
2847+
ctxt = context.get_admin_context()
2848+
volume = {
2849+
'attachments': {
2850+
uuids.server1: {
2851+
'attachment_id': uuids.attachment1
2852+
},
2853+
uuids.server2: {
2854+
'attachment_id': uuids.attachment2
2855+
}
2856+
}
2857+
}
2858+
2859+
def fake_attachment_get(_context, attachment_id):
2860+
if attachment_id == uuids.attachment1:
2861+
raise exception.VolumeAttachmentNotFound(
2862+
attachment_id=attachment_id)
2863+
return {'connection_info': {'attach_mode': 'ro'}}
2864+
2865+
with mock.patch.object(self.compute_api.volume_api, 'attachment_get',
2866+
side_effect=fake_attachment_get) as mock_get:
2867+
self.assertEqual(
2868+
1, self.compute_api._count_attachments_for_swap(ctxt, volume))
2869+
mock_get.assert_has_calls([
2870+
mock.call(ctxt, uuids.attachment1),
2871+
mock.call(ctxt, uuids.attachment2)], any_order=True)
2872+
2873+
@mock.patch('nova.volume.cinder.API.attachment_get',
2874+
new_callable=mock.NonCallableMock) # asserts not called
2875+
def test_count_attachments_for_swap_no_query(self, mock_attachment_get):
2876+
"""Tests that if the volume has <2 attachments, we don't query
2877+
the attachments for their attach_mode value.
2878+
"""
2879+
volume = {}
2880+
self.assertEqual(
2881+
0, self.compute_api._count_attachments_for_swap(
2882+
mock.sentinel.context, volume))
2883+
volume = {
2884+
'attachments': {
2885+
uuids.server: {
2886+
'attachment_id': uuids.attach1
2887+
}
2888+
}
2889+
}
2890+
self.assertEqual(
2891+
1, self.compute_api._count_attachments_for_swap(
2892+
mock.sentinel.context, volume))
2893+
28422894
@mock.patch.object(compute_api.API, '_record_action_start')
28432895
def _test_snapshot_and_backup(self, mock_record, is_snapshot=True,
28442896
with_base_ref=False, min_ram=None,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
fixes:
3+
- |
4+
The `os-volume_attachments`_ update API, commonly referred to as the swap
5+
volume API will now return a ``400`` (BadRequest) error when attempting to
6+
swap from a multi attached volume with more than one active read/write
7+
attachment resolving `bug #1775418`_.
8+
9+
.. _os-volume_attachments: https://developer.openstack.org/api-ref/compute/?expanded=update-a-volume-attachment-detail
10+
.. _bug #1775418: https://launchpad.net/bugs/1775418

0 commit comments

Comments
 (0)