summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/notification_samples/service-update.json2
-rw-r--r--nova/compute/api.py46
-rw-r--r--nova/compute/manager.py9
-rw-r--r--nova/objects/service.py7
-rw-r--r--nova/tests/unit/compute/test_compute.py89
-rw-r--r--nova/tests/unit/compute/test_compute_api.py62
-rw-r--r--nova/tests/unit/compute/test_compute_mgr.py2
-rw-r--r--nova/tests/unit/compute/test_shelve.py4
-rw-r--r--nova/tests/unit/fake_volume.py8
-rw-r--r--nova/tests/unit/virt/test_block_device.py56
-rw-r--r--nova/tests/unit/volume/test_cinder.py26
-rw-r--r--nova/virt/block_device.py21
-rw-r--r--nova/volume/cinder.py13
-rw-r--r--releasenotes/notes/remove-check-attach-68b9ec781ad184ff.yaml26
14 files changed, 232 insertions, 139 deletions
diff --git a/doc/notification_samples/service-update.json b/doc/notification_samples/service-update.json
index ebff74d687..1eacf13244 100644
--- a/doc/notification_samples/service-update.json
+++ b/doc/notification_samples/service-update.json
@@ -13,7 +13,7 @@
"disabled_reason": null,
"report_count": 1,
"forced_down": false,
- "version": 16
+ "version": 17
}
},
"event_type": "service.update",
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 898e0acee5..b9d52748dd 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -105,6 +105,7 @@ AGGREGATE_ACTION_UPDATE = 'Update'
AGGREGATE_ACTION_UPDATE_META = 'UpdateMeta'
AGGREGATE_ACTION_DELETE = 'Delete'
AGGREGATE_ACTION_ADD = 'Add'
+BFV_RESERVE_MIN_COMPUTE_VERSION = 17
# FIXME(danms): Keep a global cache of the cells we find the
# first time we look. This needs to be refreshed on a timer or
@@ -1354,15 +1355,30 @@ class API(base.Base):
"destination_type 'volume' need to have a non-zero "
"size specified"))
elif volume_id is not None:
+ min_compute_version = objects.Service.get_minimum_version(
+ context, 'nova-compute')
try:
- volume = self.volume_api.get(context, volume_id)
- self.volume_api.check_attach(context,
- volume,
- instance=instance)
+ # NOTE(ildikov): The boot from volume operation did not
+ # reserve the volume before Pike and as the older computes
+ # are running 'check_attach' which will fail if the volume
+ # is in 'attaching' state; if the compute service version
+ # is not high enough we will just perform the old check as
+ # opposed to reserving the volume here.
+ if (min_compute_version >=
+ BFV_RESERVE_MIN_COMPUTE_VERSION):
+ volume = self._check_attach_and_reserve_volume(
+ context, volume_id, instance)
+ else:
+ # NOTE(ildikov): This call is here only for backward
+ # compatibility can be removed after Ocata EOL.
+ volume = self._check_attach(context, volume_id,
+ instance)
bdm.volume_size = volume.get('size')
except (exception.CinderConnectionFailed,
exception.InvalidVolume):
raise
+ except exception.InvalidInput as exc:
+ raise exception.InvalidVolume(reason=exc.format_message())
except Exception:
raise exception.InvalidBDMVolume(id=volume_id)
elif snapshot_id is not None:
@@ -1404,6 +1420,23 @@ class API(base.Base):
if num_local > max_local:
raise exception.InvalidBDMLocalsLimit()
+ def _check_attach(self, context, volume_id, instance):
+ # TODO(ildikov): This check_attach code is kept only for backward
+ # compatibility and should be removed after Ocata EOL.
+ volume = self.volume_api.get(context, volume_id)
+ if volume['status'] != 'available':
+ msg = _("volume '%(vol)s' status must be 'available'. Currently "
+ "in '%(status)s'") % {'vol': volume['id'],
+ 'status': volume['status']}
+ raise exception.InvalidVolume(reason=msg)
+ if volume['attach_status'] == 'attached':
+ msg = _("volume %s already attached") % volume['id']
+ raise exception.InvalidVolume(reason=msg)
+ self.volume_api.check_availability_zone(context, volume,
+ instance=instance)
+
+ return volume
+
def _populate_instance_names(self, instance, num_instances):
"""Populate instance display_name and hostname."""
display_name = instance.get('display_name')
@@ -3551,6 +3584,8 @@ class API(base.Base):
instance=instance)
self.volume_api.reserve_volume(context, volume_id)
+ return volume
+
def _attach_volume(self, context, instance, volume_id, device,
disk_bus, device_type):
"""Attach an existing volume to an existing instance.
@@ -3689,7 +3724,8 @@ class API(base.Base):
msg = _("New volume must be the same size or larger.")
raise exception.InvalidVolume(reason=msg)
self.volume_api.check_detach(context, old_volume)
- self.volume_api.check_attach(context, new_volume, instance=instance)
+ self.volume_api.check_availability_zone(context, new_volume,
+ instance=instance)
self.volume_api.begin_detaching(context, old_volume['id'])
self.volume_api.reserve_volume(context, new_volume['id'])
try:
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index c7ebaba548..1c706fc4fb 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -1572,8 +1572,7 @@ class ComputeManager(manager.Manager):
bdm.update(values)
bdm.save()
- def _prep_block_device(self, context, instance, bdms,
- do_check_attach=True):
+ def _prep_block_device(self, context, instance, bdms):
"""Set up the block device for an instance with error logging."""
try:
self._add_missing_dev_names(bdms, instance)
@@ -1581,7 +1580,6 @@ class ComputeManager(manager.Manager):
mapping = driver.block_device_info_get_mapping(block_device_info)
driver_block_device.attach_block_devices(
mapping, context, instance, self.volume_api, self.driver,
- do_check_attach=do_check_attach,
wait_func=self._await_block_device_map_created)
self._block_device_info_to_legacy(block_device_info)
@@ -4431,8 +4429,7 @@ class ComputeManager(manager.Manager):
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
context, instance.uuid)
- block_device_info = self._prep_block_device(context, instance, bdms,
- do_check_attach=False)
+ block_device_info = self._prep_block_device(context, instance, bdms)
scrubbed_keys = self._unshelve_instance_key_scrub(instance)
if node is None:
@@ -4786,7 +4783,7 @@ class ComputeManager(manager.Manager):
instance=instance)
try:
bdm.attach(context, instance, self.volume_api, self.driver,
- do_check_attach=False, do_driver_attach=True)
+ do_driver_attach=True)
except Exception:
with excutils.save_and_reraise_exception():
LOG.exception(_LE("Failed to attach %(volume_id)s "
diff --git a/nova/objects/service.py b/nova/objects/service.py
index 788dd45d76..f863d6fb71 100644
--- a/nova/objects/service.py
+++ b/nova/objects/service.py
@@ -30,7 +30,7 @@ LOG = logging.getLogger(__name__)
# NOTE(danms): This is the global service version counter
-SERVICE_VERSION = 16
+SERVICE_VERSION = 17
# NOTE(danms): This is our SERVICE_VERSION history. The idea is that any
@@ -95,6 +95,11 @@ SERVICE_VERSION_HISTORY = (
# Version 16: Indicate that nova-compute will refuse to start if it doesn't
# have a placement section configured.
{'compute_rpc': '4.13'},
+ # Version 17: Add 'reserve_volume' to the boot from volume flow and
+ # remove 'check_attach'. The service version bump is needed to fall back to
+ # the old check in the API as the old computes fail if the volume is moved
+ # to 'attaching' state by reserve.
+ {'compute_rpc': '4.13'},
)
diff --git a/nova/tests/unit/compute/test_compute.py b/nova/tests/unit/compute/test_compute.py
index ef9fc1f195..6688f427da 100644
--- a/nova/tests/unit/compute/test_compute.py
+++ b/nova/tests/unit/compute/test_compute.py
@@ -372,6 +372,8 @@ class ComputeVolumeTestCase(BaseTestCase):
lambda *a, **kw: None)
self.stub_out('nova.volume.cinder.API.detach',
lambda *a, **kw: None)
+ self.stub_out('nova.volume.cinder.API.check_availability_zone',
+ lambda *a, **kw: None)
self.stub_out('eventlet.greenthread.sleep',
lambda *a, **kw: None)
@@ -864,17 +866,24 @@ class ComputeVolumeTestCase(BaseTestCase):
for expected, got in zip(expected_result, preped_bdm):
self.assertThat(expected, matchers.IsSubDictOf(got))
- def test_validate_bdm(self):
+ @mock.patch.object(objects.Service, 'get_minimum_version',
+ return_value=17)
+ def test_validate_bdm(self, mock_get_min_ver):
def fake_get(self, context, res_id):
return {'id': res_id, 'size': 4}
- def fake_check_attach(*args, **kwargs):
+ def fake_check_availability_zone(*args, **kwargs):
+ pass
+
+ def fake_reserve_volume(*args, **kwargs):
pass
self.stub_out('nova.volume.cinder.API.get', fake_get)
self.stub_out('nova.volume.cinder.API.get_snapshot', fake_get)
- self.stub_out('nova.volume.cinder.API.check_attach',
- fake_check_attach)
+ self.stub_out('nova.volume.cinder.API.check_availability_zone',
+ fake_check_availability_zone)
+ self.stub_out('nova.volume.cinder.API.reserve_volume',
+ fake_reserve_volume)
volume_id = '55555555-aaaa-bbbb-cccc-555555555555'
snapshot_id = '66666666-aaaa-bbbb-cccc-555555555555'
@@ -1085,13 +1094,52 @@ class ComputeVolumeTestCase(BaseTestCase):
self.context, self.instance,
instance_type, all_mappings)
+ @mock.patch.object(objects.Service, 'get_minimum_version',
+ return_value=16)
+ @mock.patch.object(nova.volume.cinder.API, 'get')
+ @mock.patch.object(cinder.API, 'check_availability_zone')
+ def test_validate_bdm_volume_old_compute(self, mock_check_av_zone,
+ mock_get, mock_get_min_ver):
+ instance = self._create_fake_instance_obj()
+ instance_type = {'ephemeral_gb': 2}
+ volume_id = uuids.volume_id
+ mappings = [
+ fake_block_device.FakeDbBlockDeviceDict({
+ 'device_name': '/dev/sda1',
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'device_type': 'disk',
+ 'volume_id': volume_id,
+ 'guest_format': None,
+ 'boot_index': 0,
+ }, anon=True)
+ ]
+ mappings = block_device_obj.block_device_make_list_from_dicts(
+ self.context, mappings)
+
+ volume = {'id': volume_id,
+ 'size': 4,
+ 'status': 'available',
+ 'attach_status': 'detached',
+ 'multiattach': False}
+
+ mock_get.return_value = volume
+ self.compute_api._validate_bdm(self.context, instance,
+ instance_type, mappings)
+ self.assertEqual(4, mappings[0].volume_size)
+ mock_check_av_zone.assert_called_once_with(self.context, volume,
+ instance=instance)
+
+ @mock.patch.object(objects.Service, 'get_minimum_version',
+ return_value=17)
@mock.patch.object(cinder.API, 'get')
@mock.patch.object(cinder.API, 'check_availability_zone')
@mock.patch.object(cinder.API, 'reserve_volume',
side_effect=exception.InvalidVolume(reason='error'))
def test_validate_bdm_media_service_invalid_volume(self, mock_reserve_vol,
mock_check_av_zone,
- mock_get):
+ mock_get,
+ mock_get_min_ver):
volume_id = uuids.volume_id
instance_type = {'swap': 1, 'ephemeral_gb': 1}
bdms = [fake_block_device.FakeDbBlockDeviceDict({
@@ -1137,8 +1185,11 @@ class ComputeVolumeTestCase(BaseTestCase):
instance_type, bdms)
mock_get.assert_called_with(self.context, volume_id)
+ @mock.patch.object(objects.Service, 'get_minimum_version',
+ return_value=17)
@mock.patch.object(cinder.API, 'get')
- def test_validate_bdm_media_service_volume_not_found(self, mock_get):
+ def test_validate_bdm_media_service_volume_not_found(self, mock_get,
+ mock_get_min_ver):
volume_id = uuids.volume_id
instance_type = {'swap': 1, 'ephemeral_gb': 1}
bdms = [fake_block_device.FakeDbBlockDeviceDict({
@@ -1160,10 +1211,15 @@ class ComputeVolumeTestCase(BaseTestCase):
self.context, self.instance,
instance_type, bdms)
+ @mock.patch.object(objects.Service, 'get_minimum_version',
+ return_value=17)
@mock.patch.object(cinder.API, 'get')
@mock.patch.object(cinder.API, 'check_availability_zone')
- def test_validate_bdm_media_service_valid(self, mock_check_av_zone,
- mock_get):
+ @mock.patch.object(cinder.API, 'reserve_volume')
+ def test_validate_bdm_media_service_valid(self, mock_reserve_vol,
+ mock_check_av_zone,
+ mock_get,
+ mock_get_min_ver):
volume_id = uuids.volume_id
instance_type = {'swap': 1, 'ephemeral_gb': 1}
bdms = [fake_block_device.FakeDbBlockDeviceDict({
@@ -1188,7 +1244,8 @@ class ComputeVolumeTestCase(BaseTestCase):
instance_type, bdms)
mock_get.assert_called_once_with(self.context, volume_id)
mock_check_av_zone.assert_called_once_with(self.context, volume,
- self.instance)
+ instance=self.instance)
+ mock_reserve_vol.assert_called_once_with(self.context, volume_id)
def test_volume_snapshot_create(self):
self.assertRaises(messaging.ExpectedException,
@@ -1926,7 +1983,7 @@ class ComputeTestCase(BaseTestCase):
LOG.info("Running instances: %s", instances)
self.assertEqual(len(instances), 1)
- def fake_check_attach(*args, **kwargs):
+ def fake_check_availability_zone(*args, **kwargs):
pass
def fake_reserve_volume(*args, **kwargs):
@@ -1963,7 +2020,8 @@ class ComputeTestCase(BaseTestCase):
return bdm
self.stub_out('nova.volume.cinder.API.get', fake_volume_get)
- self.stub_out('nova.volume.cinder.API.check_attach', fake_check_attach)
+ self.stub_out('nova.volume.cinder.API.check_availability_zone',
+ fake_check_availability_zone)
self.stub_out('nova.volume.cinder.API.reserve_volume',
fake_reserve_volume)
self.stub_out('nova.volume.cinder.API.terminate_connection',
@@ -4653,10 +4711,11 @@ class ComputeTestCase(BaseTestCase):
return volume
self.stub_out('nova.volume.cinder.API.get', fake_volume_get)
- def fake_volume_check_attach(self, context, volume_id, instance):
+ def fake_volume_check_availability_zone(self, context,
+ volume_id, instance):
pass
- self.stub_out('nova.volume.cinder.API.check_attach',
- fake_volume_check_attach)
+ self.stub_out('nova.volume.cinder.API.check_availability_zone',
+ fake_volume_check_availability_zone)
def fake_get_volume_encryption_metadata(self, context, volume_id):
return {}
@@ -9304,7 +9363,7 @@ class ComputeAPITestCase(BaseTestCase):
return {'id': volume_id}
self.stub_out('nova.volume.cinder.API.get', fake_volume_get)
- self.stub_out('nova.volume.cinder.API.check_attach', fake)
+ self.stub_out('nova.volume.cinder.API.check_availability_zone', fake)
self.stub_out('nova.volume.cinder.API.reserve_volume', fake)
instance = fake_instance.fake_instance_obj(None, **{
diff --git a/nova/tests/unit/compute/test_compute_api.py b/nova/tests/unit/compute/test_compute_api.py
index 297b2d2cfa..6bd3bd220e 100644
--- a/nova/tests/unit/compute/test_compute_api.py
+++ b/nova/tests/unit/compute/test_compute_api.py
@@ -3308,12 +3308,15 @@ class _ComputeAPIUnitTestMixIn(object):
self.context,
bdms, legacy_bdm=True)
+ @mock.patch.object(objects.Service, 'get_minimum_version',
+ return_value=17)
@mock.patch.object(cinder.API, 'get')
- @mock.patch.object(cinder.API, 'check_attach',
- side_effect=exception.InvalidVolume(reason='error'))
- def test_validate_bdm_with_error_volume(self, mock_check_attach, mock_get):
- # Tests that an InvalidVolume exception raised from
- # volume_api.check_attach due to the volume status not being
+ @mock.patch.object(cinder.API, 'reserve_volume',
+ side_effect=exception.InvalidInput(reason='error'))
+ def test_validate_bdm_with_error_volume(self, mock_reserve_volume,
+ mock_get, mock_get_min_ver):
+ # Tests that an InvalidInput exception raised from
+ # volume_api.reserve_volume due to the volume status not being
# 'available' results in _validate_bdm re-raising InvalidVolume.
instance = self._create_instance_obj()
instance_type = self._create_flavor()
@@ -3338,14 +3341,17 @@ class _ComputeAPIUnitTestMixIn(object):
instance, instance_type, bdms)
mock_get.assert_called_once_with(self.context, volume_id)
- mock_check_attach.assert_called_once_with(
- self.context, volume_info, instance=instance)
+ mock_reserve_volume.assert_called_once_with(
+ self.context, volume_id)
+ @mock.patch.object(objects.Service, 'get_minimum_version',
+ return_value=17)
@mock.patch.object(cinder.API, 'get_snapshot',
side_effect=exception.CinderConnectionFailed(reason='error'))
@mock.patch.object(cinder.API, 'get',
side_effect=exception.CinderConnectionFailed(reason='error'))
- def test_validate_bdm_with_cinder_down(self, mock_get, mock_get_snapshot):
+ def test_validate_bdm_with_cinder_down(self, mock_get, mock_get_snapshot,
+ mock_get_min_ver):
instance = self._create_instance_obj()
instance_type = self._create_flavor()
bdm = [objects.BlockDeviceMapping(
@@ -3445,16 +3451,26 @@ class _ComputeAPIUnitTestMixIn(object):
do_test()
+ @mock.patch.object(objects.Service, 'get_minimum_version',
+ return_value=17)
@mock.patch.object(cinder.API, 'get',
side_effect=exception.CinderConnectionFailed(reason='error'))
- def test_provision_instances_with_cinder_down(self, mock_get):
+ def test_provision_instances_with_cinder_down(self, mock_get,
+ mock_get_min_ver):
self._test_provision_instances_with_cinder_error(
expected_exception=exception.CinderConnectionFailed)
- @mock.patch.object(cinder.API, 'get',
- return_value={'id': 1, 'status': 'error',
- 'attach_status': 'detached'})
- def test_provision_instances_with_error_volume(self, mock_get):
+ @mock.patch.object(objects.Service, 'get_minimum_version',
+ return_value=17)
+ @mock.patch.object(cinder.API, 'get')
+ @mock.patch.object(cinder.API, 'check_availability_zone')
+ @mock.patch.object(cinder.API, 'reserve_volume',
+ side_effect=exception.InvalidInput(reason='error'))
+ def test_provision_instances_with_error_volume(self,
+ mock_cinder_check_av_zone,
+ mock_reserve_volume,
+ mock_get,
+ mock_get_min_ver):
self._test_provision_instances_with_cinder_error(
expected_exception=exception.InvalidVolume)
@@ -3505,9 +3521,12 @@ class _ComputeAPIUnitTestMixIn(object):
@mock.patch.object(objects.RequestSpec, 'from_components')
@mock.patch.object(objects.BuildRequest, 'create')
@mock.patch.object(objects.InstanceMapping, 'create')
- def do_test(_mock_inst_mapping_create, mock_build_req,
- mock_req_spec_from_components, _mock_ensure_default,
- mock_check_num_inst_quota, mock_volume, mock_inst_create):
+ @mock.patch.object(objects.Service, 'get_minimum_version',
+ return_value=17)
+ def do_test(mock_get_min_ver, _mock_inst_mapping_create,
+ mock_build_req, mock_req_spec_from_components,
+ _mock_ensure_default, mock_check_num_inst_quota,
+ mock_volume, mock_inst_create):
min_count = 1
max_count = 2
@@ -3651,11 +3670,16 @@ class _ComputeAPIUnitTestMixIn(object):
self.assertEqual(ctxt.project_id, inst_mapping_mock.project_id)
do_test()
+ @mock.patch.object(objects.Service, 'get_minimum_version',
+ return_value=17)
@mock.patch.object(cinder.API, 'get')
- @mock.patch.object(cinder.API, 'check_attach',
- side_effect=(None, exception.InvalidVolume(reason='error')))
+ @mock.patch.object(cinder.API, 'check_availability_zone',)
+ @mock.patch.object(cinder.API, 'reserve_volume',
+ side_effect=(None, exception.InvalidInput(reason='error')))
def test_provision_instances_cleans_up_when_volume_invalid(self,
- _mock_cinder_get, _mock_cinder_check_attach):
+ _mock_cinder_reserve_volume,
+ _mock_cinder_check_availability_zone, _mock_cinder_get,
+ _mock_get_min_ver):
@mock.patch.object(self.compute_api, '_check_num_instances_quota')
@mock.patch.object(objects, 'Instance')
@mock.patch.object(self.compute_api.security_group_api,
diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py
index b93fead676..c56757e612 100644
--- a/nova/tests/unit/compute/test_compute_mgr.py
+++ b/nova/tests/unit/compute/test_compute_mgr.py
@@ -3106,7 +3106,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
self.assertTrue(mock_power_off.called)
self.assertFalse(mock_destroy.called)
- def _attach(context, instance, bdms, do_check_attach=True):
+ def _attach(context, instance, bdms):
return {'block_device_mapping': 'shared_block_storage'}
def _spawn(context, instance, image_meta, injected_files,
diff --git a/nova/tests/unit/compute/test_shelve.py b/nova/tests/unit/compute/test_shelve.py
index 569d23b1bc..9d6c500d69 100644
--- a/nova/tests/unit/compute/test_shelve.py
+++ b/nova/tests/unit/compute/test_shelve.py
@@ -285,7 +285,7 @@ class ShelveComputeManagerTestCase(test_compute.BaseTestCase):
self.compute._notify_about_instance_usage(self.context, instance,
'unshelve.start')
self.compute._prep_block_device(self.context, instance,
- mox.IgnoreArg(), do_check_attach=False).AndReturn('fake_bdm')
+ mox.IgnoreArg()).AndReturn('fake_bdm')
self.compute.network_api.setup_instance_network_on_host(
self.context, instance, self.compute.host)
self.compute.driver.spawn(self.context, instance,
@@ -367,7 +367,7 @@ class ShelveComputeManagerTestCase(test_compute.BaseTestCase):
'unshelve.start')
self.compute._prep_block_device(self.context, instance,
- mox.IgnoreArg(), do_check_attach=False).AndReturn('fake_bdm')
+ mox.IgnoreArg()).AndReturn('fake_bdm')
self.compute.network_api.setup_instance_network_on_host(
self.context, instance, self.compute.host)
self.rt.instance_claim(self.context, instance, node, limits).AndReturn(
diff --git a/nova/tests/unit/fake_volume.py b/nova/tests/unit/fake_volume.py
index d1794fcaf7..1810560b30 100644
--- a/nova/tests/unit/fake_volume.py
+++ b/nova/tests/unit/fake_volume.py
@@ -179,13 +179,7 @@ class API(object):
self.volume_list = [v for v in self.volume_list
if v['id'] != volume_id]
- def check_attach(self, context, volume, instance=None):
- if volume['status'] != 'available':
- msg = "Status of volume '%s' must be available" % volume
- raise exception.InvalidVolume(reason=msg)
- if volume['attach_status'] == 'attached':
- msg = "already attached"
- raise exception.InvalidVolume(reason=msg)
+ def check_availability_zone(self, context, volume, instance=None):
if instance and not CONF.cinder.cross_az_attach:
if instance['availability_zone'] != volume['availability_zone']:
msg = "Instance and volume not in same availability_zone"
diff --git a/nova/tests/unit/virt/test_block_device.py b/nova/tests/unit/virt/test_block_device.py
index 1a2acaf234..870ec2994f 100644
--- a/nova/tests/unit/virt/test_block_device.py
+++ b/nova/tests/unit/virt/test_block_device.py
@@ -384,11 +384,10 @@ class TestDriverBlockDevice(test.NoDBTestCase):
self._test_call_wait_func(False)
def _test_volume_attach(self, driver_bdm, bdm_dict,
- fake_volume, check_attach=True,
- fail_check_attach=False, driver_attach=False,
- fail_driver_attach=False, volume_attach=True,
- fail_volume_attach=False, access_mode='rw',
- availability_zone=None):
+ fake_volume, fail_check_av_zone=False,
+ driver_attach=False, fail_driver_attach=False,
+ volume_attach=True, fail_volume_attach=False,
+ access_mode='rw', availability_zone=None):
elevated_context = self.context.elevated()
self.stubs.Set(self.context, 'elevated',
lambda: elevated_context)
@@ -406,16 +405,17 @@ class TestDriverBlockDevice(test.NoDBTestCase):
self.volume_api.get(self.context,
fake_volume['id']).AndReturn(fake_volume)
- if check_attach:
- if not fail_check_attach:
- self.volume_api.check_attach(self.context, fake_volume,
- instance=instance).AndReturn(None)
- else:
- self.volume_api.check_attach(self.context, fake_volume,
- instance=instance).AndRaise(
- test.TestingException)
- driver_bdm._bdm_obj.save().AndReturn(None)
- return instance, expected_conn_info
+ if not fail_check_av_zone:
+ self.volume_api.check_availability_zone(self.context,
+ fake_volume,
+ instance=instance).AndReturn(None)
+ else:
+ self.volume_api.check_availability_zone(self.context,
+ fake_volume,
+ instance=instance).AndRaise(
+ test.TestingException)
+ driver_bdm._bdm_obj.save().AndReturn(None)
+ return instance, expected_conn_info
self.virt_driver.get_volume_connector(instance).AndReturn(connector)
self.volume_api.initialize_connection(
@@ -518,13 +518,13 @@ class TestDriverBlockDevice(test.NoDBTestCase):
self.assertEqual(expected_conn_info, test_bdm['connection_info'])
self.assertEqual(42, test_bdm.volume_size)
- def test_volume_attach_check_attach_fails(self):
+ def test_volume_attach_check_av_zone_fails(self):
test_bdm = self.driver_classes['volume'](
self.volume_bdm)
volume = {'id': 'fake-volume-id-1'}
instance, _ = self._test_volume_attach(
- test_bdm, self.volume_bdm, volume, fail_check_attach=True)
+ test_bdm, self.volume_bdm, volume, fail_check_av_zone=True)
self.mox.ReplayAll()
self.assertRaises(test.TestingException, test_bdm.attach, self.context,
@@ -537,14 +537,13 @@ class TestDriverBlockDevice(test.NoDBTestCase):
'attach_status': 'detached'}
instance, expected_conn_info = self._test_volume_attach(
- test_bdm, self.volume_bdm, volume, check_attach=False,
- driver_attach=False)
+ test_bdm, self.volume_bdm, volume, driver_attach=False)
self.mox.ReplayAll()
test_bdm.attach(self.context, instance,
self.volume_api, self.virt_driver,
- do_check_attach=False, do_driver_attach=False)
+ do_driver_attach=False)
self.assertThat(test_bdm['connection_info'],
matchers.DictMatches(expected_conn_info))
@@ -555,14 +554,13 @@ class TestDriverBlockDevice(test.NoDBTestCase):
'attach_status': 'detached'}
instance, expected_conn_info = self._test_volume_attach(
- test_bdm, self.volume_bdm, volume, check_attach=False,
- driver_attach=True)
+ test_bdm, self.volume_bdm, volume, driver_attach=True)
self.mox.ReplayAll()
test_bdm.attach(self.context, instance,
self.volume_api, self.virt_driver,
- do_check_attach=False, do_driver_attach=True)
+ do_driver_attach=True)
self.assertThat(test_bdm['connection_info'],
matchers.DictMatches(expected_conn_info))
@@ -745,8 +743,7 @@ class TestDriverBlockDevice(test.NoDBTestCase):
self.mox.StubOutWithMock(self.volume_api, 'create')
volume_class.attach(self.context, instance, self.volume_api,
- self.virt_driver, do_check_attach=True
- ).AndReturn(None)
+ self.virt_driver).AndReturn(None)
self.mox.ReplayAll()
test_bdm.attach(self.context, instance, self.volume_api,
@@ -854,8 +851,7 @@ class TestDriverBlockDevice(test.NoDBTestCase):
self.mox.StubOutWithMock(self.volume_api, 'create')
volume_class.attach(self.context, instance, self.volume_api,
- self.virt_driver, do_check_attach=True
- ).AndReturn(None)
+ self.virt_driver).AndReturn(None)
self.mox.ReplayAll()
test_bdm.attach(self.context, instance, self.volume_api,
@@ -922,8 +918,7 @@ class TestDriverBlockDevice(test.NoDBTestCase):
'', availability_zone=None)
vol_attach.assert_called_once_with(self.context, instance,
self.volume_api,
- self.virt_driver,
- do_check_attach=True)
+ self.virt_driver)
self.assertEqual('fake-volume-id-2', test_bdm.volume_id)
def test_blank_attach_volume_cinder_cross_az_attach_false(self):
@@ -954,8 +949,7 @@ class TestDriverBlockDevice(test.NoDBTestCase):
'', availability_zone='test-az')
vol_attach.assert_called_once_with(self.context, instance,
self.volume_api,
- self.virt_driver,
- do_check_attach=True)
+ self.virt_driver)
self.assertEqual('fake-volume-id-2', test_bdm.volume_id)
def test_convert_block_devices(self):
diff --git a/nova/tests/unit/volume/test_cinder.py b/nova/tests/unit/volume/test_cinder.py
index 7b325a3a60..a0a2f8987e 100644
--- a/nova/tests/unit/volume/test_cinder.py
+++ b/nova/tests/unit/volume/test_cinder.py
@@ -181,17 +181,6 @@ class CinderApiTestCase(test.NoDBTestCase):
mock_volumes.list.assert_called_once_with(detailed=True,
search_opts={'id': 'id1'})
- def test_check_attach_volume_status_error(self):
- volume = {'id': 'fake', 'status': 'error'}
- self.assertRaises(exception.InvalidVolume,
- self.api.check_attach, self.ctx, volume)
-
- def test_check_attach_volume_already_attached(self):
- volume = {'id': 'fake', 'status': 'available'}
- volume['attach_status'] = "attached"
- self.assertRaises(exception.InvalidVolume,
- self.api.check_attach, self.ctx, volume)
-
@mock.patch.object(cinder.az, 'get_instance_availability_zone',
return_value='zone1')
def test_check_availability_zone_differs(self, mock_get_instance_az):
@@ -207,21 +196,6 @@ class CinderApiTestCase(test.NoDBTestCase):
self.ctx, volume, instance)
mock_get_instance_az.assert_called_once_with(self.ctx, instance)
- def test_check_attach(self):
- volume = {'status': 'available'}
- volume['attach_status'] = "detached"
- volume['availability_zone'] = 'zone1'
- volume['multiattach'] = False
- instance = {'availability_zone': 'zone1', 'host': 'fakehost'}
- CONF.set_override('cross_az_attach', False, group='cinder')
-
- with mock.patch.object(cinder.az, 'get_instance_availability_zone',
- side_effect=lambda context, instance: 'zone1'):
- self.assertIsNone(self.api.check_attach(
- self.ctx, volume, instance))
-
- CONF.reset()
-
def test_check_detach(self):
volume = {'id': 'fake', 'status': 'in-use',
'attach_status': 'attached',
diff --git a/nova/virt/block_device.py b/nova/virt/block_device.py
index 1da9d09912..d8c07aea98 100644
--- a/nova/virt/block_device.py
+++ b/nova/virt/block_device.py
@@ -244,10 +244,10 @@ class DriverVolumeBlockDevice(DriverBlockDevice):
@update_db
def attach(self, context, instance, volume_api, virt_driver,
- do_check_attach=True, do_driver_attach=False, **kwargs):
+ do_driver_attach=False, **kwargs):
volume = volume_api.get(context, self.volume_id)
- if do_check_attach:
- volume_api.check_attach(context, volume, instance=instance)
+ volume_api.check_availability_zone(context, volume,
+ instance=instance)
volume_id = volume['id']
context = context.elevated()
@@ -367,7 +367,7 @@ class DriverSnapshotBlockDevice(DriverVolumeBlockDevice):
_proxy_as_attr = set(['volume_size', 'volume_id', 'snapshot_id'])
def attach(self, context, instance, volume_api,
- virt_driver, wait_func=None, do_check_attach=True):
+ virt_driver, wait_func=None):
if not self.volume_id:
av_zone = _get_volume_create_az_value(instance)
@@ -382,8 +382,7 @@ class DriverSnapshotBlockDevice(DriverVolumeBlockDevice):
# Call the volume attach now
super(DriverSnapshotBlockDevice, self).attach(
- context, instance, volume_api, virt_driver,
- do_check_attach=do_check_attach)
+ context, instance, volume_api, virt_driver)
class DriverImageBlockDevice(DriverVolumeBlockDevice):
@@ -392,7 +391,7 @@ class DriverImageBlockDevice(DriverVolumeBlockDevice):
_proxy_as_attr = set(['volume_size', 'volume_id', 'image_id'])
def attach(self, context, instance, volume_api,
- virt_driver, wait_func=None, do_check_attach=True):
+ virt_driver, wait_func=None):
if not self.volume_id:
av_zone = _get_volume_create_az_value(instance)
vol = volume_api.create(context, self.volume_size,
@@ -404,8 +403,7 @@ class DriverImageBlockDevice(DriverVolumeBlockDevice):
self.volume_id = vol['id']
super(DriverImageBlockDevice, self).attach(
- context, instance, volume_api, virt_driver,
- do_check_attach=do_check_attach)
+ context, instance, volume_api, virt_driver)
class DriverBlankBlockDevice(DriverVolumeBlockDevice):
@@ -414,7 +412,7 @@ class DriverBlankBlockDevice(DriverVolumeBlockDevice):
_proxy_as_attr = set(['volume_size', 'volume_id', 'image_id'])
def attach(self, context, instance, volume_api,
- virt_driver, wait_func=None, do_check_attach=True):
+ virt_driver, wait_func=None):
if not self.volume_id:
vol_name = instance.uuid + '-blank-vol'
av_zone = _get_volume_create_az_value(instance)
@@ -426,8 +424,7 @@ class DriverBlankBlockDevice(DriverVolumeBlockDevice):
self.volume_id = vol['id']
super(DriverBlankBlockDevice, self).attach(
- context, instance, volume_api, virt_driver,
- do_check_attach=do_check_attach)
+ context, instance, volume_api, virt_driver)
def _convert_block_devices(device_type, block_device_mapping):
diff --git a/nova/volume/cinder.py b/nova/volume/cinder.py
index 0199562758..449f700dfe 100644
--- a/nova/volume/cinder.py
+++ b/nova/volume/cinder.py
@@ -254,19 +254,6 @@ class API(object):
"status": volume['status']}
raise exception.InvalidVolume(reason=msg)
- def check_attach(self, context, volume, instance=None):
- # TODO(vish): abstract status checking?
- if volume['status'] != "available":
- msg = _("volume '%(vol)s' status must be 'available'. Currently "
- "in '%(status)s'") % {'vol': volume['id'],
- 'status': volume['status']}
- raise exception.InvalidVolume(reason=msg)
- if volume['attach_status'] == "attached":
- msg = _("volume %s already attached") % volume['id']
- raise exception.InvalidVolume(reason=msg)
-
- self.check_availability_zone(context, volume, instance)
-
def check_availability_zone(self, context, volume, instance=None):
"""Ensure that the availability zone is the same."""
diff --git a/releasenotes/notes/remove-check-attach-68b9ec781ad184ff.yaml b/releasenotes/notes/remove-check-attach-68b9ec781ad184ff.yaml
new file mode 100644
index 0000000000..9986fd89dc
--- /dev/null
+++ b/releasenotes/notes/remove-check-attach-68b9ec781ad184ff.yaml
@@ -0,0 +1,26 @@
+---
+fixes:
+ - |
+ Fixes `bug 1581230`_ by removing the internal ``check_attach`` call from
+ the Nova code as it can cause race conditions and the checks are handled by
+ ``reserve_volume`` in Cinder. ``reserve_volume`` is called in every volume
+ attach scenario to provide the necessary checks and volume state validation
+ on the Cinder side.
+other:
+ - |
+ By removing the ``check_attach`` internal call from Nova, small behavioral
+ changes were introduced.
+
+ ``reserve_volume`` call was added to the boot from volume scenario. In case
+ a failure occurs while building the instance, the instance goes into ERROR
+ state while the volume stays in ``attaching`` state. The volume state will
+ be set back to ``available`` when the instance gets deleted.
+
+ Additional availability zone check is added to the volume attach flow,
+ which results in an availability zone check when an instance gets
+ unshelved. In case the deployment is not sensitive to availability zones
+ and not using the AvailabilityZoneFilter scheduler filter the current
+ default settings (cross_az_attach=True) are allowing to perform unshelve
+ the same way as before this change without additional configuration.
+
+ .. _`bug 1581230`: https://bugs.launchpad.net/nova/+bug/1581230