diff --git a/cinder/tests/test_volume.py b/cinder/tests/test_volume.py index 72530eae236..198bf895718 100644 --- a/cinder/tests/test_volume.py +++ b/cinder/tests/test_volume.py @@ -89,7 +89,8 @@ class VolumeTestCase(test.TestCase): @staticmethod def _create_volume(size=0, snapshot_id=None, image_id=None, - source_volid=None, metadata=None, status="creating"): + source_volid=None, metadata=None, status="creating", + availability_zone=None): """Create a volume object.""" vol = {} vol['size'] = size @@ -98,7 +99,8 @@ class VolumeTestCase(test.TestCase): vol['source_volid'] = source_volid vol['user_id'] = 'fake' vol['project_id'] = 'fake' - vol['availability_zone'] = CONF.storage_availability_zone + vol['availability_zone'] = \ + availability_zone or CONF.storage_availability_zone vol['status'] = status vol['attach_status'] = "detached" vol['host'] = CONF.host @@ -353,6 +355,41 @@ class VolumeTestCase(test.TestCase): description='fake_desc', snapshot=snapshot) + def test_create_volume_from_snapshot_fail_wrong_az(self): + """Test volume can't be created from snapshot in a different az.""" + volume_api = cinder.volume.api.API() + + def fake_list_availability_zones(): + return ({'name': 'nova', 'available': True}, + {'name': 'az2', 'available': True}) + + self.stubs.Set(volume_api, + 'list_availability_zones', + fake_list_availability_zones) + + volume_src = self._create_volume(availability_zone='az2') + self.volume.create_volume(self.context, volume_src['id']) + snapshot = self._create_snapshot(volume_src['id']) + self.volume.create_snapshot(self.context, volume_src['id'], + snapshot['id']) + snapshot = db.snapshot_get(self.context, snapshot['id']) + + volume_dst = volume_api.create(self.context, + size=1, + name='fake_name', + description='fake_desc', + snapshot=snapshot) + self.assertEqual(volume_dst['availability_zone'], 'az2') + + self.assertRaises(exception.InvalidInput, + volume_api.create, + self.context, + size=1, + name='fake_name', + description='fake_desc', + snapshot=snapshot, + availability_zone='nova') + def test_create_volume_with_invalid_exclusive_options(self): """Test volume create with multiple exclusive options fails.""" volume_api = cinder.volume.api.API() @@ -1394,6 +1431,39 @@ class VolumeTestCase(test.TestCase): self.volume.delete_volume(self.context, volume_dst['id']) self.volume.delete_volume(self.context, volume_src['id']) + def test_create_volume_from_sourcevol_fail_wrong_az(self): + """Test volume can't be cloned from an other volume in different az.""" + volume_api = cinder.volume.api.API() + + def fake_list_availability_zones(): + return ({'name': 'nova', 'available': True}, + {'name': 'az2', 'available': True}) + + self.stubs.Set(volume_api, + 'list_availability_zones', + fake_list_availability_zones) + + volume_src = self._create_volume(availability_zone='az2') + self.volume.create_volume(self.context, volume_src['id']) + + volume_src = db.volume_get(self.context, volume_src['id']) + + volume_dst = volume_api.create(self.context, + size=1, + name='fake_name', + description='fake_desc', + source_volume=volume_src) + self.assertEqual(volume_dst['availability_zone'], 'az2') + + self.assertRaises(exception.InvalidInput, + volume_api.create, + self.context, + size=1, + name='fake_name', + description='fake_desc', + source_volume=volume_src, + availability_zone='nova') + def test_create_volume_from_sourcevol_with_glance_metadata(self): """Test glance metadata can be correctly copied to new volume.""" def fake_create_cloned_volume(volume, src_vref): diff --git a/cinder/volume/api.py b/cinder/volume/api.py index 43d4a99e14f..2fe3290ffd1 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -45,9 +45,14 @@ volume_host_opt = cfg.BoolOpt('snapshot_same_host', default=True, help='Create volume from snapshot at the host ' 'where snapshot resides') +volume_same_az_opt = cfg.BoolOpt('cloned_volume_same_az', + default=True, + help='Ensure that the new volumes are the ' + 'same AZ as snapshot or source volume') CONF = cfg.CONF CONF.register_opt(volume_host_opt) +CONF.register_opt(volume_same_az_opt) CONF.import_opt('storage_availability_zone', 'cinder.volume.manager') LOG = logging.getLogger(__name__) @@ -160,6 +165,29 @@ class API(base.Base): msg = _('Image minDisk size is larger than the volume size.') raise exception.InvalidInput(reason=msg) + if availability_zone is None: + if snapshot is not None: + availability_zone = snapshot['volume']['availability_zone'] + elif source_volume is not None: + availability_zone = source_volume['availability_zone'] + else: + availability_zone = CONF.storage_availability_zone + else: + self._check_availabilty_zone(availability_zone) + + if CONF.cloned_volume_same_az: + if (snapshot and + snapshot['volume']['availability_zone'] != + availability_zone): + msg = _("Volume must be in the same " + "availability zone as the snapshot") + raise exception.InvalidInput(reason=msg) + elif source_volume and \ + source_volume['availability_zone'] != availability_zone: + msg = _("Volume must be in the same " + "availability zone as the source volume") + raise exception.InvalidInput(reason=msg) + if not volume_type and not source_volume: volume_type = volume_types.get_default_volume_type() @@ -198,11 +226,6 @@ class API(base.Base): 'd_consumed': _consumed(over)}) raise exception.VolumeLimitExceeded(allowed=quotas[over]) - if availability_zone is None: - availability_zone = CONF.storage_availability_zone - else: - self._check_availabilty_zone(availability_zone) - self._check_metadata_properties(context, metadata) options = {'size': size, 'user_id': context.user_id,