diff --git a/cinder/tests/fake_driver.py b/cinder/tests/fake_driver.py index 6234e00955a..1d0e96e7cad 100644 --- a/cinder/tests/fake_driver.py +++ b/cinder/tests/fake_driver.py @@ -74,6 +74,9 @@ class FakeLoggingVolumeDriver(lvm.LVMVolumeDriver): def create_cloned_volume(self, volume, src_vol): pass + def create_volume_from_snapshot(self, volume, snapshot): + pass + def initialize_connection(self, volume, connector): # NOTE(thangp): There are several places in the core cinder code where # the volume passed through is a dict and not an oslo_versionedobject. diff --git a/cinder/tests/functional/api/client.py b/cinder/tests/functional/api/client.py index 53a2e796fc1..7baeae9c7e0 100644 --- a/cinder/tests/functional/api/client.py +++ b/cinder/tests/functional/api/client.py @@ -63,7 +63,7 @@ class TestOpenStackClient(object): """ - def __init__(self, auth_user, auth_key, auth_uri): + def __init__(self, auth_user, auth_key, auth_uri, api_version=None): super(TestOpenStackClient, self).__init__() self.auth_result = None self.auth_user = auth_user @@ -71,6 +71,7 @@ class TestOpenStackClient(object): self.auth_uri = auth_uri # default project_id self.project_id = fake.PROJECT_ID + self.api_version = api_version def request(self, url, method='GET', body=None, headers=None, ssl_verify=True, stream=False): @@ -133,6 +134,9 @@ class TestOpenStackClient(object): headers = kwargs.setdefault('headers', {}) headers['X-Auth-Token'] = auth_result['x-auth-token'] + if self.api_version: + headers['OpenStack-API-Version'] = 'volume ' + self.api_version + response = self.request(full_uri, **kwargs) http_status = response.status_code @@ -219,3 +223,50 @@ class TestOpenStackClient(object): type['extra_specs'] = extra_specs return self.api_post('/types', type)['volume_type'] + + def delete_type(self, type_id): + return self.api_delete('/types/%s' % type_id) + + def create_group_type(self, type_name, grp_specs=None): + grp_type = {"group_type": {"name": type_name}} + if grp_specs: + grp_type['group_specs'] = grp_specs + + return self.api_post('/group_types', grp_type)['group_type'] + + def delete_group_type(self, group_type_id): + return self.api_delete('/group_types/%s' % group_type_id) + + def get_group(self, group_id): + return self.api_get('/groups/%s' % group_id)['group'] + + def get_groups(self, detail=True): + rel_url = '/groups/detail' if detail else '/groups' + return self.api_get(rel_url)['groups'] + + def post_group(self, group): + return self.api_post('/groups', group)['group'] + + def post_group_from_src(self, group): + return self.api_post('/groups/action', group)['group'] + + def delete_group(self, group_id, params): + return self.api_post('/groups/%s/action' % group_id, params) + + def put_group(self, group_id, group): + return self.api_put('/groups/%s' % group_id, group)['group'] + + def get_group_snapshot(self, group_snapshot_id): + return self.api_get('/group_snapshots/%s' % group_snapshot_id)[ + 'group_snapshot'] + + def get_group_snapshots(self, detail=True): + rel_url = '/group_snapshots/detail' if detail else '/group_snapshots' + return self.api_get(rel_url)['group_snapshots'] + + def post_group_snapshot(self, group_snapshot): + return self.api_post('/group_snapshots', group_snapshot)[ + 'group_snapshot'] + + def delete_group_snapshot(self, group_snapshot_id): + return self.api_delete('/group_snapshots/%s' % group_snapshot_id) diff --git a/cinder/tests/functional/functional_helpers.py b/cinder/tests/functional/functional_helpers.py index 240ec7b90d3..aafc34397fc 100644 --- a/cinder/tests/functional/functional_helpers.py +++ b/cinder/tests/functional/functional_helpers.py @@ -59,6 +59,9 @@ def generate_new_element(items, prefix, numeric=False): class _FunctionalTestBase(test.TestCase): + osapi_version_major = '2' + osapi_version_minor = '0' + def setUp(self): super(_FunctionalTestBase, self).setUp() @@ -78,8 +81,10 @@ class _FunctionalTestBase(test.TestCase): self._start_api_service() self.addCleanup(self.osapi.stop) + api_version = self.osapi_version_major + '.' + self.osapi_version_minor self.api = client.TestOpenStackClient(fake.USER_ID, - fake.PROJECT_ID, self.auth_url) + fake.PROJECT_ID, self.auth_url, + api_version) def _update_project(self, new_project_id): self.api.update_project(new_project_id) @@ -93,7 +98,8 @@ class _FunctionalTestBase(test.TestCase): self.osapi.start() # FIXME(ja): this is not the auth url - this is the service url # FIXME(ja): this needs fixed in nova as well - self.auth_url = 'http://%s:%s/v2' % (self.osapi.host, self.osapi.port) + self.auth_url = 'http://%s:%s/v' % (self.osapi.host, self.osapi.port) + self.auth_url += self.osapi_version_major def _get_flags(self): """An opportunity to setup flags, before the services are started.""" @@ -167,3 +173,63 @@ class _FunctionalTestBase(test.TestCase): time.sleep(1) retries += 1 + + def _poll_group_while(self, group_id, continue_states, + expected_end_status=None, max_retries=30): + """Poll (briefly) while the state is in continue_states. + + Continues until the state changes from continue_states or max_retries + are hit. If expected_end_status is specified, we assert that the end + status of the group is expected_end_status. + """ + retries = 0 + while retries <= max_retries: + try: + found_grp = self.api.get_group(group_id) + except client.OpenStackApiException404: + return None + except client.OpenStackApiException: + # NOTE(xyang): Got OpenStackApiException( + # u'Unexpected status code',) sometimes, but + # it works if continue. + continue + + self.assertEqual(group_id, found_grp['id']) + grp_status = found_grp['status'] + if grp_status not in continue_states: + if expected_end_status: + self.assertEqual(expected_end_status, grp_status) + return found_grp + + time.sleep(1) + retries += 1 + + def _poll_group_snapshot_while(self, group_snapshot_id, continue_states, + expected_end_status=None, max_retries=30): + """Poll (briefly) while the state is in continue_states. + + Continues until the state changes from continue_states or max_retries + are hit. If expected_end_status is specified, we assert that the end + status of the group_snapshot is expected_end_status. + """ + retries = 0 + while retries <= max_retries: + try: + found_grp_snap = self.api.get_group_snapshot(group_snapshot_id) + except client.OpenStackApiException404: + return None + except client.OpenStackApiException: + # NOTE(xyang): Got OpenStackApiException( + # u'Unexpected status code',) sometimes, but + # it works if continue. + continue + + self.assertEqual(group_snapshot_id, found_grp_snap['id']) + grp_snap_status = found_grp_snap['status'] + if grp_snap_status not in continue_states: + if expected_end_status: + self.assertEqual(expected_end_status, grp_snap_status) + return found_grp_snap + + time.sleep(1) + retries += 1 diff --git a/cinder/tests/functional/test_group_snapshots.py b/cinder/tests/functional/test_group_snapshots.py new file mode 100644 index 00000000000..c9487dfb6df --- /dev/null +++ b/cinder/tests/functional/test_group_snapshots.py @@ -0,0 +1,297 @@ +# Copyright 2016 EMC Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cinder.tests.functional import functional_helpers + + +class GroupSnapshotsTest(functional_helpers._FunctionalTestBase): + _vol_type_name = 'functional_test_type' + _grp_type_name = 'functional_grp_test_type' + osapi_version_major = '3' + osapi_version_minor = '14' + + def setUp(self): + super(GroupSnapshotsTest, self).setUp() + self.volume_type = self.api.create_type(self._vol_type_name) + self.group_type = self.api.create_group_type(self._grp_type_name) + + def _get_flags(self): + f = super(GroupSnapshotsTest, self)._get_flags() + f['volume_driver'] = ( + 'cinder.tests.fake_driver.FakeLoggingVolumeDriver') + f['default_volume_type'] = self._vol_type_name + f['default_group_type'] = self._grp_type_name + return f + + def test_get_group_snapshots_summary(self): + """Simple check that listing group snapshots works.""" + grp_snaps = self.api.get_group_snapshots(False) + self.assertIsNotNone(grp_snaps) + + def test_get_group_snapshots(self): + """Simple check that listing group snapshots works.""" + grp_snaps = self.api.get_group_snapshots() + self.assertIsNotNone(grp_snaps) + + def test_create_and_delete_group_snapshot(self): + """Creates and deletes a group snapshot.""" + + # Create group + created_group = self.api.post_group( + {'group': {'group_type': self.group_type['id'], + 'volume_types': [self.volume_type['id']]}}) + self.assertTrue(created_group['id']) + created_group_id = created_group['id'] + + # Check it's there + found_group = self._poll_group_while(created_group_id, + ['creating']) + self.assertEqual(created_group_id, found_group['id']) + self.assertEqual(self.group_type['id'], found_group['group_type']) + self.assertEqual('available', found_group['status']) + + # Create volume + created_volume = self.api.post_volume( + {'volume': {'size': 1, + 'group_id': created_group_id, + 'volume_type': self.volume_type['id']}}) + self.assertTrue(created_volume['id']) + created_volume_id = created_volume['id'] + + # Check it's there + found_volume = self.api.get_volume(created_volume_id) + self.assertEqual(created_volume_id, found_volume['id']) + self.assertEqual(self._vol_type_name, found_volume['volume_type']) + self.assertEqual(created_group_id, found_volume['group_id']) + + # Wait (briefly) for creation. Delay is due to the 'message queue' + found_volume = self._poll_volume_while(created_volume_id, ['creating']) + + # It should be available... + self.assertEqual('available', found_volume['status']) + + # Create group snapshot + created_group_snapshot = self.api.post_group_snapshot( + {'group_snapshot': {'group_id': created_group_id}}) + self.assertTrue(created_group_snapshot['id']) + created_group_snapshot_id = created_group_snapshot['id'] + + # Check it's there + found_group_snapshot = self._poll_group_snapshot_while( + created_group_snapshot_id, ['creating']) + self.assertEqual(created_group_snapshot_id, found_group_snapshot['id']) + self.assertEqual(created_group_id, + found_group_snapshot['group_id']) + self.assertEqual('available', found_group_snapshot['status']) + + # Delete the group snapshot + self.api.delete_group_snapshot(created_group_snapshot_id) + + # Wait (briefly) for deletion. Delay is due to the 'message queue' + found_group_snapshot = self._poll_group_snapshot_while( + created_group_snapshot_id, ['deleting']) + + # Delete the original group + self.api.delete_group(created_group_id, + {'delete': {'delete-volumes': True}}) + + # Wait (briefly) for deletion. Delay is due to the 'message queue' + found_volume = self._poll_volume_while(created_volume_id, ['deleting']) + found_group = self._poll_group_while(created_group_id, ['deleting']) + + # Should be gone + self.assertFalse(found_group_snapshot) + self.assertFalse(found_volume) + self.assertFalse(found_group) + + def test_create_group_from_group_snapshot(self): + """Creates a group from a group snapshot.""" + + # Create group + created_group = self.api.post_group( + {'group': {'group_type': self.group_type['id'], + 'volume_types': [self.volume_type['id']]}}) + self.assertTrue(created_group['id']) + created_group_id = created_group['id'] + + # Check it's there + found_group = self._poll_group_while(created_group_id, + ['creating']) + self.assertEqual(created_group_id, found_group['id']) + self.assertEqual(self.group_type['id'], found_group['group_type']) + self.assertEqual('available', found_group['status']) + + # Create volume + created_volume = self.api.post_volume( + {'volume': {'size': 1, + 'group_id': created_group_id, + 'volume_type': self.volume_type['id']}}) + self.assertTrue(created_volume['id']) + created_volume_id = created_volume['id'] + + # Check it's there + found_volume = self.api.get_volume(created_volume_id) + self.assertEqual(created_volume_id, found_volume['id']) + self.assertEqual(self._vol_type_name, found_volume['volume_type']) + self.assertEqual(created_group_id, found_volume['group_id']) + + # Wait (briefly) for creation. Delay is due to the 'message queue' + found_volume = self._poll_volume_while(created_volume_id, ['creating']) + + # It should be available... + self.assertEqual('available', found_volume['status']) + + # Create group snapshot + created_group_snapshot = self.api.post_group_snapshot( + {'group_snapshot': {'group_id': created_group_id}}) + self.assertTrue(created_group_snapshot['id']) + created_group_snapshot_id = created_group_snapshot['id'] + + # Check it's there + found_group_snapshot = self._poll_group_snapshot_while( + created_group_snapshot_id, ['creating']) + self.assertEqual(created_group_snapshot_id, found_group_snapshot['id']) + self.assertEqual(created_group_id, + found_group_snapshot['group_id']) + self.assertEqual('available', found_group_snapshot['status']) + + # Create group from group snapshot + created_group_from_snap = self.api.post_group_from_src( + {'create-from-src': { + 'group_snapshot_id': created_group_snapshot_id}}) + self.assertTrue(created_group_from_snap['id']) + created_group_from_snap_id = created_group_from_snap['id'] + + # Check it's there + found_volumes = self.api.get_volumes() + self._poll_volume_while(found_volumes[0], ['creating']) + self._poll_volume_while(found_volumes[1], ['creating']) + found_group_from_snap = self._poll_group_while( + created_group_from_snap_id, ['creating']) + self.assertEqual(created_group_from_snap_id, + found_group_from_snap['id']) + self.assertEqual(created_group_snapshot_id, + found_group_from_snap['group_snapshot_id']) + self.assertEqual(self.group_type['id'], + found_group_from_snap['group_type']) + self.assertEqual('available', found_group_from_snap['status']) + + # Delete the group from snap + self.api.delete_group(created_group_from_snap_id, + {'delete': {'delete-volumes': True}}) + + # Wait (briefly) for deletion. Delay is due to the 'message queue' + found_group_from_snap = self._poll_group_while( + created_group_from_snap_id, ['deleting']) + + # Delete the group snapshot + self.api.delete_group_snapshot(created_group_snapshot_id) + + # Wait (briefly) for deletion. Delay is due to the 'message queue' + found_group_snapshot = self._poll_group_snapshot_while( + created_group_snapshot_id, ['deleting']) + + # Delete the original group + self.api.delete_group(created_group_id, + {'delete': {'delete-volumes': True}}) + + # Wait (briefly) for deletion. Delay is due to the 'message queue' + found_volume = self._poll_volume_while(created_volume_id, ['deleting']) + found_group = self._poll_group_while(created_group_id, ['deleting']) + + # Should be gone + self.assertFalse(found_group_from_snap) + self.assertFalse(found_group_snapshot) + self.assertFalse(found_volume) + self.assertFalse(found_group) + + def test_create_group_from_source_group(self): + """Creates a group from a source group.""" + + # Create group + created_group = self.api.post_group( + {'group': {'group_type': self.group_type['id'], + 'volume_types': [self.volume_type['id']]}}) + self.assertTrue(created_group['id']) + created_group_id = created_group['id'] + + # Check it's there + found_group = self._poll_group_while(created_group_id, + ['creating']) + self.assertEqual(created_group_id, found_group['id']) + self.assertEqual(self.group_type['id'], found_group['group_type']) + self.assertEqual('available', found_group['status']) + + # Create volume + created_volume = self.api.post_volume( + {'volume': {'size': 1, + 'group_id': created_group_id, + 'volume_type': self.volume_type['id']}}) + self.assertTrue(created_volume['id']) + created_volume_id = created_volume['id'] + + # Check it's there + found_volume = self.api.get_volume(created_volume_id) + self.assertEqual(created_volume_id, found_volume['id']) + self.assertEqual(self._vol_type_name, found_volume['volume_type']) + self.assertEqual(created_group_id, found_volume['group_id']) + + # Wait (briefly) for creation. Delay is due to the 'message queue' + found_volume = self._poll_volume_while(created_volume_id, ['creating']) + + # It should be available... + self.assertEqual('available', found_volume['status']) + + # Test create group from source group + created_group_from_group = self.api.post_group_from_src( + {'create-from-src': { + 'source_group_id': created_group_id}}) + self.assertTrue(created_group_from_group['id']) + created_group_from_group_id = created_group_from_group['id'] + + # Check it's there + found_volumes = self.api.get_volumes() + self._poll_volume_while(found_volumes[0], ['creating']) + self._poll_volume_while(found_volumes[1], ['creating']) + found_group_from_group = self._poll_group_while( + created_group_from_group_id, ['creating']) + self.assertEqual(created_group_from_group_id, + found_group_from_group['id']) + self.assertEqual(created_group_id, + found_group_from_group['source_group_id']) + self.assertEqual(self.group_type['id'], + found_group_from_group['group_type']) + self.assertEqual('available', found_group_from_group['status']) + + # Delete the group from group + self.api.delete_group(created_group_from_group_id, + {'delete': {'delete-volumes': True}}) + + # Wait (briefly) for deletion. Delay is due to the 'message queue' + found_group_from_group = self._poll_group_while( + created_group_from_group_id, ['deleting']) + + # Delete the original group + self.api.delete_group(created_group_id, + {'delete': {'delete-volumes': True}}) + + # Wait (briefly) for deletion. Delay is due to the 'message queue' + found_volume = self._poll_volume_while(created_volume_id, ['deleting']) + found_group = self._poll_group_while(created_group_id, ['deleting']) + + # Should be gone + self.assertFalse(found_group_from_group) + self.assertFalse(found_volume) + self.assertFalse(found_group) diff --git a/cinder/tests/functional/test_groups.py b/cinder/tests/functional/test_groups.py new file mode 100644 index 00000000000..3ac69b305f8 --- /dev/null +++ b/cinder/tests/functional/test_groups.py @@ -0,0 +1,95 @@ +# Copyright 2016 EMC Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cinder.tests.functional import functional_helpers + + +class GroupsTest(functional_helpers._FunctionalTestBase): + _vol_type_name = 'functional_test_type' + _grp_type_name = 'functional_grp_test_type' + osapi_version_major = '3' + osapi_version_minor = '13' + + def setUp(self): + super(GroupsTest, self).setUp() + self.volume_type = self.api.create_type(self._vol_type_name) + self.group_type = self.api.create_group_type(self._grp_type_name) + + def _get_flags(self): + f = super(GroupsTest, self)._get_flags() + f['volume_driver'] = ( + 'cinder.tests.fake_driver.FakeLoggingVolumeDriver') + f['default_volume_type'] = self._vol_type_name + f['default_group_type'] = self._grp_type_name + return f + + def test_get_groups_summary(self): + """Simple check that listing groups works.""" + grps = self.api.get_groups(False) + self.assertIsNotNone(grps) + + def test_get_groups(self): + """Simple check that listing groups works.""" + grps = self.api.get_groups() + self.assertIsNotNone(grps) + + def test_create_and_delete_group(self): + """Creates and deletes a group.""" + + # Create group + created_group = self.api.post_group( + {'group': {'group_type': self.group_type['id'], + 'volume_types': [self.volume_type['id']]}}) + self.assertTrue(created_group['id']) + created_group_id = created_group['id'] + + # Check it's there + found_group = self._poll_group_while(created_group_id, + ['creating']) + self.assertEqual(created_group_id, found_group['id']) + self.assertEqual(self.group_type['id'], found_group['group_type']) + self.assertEqual('available', found_group['status']) + + # Create volume + created_volume = self.api.post_volume( + {'volume': {'size': 1, + 'group_id': created_group_id, + 'volume_type': self.volume_type['id']}}) + self.assertTrue(created_volume['id']) + created_volume_id = created_volume['id'] + + # Check it's there + found_volume = self.api.get_volume(created_volume_id) + self.assertEqual(created_volume_id, found_volume['id']) + self.assertEqual(self._vol_type_name, found_volume['volume_type']) + self.assertEqual(created_group_id, found_volume['group_id']) + + # Wait (briefly) for creation. Delay is due to the 'message queue' + found_volume = self._poll_volume_while(created_volume_id, ['creating']) + + # It should be available... + self.assertEqual('available', found_volume['status']) + + # Delete the original group + self.api.delete_group(created_group_id, + {'delete': {'delete-volumes': True}}) + + # Wait (briefly) for deletion. Delay is due to the 'message queue' + found_volume = self._poll_volume_while(created_volume_id, ['deleting']) + found_group = self._poll_group_while(created_group_id, ['deleting']) + + # Should be gone + self.assertFalse(found_volume) + self.assertFalse(found_group) diff --git a/cinder/tests/unit/group/test_groups_manager.py b/cinder/tests/unit/group/test_groups_manager.py index 18515be9836..bc5a8703e6c 100644 --- a/cinder/tests/unit/group/test_groups_manager.py +++ b/cinder/tests/unit/group/test_groups_manager.py @@ -29,7 +29,7 @@ from cinder.tests.unit import conf_fixture from cinder.tests.unit import fake_constants as fake from cinder.tests.unit import fake_snapshot from cinder.tests.unit import utils as tests_utils -import cinder.volume +from cinder.volume import api as volume_api from cinder.volume import configuration as conf from cinder.volume import driver from cinder.volume import utils as volutils @@ -51,16 +51,15 @@ class GroupManagerTestCase(test.TestCase): self.volume.driver.set_initialized() self.volume.stats = {'allocated_capacity_gb': 0, 'pools': {}} - self.volume_api = cinder.volume.api.API() + self.volume_api = volume_api.API() def test_delete_volume_in_group(self): """Test deleting a volume that's tied to a group fails.""" - volume_api = cinder.volume.api.API() volume_params = {'status': 'available', 'group_id': fake.GROUP_ID} volume = tests_utils.create_volume(self.context, **volume_params) self.assertRaises(exception.InvalidVolume, - volume_api.delete, self.context, volume) + self.volume_api.delete, self.context, volume) @mock.patch.object(GROUP_QUOTAS, "reserve", return_value=["RESERVATION"]) @@ -675,18 +674,17 @@ class GroupManagerTestCase(test.TestCase): 'id': '9999', 'name': 'fake', } - vol_api = cinder.volume.api.API() # Volume type must be provided when creating a volume in a # group. self.assertRaises(exception.InvalidInput, - vol_api.create, + self.volume_api.create, self.context, 1, 'vol1', 'volume 1', group=grp) # Volume type must be valid. self.assertRaises(exception.InvalidInput, - vol_api.create, + self.volume_api.create, self.context, 1, 'vol1', 'volume 1', volume_type=fake_type, group=grp) diff --git a/cinder/tests/unit/test_volume.py b/cinder/tests/unit/test_volume.py index d8a67978f8a..77a32852c9b 100644 --- a/cinder/tests/unit/test_volume.py +++ b/cinder/tests/unit/test_volume.py @@ -1603,7 +1603,7 @@ class VolumeTestCase(BaseVolumeTestCase): pass @mock.patch.object(coordination.Coordinator, 'get_lock') - @mock.patch.object(cinder.volume.drivers.lvm.LVMVolumeDriver, + @mock.patch.object(cinder.tests.fake_driver.FakeLoggingVolumeDriver, 'create_volume_from_snapshot') def test_create_volume_from_snapshot_check_locks( self, mock_lvm_create, mock_lock):