From 9b434d11174412c02189f4095087bab50fe6711e Mon Sep 17 00:00:00 2001 From: Patrick East Date: Fri, 12 Aug 2016 17:23:19 -0700 Subject: [PATCH] Add tempest tests for Consistency Groups These tempest tests add coverage for all the API's related to consistency groups in Cinder. They were originally proposed for upstream tempest in https://review.openstack.org/#/c/252213/ but were not a good fit since they aren't supported by the reference driver. They have been modified to work as part of the in-tree tempest plugins for Cinder now. The tests are behind a new config option for tempest, which in turn is part of a new config group called 'cinder'. This was added to avoid any collisions with the 'volume-features-enabled' or 'volume' groups already in the upstream tempest tests. To enable them set the following in tempest.conf [cinder] consistency_group = True Then make sure to run tempest with the 'all-plugin' tox environment. Don't forget to update policy.json to allow for CG API's to be called.. Change-Id: I772ea13ca156e71620d722eee476f222a8653831 Co-Authored-By: Xing Yang --- .../api/volume/test_consistencygroups.py | 283 ++++++++++++++++++ cinder/tests/tempest/cinder_clients.py | 37 +++ cinder/tests/tempest/config.py | 11 + cinder/tests/tempest/plugin.py | 12 +- .../services/consistencygroups_client.py | 192 ++++++++++++ 5 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 cinder/tests/tempest/api/volume/test_consistencygroups.py create mode 100644 cinder/tests/tempest/cinder_clients.py create mode 100644 cinder/tests/tempest/services/consistencygroups_client.py diff --git a/cinder/tests/tempest/api/volume/test_consistencygroups.py b/cinder/tests/tempest/api/volume/test_consistencygroups.py new file mode 100644 index 0000000..09dd84e --- /dev/null +++ b/cinder/tests/tempest/api/volume/test_consistencygroups.py @@ -0,0 +1,283 @@ +# Copyright (C) 2015 EMC Corporation. +# Copyright (C) 2016 Pure Storage, Inc. +# 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 oslo_log import log as logging +from tempest.api.volume import base +from tempest.common import waiters +from tempest import config +from tempest.lib.common.utils import data_utils +from tempest import test + +from cinder.tests.tempest import cinder_clients + +CONF = config.CONF +LOG = logging.getLogger(__name__) + + +class ConsistencyGroupsV2Test(base.BaseVolumeAdminTest): + + @classmethod + def setup_clients(cls): + cls._api_version = 2 + super(ConsistencyGroupsV2Test, cls).setup_clients() + + manager = cinder_clients.Manager(cls.os_adm) + cls.consistencygroups_adm_client = manager.consistencygroups_adm_client + + @classmethod + def skip_checks(cls): + super(ConsistencyGroupsV2Test, cls).skip_checks() + if not CONF.cinder.consistency_group: + raise cls.skipException("Cinder consistency group " + "feature disabled") + + def _delete_consistencygroup(self, cg_id): + self.consistencygroups_adm_client.delete_consistencygroup(cg_id) + vols = self.admin_volume_client.list_volumes(detail=True)['volumes'] + for vol in vols: + if vol['consistencygroup_id'] == cg_id: + self.admin_volume_client.wait_for_resource_deletion(vol['id']) + self.consistencygroups_adm_client.wait_for_consistencygroup_deletion( + cg_id) + + def _delete_cgsnapshot(self, cgsnapshot_id, cg_id): + self.consistencygroups_adm_client.delete_cgsnapshot(cgsnapshot_id) + vols = self.admin_volume_client.list_volumes(detail=True)['volumes'] + snapshots = self.admin_snapshots_client.list_snapshots( + detail=True)['snapshots'] + for vol in vols: + for snap in snapshots: + if (vol['consistencygroup_id'] == cg_id and + vol['id'] == snap['volume_id']): + self.snapshots_client.wait_for_resource_deletion( + snap['id']) + self.consistencygroups_adm_client.wait_for_cgsnapshot_deletion( + cgsnapshot_id) + + @test.idempotent_id('3fe776ba-ec1f-4e6c-8d78-4b14c3a7fc44') + def test_consistencygroup_create_delete(self): + # Create volume type + name = data_utils.rand_name("volume-type") + volume_type = self.admin_volume_types_client.create_volume_type( + name=name)['volume_type'] + + # Create CG + cg_name = data_utils.rand_name('CG') + create_consistencygroup = ( + self.consistencygroups_adm_client.create_consistencygroup) + cg = create_consistencygroup(volume_type['id'], + name=cg_name)['consistencygroup'] + vol_name = data_utils.rand_name("volume") + self.name_field = self.special_fields['name_field'] + params = {self.name_field: vol_name, + 'volume_type': volume_type['id'], + 'consistencygroup_id': cg['id']} + + # Create volume + volume = self.admin_volume_client.create_volume(**params)['volume'] + + waiters.wait_for_volume_status(self.admin_volume_client, + volume['id'], 'available') + self.consistencygroups_adm_client.wait_for_consistencygroup_status( + cg['id'], 'available') + self.assertEqual(cg_name, cg['name']) + + # Get a given CG + cg = self.consistencygroups_adm_client.show_consistencygroup( + cg['id'])['consistencygroup'] + self.assertEqual(cg_name, cg['name']) + + # Get all CGs with detail + cgs = self.consistencygroups_adm_client.list_consistencygroups( + detail=True)['consistencygroups'] + self.assertIn((cg['name'], cg['id']), + [(m['name'], m['id']) for m in cgs]) + + # Clean up + self._delete_consistencygroup(cg['id']) + self.admin_volume_types_client.delete_volume_type(volume_type['id']) + + @test.idempotent_id('2134dd52-f333-4456-bb05-6cb0f009a44f') + def test_consistencygroup_cgsnapshot_create_delete(self): + # Create volume type + name = data_utils.rand_name("volume-type") + volume_type = self.admin_volume_types_client.create_volume_type( + name=name)['volume_type'] + + # Create CG + cg_name = data_utils.rand_name('CG') + create_consistencygroup = ( + self.consistencygroups_adm_client.create_consistencygroup) + cg = create_consistencygroup(volume_type['id'], + name=cg_name)['consistencygroup'] + vol_name = data_utils.rand_name("volume") + self.name_field = self.special_fields['name_field'] + params = {self.name_field: vol_name, + 'volume_type': volume_type['id'], + 'consistencygroup_id': cg['id']} + + # Create volume + volume = self.admin_volume_client.create_volume(**params)['volume'] + waiters.wait_for_volume_status(self.admin_volume_client, + volume['id'], 'available') + self.consistencygroups_adm_client.wait_for_consistencygroup_status( + cg['id'], 'available') + self.assertEqual(cg_name, cg['name']) + + # Create cgsnapshot + cgsnapshot_name = data_utils.rand_name('cgsnapshot') + create_cgsnapshot = ( + self.consistencygroups_adm_client.create_cgsnapshot) + cgsnapshot = create_cgsnapshot(cg['id'], + name=cgsnapshot_name)['cgsnapshot'] + snapshots = self.admin_snapshots_client.list_snapshots( + detail=True)['snapshots'] + for snap in snapshots: + if volume['id'] == snap['volume_id']: + waiters.wait_for_snapshot_status(self.admin_snapshots_client, + snap['id'], 'available') + self.consistencygroups_adm_client.wait_for_cgsnapshot_status( + cgsnapshot['id'], 'available') + self.assertEqual(cgsnapshot_name, cgsnapshot['name']) + + # Get a given CG snapshot + cgsnapshot = self.consistencygroups_adm_client.show_cgsnapshot( + cgsnapshot['id'])['cgsnapshot'] + self.assertEqual(cgsnapshot_name, cgsnapshot['name']) + + # Get all CG snapshots with detail + cgsnapshots = self.consistencygroups_adm_client.list_cgsnapshots( + detail=True)['cgsnapshots'] + self.assertIn((cgsnapshot['name'], cgsnapshot['id']), + [(m['name'], m['id']) for m in cgsnapshots]) + + # Clean up + self._delete_cgsnapshot(cgsnapshot['id'], cg['id']) + self._delete_consistencygroup(cg['id']) + self.admin_volume_types_client.delete_volume_type(volume_type['id']) + + @test.idempotent_id('3a6a5525-25ca-4a6c-aac4-cac6fa8f5b43') + def test_create_consistencygroup_from_cgsnapshot(self): + # Create volume type + name = data_utils.rand_name("volume-type") + volume_type = self.admin_volume_types_client.create_volume_type( + name=name)['volume_type'] + + # Create CG + cg_name = data_utils.rand_name('CG') + create_consistencygroup = ( + self.consistencygroups_adm_client.create_consistencygroup) + cg = create_consistencygroup(volume_type['id'], + name=cg_name)['consistencygroup'] + vol_name = data_utils.rand_name("volume") + self.name_field = self.special_fields['name_field'] + params = {self.name_field: vol_name, + 'volume_type': volume_type['id'], + 'consistencygroup_id': cg['id']} + + # Create volume + volume = self.admin_volume_client.create_volume(**params)['volume'] + waiters.wait_for_volume_status(self.admin_volume_client, + volume['id'], 'available') + self.consistencygroups_adm_client.wait_for_consistencygroup_status( + cg['id'], 'available') + self.assertEqual(cg_name, cg['name']) + + # Create cgsnapshot + cgsnapshot_name = data_utils.rand_name('cgsnapshot') + create_cgsnapshot = ( + self.consistencygroups_adm_client.create_cgsnapshot) + cgsnapshot = create_cgsnapshot(cg['id'], + name=cgsnapshot_name)['cgsnapshot'] + snapshots = self.snapshots_client.list_snapshots( + detail=True)['snapshots'] + for snap in snapshots: + if volume['id'] == snap['volume_id']: + waiters.wait_for_snapshot_status(self.admin_snapshots_client, + snap['id'], 'available') + self.consistencygroups_adm_client.wait_for_cgsnapshot_status( + cgsnapshot['id'], 'available') + self.assertEqual(cgsnapshot_name, cgsnapshot['name']) + + # Create CG from CG snapshot + cg_name2 = data_utils.rand_name('CG_from_snap') + create_consistencygroup2 = ( + self.consistencygroups_adm_client.create_consistencygroup_from_src) + cg2 = create_consistencygroup2(cgsnapshot_id=cgsnapshot['id'], + name=cg_name2)['consistencygroup'] + vols = self.admin_volume_client.list_volumes( + detail=True)['volumes'] + for vol in vols: + if vol['consistencygroup_id'] == cg2['id']: + waiters.wait_for_volume_status(self.admin_volume_client, + vol['id'], 'available') + self.consistencygroups_adm_client.wait_for_consistencygroup_status( + cg2['id'], 'available') + self.assertEqual(cg_name2, cg2['name']) + + # Clean up + self._delete_consistencygroup(cg2['id']) + self._delete_cgsnapshot(cgsnapshot['id'], cg['id']) + self._delete_consistencygroup(cg['id']) + self.admin_volume_types_client.delete_volume_type(volume_type['id']) + + @test.idempotent_id('556121ae-de9c-4342-9897-e54260447a19') + def test_create_consistencygroup_from_consistencygroup(self): + # Create volume type + name = data_utils.rand_name("volume-type") + volume_type = self.admin_volume_types_client.create_volume_type( + name=name)['volume_type'] + + # Create CG + cg_name = data_utils.rand_name('CG') + create_consistencygroup = ( + self.consistencygroups_adm_client.create_consistencygroup) + cg = create_consistencygroup(volume_type['id'], + name=cg_name)['consistencygroup'] + vol_name = data_utils.rand_name("volume") + self.name_field = self.special_fields['name_field'] + params = {self.name_field: vol_name, + 'volume_type': volume_type['id'], + 'consistencygroup_id': cg['id']} + + # Create volume + volume = self.admin_volume_client.create_volume(**params)['volume'] + waiters.wait_for_volume_status(self.admin_volume_client, + volume['id'], 'available') + self.consistencygroups_adm_client.wait_for_consistencygroup_status( + cg['id'], 'available') + self.assertEqual(cg_name, cg['name']) + + # Create CG from CG + cg_name2 = data_utils.rand_name('CG_from_cg') + create_consistencygroup2 = ( + self.consistencygroups_adm_client.create_consistencygroup_from_src) + cg2 = create_consistencygroup2(source_cgid=cg['id'], + name=cg_name2)['consistencygroup'] + vols = self.admin_volume_client.list_volumes( + detail=True)['volumes'] + for vol in vols: + if vol['consistencygroup_id'] == cg2['id']: + waiters.wait_for_volume_status(self.admin_volume_client, + vol['id'], 'available') + self.consistencygroups_adm_client.wait_for_consistencygroup_status( + cg2['id'], 'available') + self.assertEqual(cg_name2, cg2['name']) + + # Clean up + self._delete_consistencygroup(cg2['id']) + self._delete_consistencygroup(cg['id']) + self.admin_volume_types_client.delete_volume_type(volume_type['id']) diff --git a/cinder/tests/tempest/cinder_clients.py b/cinder/tests/tempest/cinder_clients.py new file mode 100644 index 0000000..8f829ef --- /dev/null +++ b/cinder/tests/tempest/cinder_clients.py @@ -0,0 +1,37 @@ +# Copyright (c) 2016 Pure Storage, Inc. +# 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 tempest import config + +from cinder.tests.tempest.services import consistencygroups_client + +CONF = config.CONF + + +class Manager(object): + def __init__(self, base_manager): + params = { + 'service': CONF.volume.catalog_type, + 'region': CONF.volume.region or CONF.identity.region, + 'endpoint_type': CONF.volume.endpoint_type, + 'build_interval': CONF.volume.build_interval, + 'build_timeout': CONF.volume.build_timeout + } + params.update(base_manager.default_params) + auth_provider = base_manager.auth_provider + + self.consistencygroups_adm_client = ( + consistencygroups_client.ConsistencyGroupsClient(auth_provider, + **params)) diff --git a/cinder/tests/tempest/config.py b/cinder/tests/tempest/config.py index 72fd941..d1a2db7 100644 --- a/cinder/tests/tempest/config.py +++ b/cinder/tests/tempest/config.py @@ -24,3 +24,14 @@ ServiceAvailableGroup = [ default=True, help="Whether or not cinder is expected to be available"), ] + +# Use a new config group specific to the cinder in-tree tests to avoid +# any naming confusion with the upstream tempest config options. +cinder_group = cfg.OptGroup(name='cinder', + title='Cinder Tempest Config Options') + +CinderGroup = [ + cfg.BoolOpt('consistency_group', + default=False, + help='Enable to run Cinder volume consistency group tests'), +] diff --git a/cinder/tests/tempest/plugin.py b/cinder/tests/tempest/plugin.py index 7760fb9..ed7a912 100644 --- a/cinder/tests/tempest/plugin.py +++ b/cinder/tests/tempest/plugin.py @@ -17,6 +17,7 @@ import cinder import os from cinder.tests.tempest import config as project_config + from tempest import config from tempest.test_discover import plugins @@ -33,6 +34,15 @@ class CinderTempestPlugin(plugins.TempestPlugin): config.register_opt_group( conf, project_config.service_available_group, project_config.ServiceAvailableGroup) + config.register_opt_group( + conf, project_config.cinder_group, + project_config.CinderGroup + ) def get_opt_lists(self): - pass + return [ + (project_config.service_available_group.name, + project_config.ServiceAvailableGroup), + (project_config.cinder_group.name, + project_config.CinderGroup), + ] diff --git a/cinder/tests/tempest/services/consistencygroups_client.py b/cinder/tests/tempest/services/consistencygroups_client.py new file mode 100644 index 0000000..28853ec --- /dev/null +++ b/cinder/tests/tempest/services/consistencygroups_client.py @@ -0,0 +1,192 @@ +# Copyright (C) 2015 EMC Corporation. +# Copyright (C) 2016 Pure Storage, Inc. +# 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. + +import time + +from oslo_serialization import jsonutils as json +from tempest import exceptions +from tempest.lib.common import rest_client +from tempest.lib import exceptions as lib_exc + + +class ConsistencyGroupsClient(rest_client.RestClient): + """Client class to send CRUD Volume ConsistencyGroup API requests""" + + def __init__(self, auth_provider, service, region, **kwargs): + super(ConsistencyGroupsClient, self).__init__( + auth_provider, service, region, **kwargs) + + def create_consistencygroup(self, volume_types, **kwargs): + """Creates a consistency group.""" + post_body = {'volume_types': volume_types} + if kwargs.get('availability_zone'): + post_body['availability_zone'] = kwargs.get('availability_zone') + if kwargs.get('name'): + post_body['name'] = kwargs.get('name') + if kwargs.get('description'): + post_body['description'] = kwargs.get('description') + post_body = json.dumps({'consistencygroup': post_body}) + resp, body = self.post('consistencygroups', post_body) + body = json.loads(body) + self.expected_success(202, resp.status) + return rest_client.ResponseBody(resp, body) + + def create_consistencygroup_from_src(self, **kwargs): + """Creates a consistency group from source.""" + post_body = {} + if kwargs.get('cgsnapshot_id'): + post_body['cgsnapshot_id'] = kwargs.get('cgsnapshot_id') + if kwargs.get('source_cgid'): + post_body['source_cgid'] = kwargs.get('source_cgid') + if kwargs.get('name'): + post_body['name'] = kwargs.get('name') + if kwargs.get('description'): + post_body['description'] = kwargs.get('description') + post_body = json.dumps({'consistencygroup-from-src': post_body}) + resp, body = self.post('consistencygroups/create_from_src', post_body) + body = json.loads(body) + self.expected_success(202, resp.status) + return rest_client.ResponseBody(resp, body) + + def delete_consistencygroup(self, cg_id): + """Delete a consistency group.""" + post_body = {'force': True} + post_body = json.dumps({'consistencygroup': post_body}) + resp, body = self.post('consistencygroups/%s/delete' % cg_id, + post_body) + self.expected_success(202, resp.status) + return rest_client.ResponseBody(resp, body) + + def show_consistencygroup(self, cg_id): + """Returns the details of a single consistency group.""" + url = "consistencygroups/%s" % str(cg_id) + resp, body = self.get(url) + body = json.loads(body) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, body) + + def list_consistencygroups(self, detail=False): + """Information for all the tenant's consistency groups.""" + url = "consistencygroups" + if detail: + url += "/detail" + resp, body = self.get(url) + body = json.loads(body) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, body) + + def create_cgsnapshot(self, consistencygroup_id, **kwargs): + """Creates a consistency group snapshot.""" + post_body = {'consistencygroup_id': consistencygroup_id} + if kwargs.get('name'): + post_body['name'] = kwargs.get('name') + if kwargs.get('description'): + post_body['description'] = kwargs.get('description') + post_body = json.dumps({'cgsnapshot': post_body}) + resp, body = self.post('cgsnapshots', post_body) + body = json.loads(body) + self.expected_success(202, resp.status) + return rest_client.ResponseBody(resp, body) + + def delete_cgsnapshot(self, cgsnapshot_id): + """Delete a consistency group snapshot.""" + resp, body = self.delete('cgsnapshots/%s' % (str(cgsnapshot_id))) + self.expected_success(202, resp.status) + return rest_client.ResponseBody(resp, body) + + def show_cgsnapshot(self, cgsnapshot_id): + """Returns the details of a single consistency group snapshot.""" + url = "cgsnapshots/%s" % str(cgsnapshot_id) + resp, body = self.get(url) + body = json.loads(body) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, body) + + def list_cgsnapshots(self, detail=False): + """Information for all the tenant's consistency group snapshotss.""" + url = "cgsnapshots" + if detail: + url += "/detail" + resp, body = self.get(url) + body = json.loads(body) + self.expected_success(200, resp.status) + return rest_client.ResponseBody(resp, body) + + def wait_for_consistencygroup_status(self, cg_id, status): + """Waits for a consistency group to reach a given status.""" + body = self.show_consistencygroup(cg_id)['consistencygroup'] + cg_status = body['status'] + start = int(time.time()) + + while cg_status != status: + time.sleep(self.build_interval) + body = self.show_consistencygroup(cg_id)['consistencygroup'] + cg_status = body['status'] + if cg_status == 'error': + raise exceptions.ConsistencyGroupException(cg_id=cg_id) + + if int(time.time()) - start >= self.build_timeout: + message = ('Consistency group %s failed to reach %s status ' + '(current %s) within the required time (%s s).' % + (cg_id, status, cg_status, + self.build_timeout)) + raise exceptions.TimeoutException(message) + + def wait_for_consistencygroup_deletion(self, cg_id): + """Waits for consistency group deletion""" + start_time = int(time.time()) + while True: + try: + self.show_consistencygroup(cg_id) + except lib_exc.NotFound: + return + if int(time.time()) - start_time >= self.build_timeout: + raise exceptions.TimeoutException + time.sleep(self.build_interval) + + def wait_for_cgsnapshot_status(self, cgsnapshot_id, status): + """Waits for a consistency group snapshot to reach a given status.""" + body = self.show_cgsnapshot(cgsnapshot_id)['cgsnapshot'] + cgsnapshot_status = body['status'] + start = int(time.time()) + + while cgsnapshot_status != status: + time.sleep(self.build_interval) + body = self.show_cgsnapshot(cgsnapshot_id)['cgsnapshot'] + cgsnapshot_status = body['status'] + if cgsnapshot_status == 'error': + raise exceptions.ConsistencyGroupSnapshotException( + cgsnapshot_id=cgsnapshot_id) + + if int(time.time()) - start >= self.build_timeout: + message = ('Consistency group snapshot %s failed to reach ' + '%s status (current %s) within the required time ' + '(%s s).' % + (cgsnapshot_id, status, cgsnapshot_status, + self.build_timeout)) + raise exceptions.TimeoutException(message) + + def wait_for_cgsnapshot_deletion(self, cgsnapshot_id): + """Waits for consistency group snapshot deletion""" + start_time = int(time.time()) + while True: + try: + self.show_cgsnapshot(cgsnapshot_id) + except lib_exc.NotFound: + return + if int(time.time()) - start_time >= self.build_timeout: + raise exceptions.TimeoutException + time.sleep(self.build_interval)