diff --git a/contrib/ci/post_test_hook.sh b/contrib/ci/post_test_hook.sh index 33402383fa..2d16061591 100755 --- a/contrib/ci/post_test_hook.sh +++ b/contrib/ci/post_test_hook.sh @@ -41,6 +41,10 @@ iniset $BASE/new/tempest/etc/tempest.conf share share_creation_retry_number 2 SUPPRESS_ERRORS=${SUPPRESS_ERRORS_IN_CLEANUP:-True} iniset $BASE/new/tempest/etc/tempest.conf share suppress_errors_in_cleanup $SUPPRESS_ERRORS +# Enable consistency group tests +RUN_MANILA_CG_TESTS=${RUN_MANILA_CG_TESTS:-True} +iniset $BASE/new/tempest/etc/tempest.conf share run_consistency_group_tests $RUN_MANILA_CG_TESTS + # Enable manage/unmanage tests RUN_MANILA_MANAGE_TESTS=${RUN_MANILA_MANAGE_TESTS:-True} iniset $BASE/new/tempest/etc/tempest.conf share run_manage_unmanage_tests $RUN_MANILA_MANAGE_TESTS diff --git a/contrib/ci/pre_test_hook.sh b/contrib/ci/pre_test_hook.sh index 4b235cb350..a1ba34ff24 100755 --- a/contrib/ci/pre_test_hook.sh +++ b/contrib/ci/pre_test_hook.sh @@ -25,6 +25,7 @@ echo "DEVSTACK_GATE_TEMPEST_ALLOW_TENANT_ISOLATION=1" >> $localrc_path echo "API_RATE_LIMIT=False" >> $localrc_path echo "TEMPEST_SERVICES+=,manila" >> $localrc_path echo "VOLUME_BACKING_FILE_SIZE=22G" >> $localrc_path +echo "CINDER_LVM_TYPE=thin" >> $localrc_path echo "MANILA_BACKEND1_CONFIG_GROUP_NAME=london" >> $localrc_path echo "MANILA_BACKEND2_CONFIG_GROUP_NAME=paris" >> $localrc_path diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py index 2c5e809004..046f8cdb2c 100644 --- a/manila_tempest_tests/config.py +++ b/manila_tempest_tests/config.py @@ -139,6 +139,11 @@ ShareGroup = [ help="Defines whether to run tests that use share snapshots " "or not. Disable this feature if used driver doesn't " "support it."), + cfg.BoolOpt("run_consistency_group_tests", + default=True, + help="Defines whether to run consistency group tests or not. " + "Disable this feature if used driver doesn't support " + "it."), cfg.StrOpt("image_with_share_tools", default="manila-service-image", help="Image name for vm booting with nfs/smb clients tool."), diff --git a/manila_tempest_tests/services/share/json/shares_client.py b/manila_tempest_tests/services/share/json/shares_client.py index 5ce561e8e1..cf58d8cd05 100644 --- a/manila_tempest_tests/services/share/json/shares_client.py +++ b/manila_tempest_tests/services/share/json/shares_client.py @@ -26,6 +26,13 @@ from tempest_lib import exceptions from manila_tempest_tests import share_exceptions CONF = config.CONF +LATEST_MICRO_API = { + 'X-OpenStack-Manila-API-Version': CONF.share.max_api_microversion, +} +EXPERIMENTAL = { + 'X-OpenStack-Manila-API-Experimental': 'True', + 'X-OpenStack-Manila-API-Version': CONF.share.max_api_microversion, +} class SharesClient(rest_client.RestClient): @@ -86,7 +93,8 @@ class SharesClient(rest_client.RestClient): def create_share(self, share_protocol=None, size=1, name=None, snapshot_id=None, description=None, metadata=None, share_network_id=None, - share_type_id=None, is_public=False): + share_type_id=None, is_public=False, + consistency_group_id=None): metadata = metadata or {} if name is None: name = data_utils.rand_name("tempest-created-share") @@ -111,13 +119,18 @@ class SharesClient(rest_client.RestClient): post_body["share"]["share_network_id"] = share_network_id if share_type_id: post_body["share"]["share_type"] = share_type_id + if consistency_group_id: + post_body["share"]["consistency_group_id"] = consistency_group_id body = json.dumps(post_body) - resp, body = self.post("shares", body) + resp, body = self.post("shares", body, headers=LATEST_MICRO_API, + extra_headers=True) self.expected_success(200, resp.status) return self._parse_resp(body) - def delete_share(self, share_id): - resp, body = self.delete("shares/%s" % share_id) + def delete_share(self, share_id, params=None): + uri = "shares/%s" % share_id + uri += '?%s' % (urllib.urlencode(params) if params else '') + resp, body = self.delete(uri) self.expected_success(202, resp.status) return body @@ -148,7 +161,8 @@ class SharesClient(rest_client.RestClient): """Get list of shares w/o filters.""" uri = 'shares/detail' if detailed else 'shares' uri += '?%s' % urllib.urlencode(params) if params else '' - resp, body = self.get(uri) + resp, body = self.get(uri, headers=LATEST_MICRO_API, + extra_headers=True) self.expected_success(200, resp.status) return self._parse_resp(body) @@ -157,7 +171,8 @@ class SharesClient(rest_client.RestClient): return self.list_shares(detailed=True, params=params) def get_share(self, share_id): - resp, body = self.get("shares/%s" % share_id) + resp, body = self.get("shares/%s" % share_id, headers=LATEST_MICRO_API, + extra_headers=True) self.expected_success(200, resp.status) return self._parse_resp(body) @@ -367,6 +382,49 @@ class SharesClient(rest_client.RestClient): (snapshot_name, status, self.build_timeout)) raise exceptions.TimeoutException(message) + def wait_for_consistency_group_status(self, consistency_group_id, status): + """Waits for a consistency group to reach a given status.""" + body = self.get_consistency_group(consistency_group_id) + consistency_group_name = body['name'] + consistency_group_status = body['status'] + start = int(time.time()) + + while consistency_group_status != status: + time.sleep(self.build_interval) + body = self.get_consistency_group(consistency_group_id) + consistency_group_status = body['status'] + if 'error' in consistency_group_status and status != 'error': + raise share_exceptions.ConsistencyGroupBuildErrorException( + consistency_group_id=consistency_group_id) + + if int(time.time()) - start >= self.build_timeout: + message = ('Consistency Group %s failed to reach %s status ' + 'within the required time (%s s).' % + (consistency_group_name, status, + self.build_timeout)) + raise exceptions.TimeoutException(message) + + def wait_for_cgsnapshot_status(self, cgsnapshot_id, status): + """Waits for a cgsnapshot to reach a given status.""" + body = self.get_cgsnapshot(cgsnapshot_id) + cgsnapshot_name = body['name'] + cgsnapshot_status = body['status'] + start = int(time.time()) + + while cgsnapshot_status != status: + time.sleep(self.build_interval) + body = self.get_cgsnapshot(cgsnapshot_id) + cgsnapshot_status = body['status'] + if 'error' in cgsnapshot_status and status != 'error': + raise share_exceptions.CGSnapshotBuildErrorException( + cgsnapshot_id=cgsnapshot_id) + + if int(time.time()) - start >= self.build_timeout: + message = ('CGSnapshot %s failed to reach %s status ' + 'within the required time (%s s).' % + (cgsnapshot_name, status, self.build_timeout)) + raise exceptions.TimeoutException(message) + def wait_for_access_rule_status(self, share_id, rule_id, status): """Waits for an access rule to reach a given status.""" rule_status = "new" @@ -444,7 +502,8 @@ class SharesClient(rest_client.RestClient): """Verifies whether provided resource deleted or not. :param kwargs: dict with expected keys 'share_id', 'snapshot_id', - :param kwargs: 'sn_id', 'ss_id', 'vt_id' and 'server_id' + :param kwargs: 'sn_id', 'ss_id', 'vt_id', 'server_id', 'cg_id', + :param kwargs: and 'cgsnapshot_id' :raises share_exceptions.InvalidResource """ if "share_id" in kwargs: @@ -480,6 +539,12 @@ class SharesClient(rest_client.RestClient): elif "server_id" in kwargs: return self._is_resource_deleted( self.show_share_server, kwargs.get("server_id")) + elif "cg_id" in kwargs: + return self._is_resource_deleted( + self.get_consistency_group, kwargs.get("cg_id")) + elif "cgsnapshot_id" in kwargs: + return self._is_resource_deleted( + self.get_cgsnapshot, kwargs.get("cgsnapshot_id")) else: raise share_exceptions.InvalidResource( message=six.text_type(kwargs)) @@ -489,7 +554,7 @@ class SharesClient(rest_client.RestClient): res = func(res_id) except exceptions.NotFound: return True - if res.get('status') == 'error_deleting': + if res.get('status') in ['error_deleting', 'error']: # Resource has "error_deleting" status and can not be deleted. resource_type = func.__name__.split('_', 1)[-1] raise share_exceptions.ResourceReleaseFailed( @@ -533,26 +598,29 @@ class SharesClient(rest_client.RestClient): self.expected_success(200, resp.status) return self._parse_resp(body) - def reset_state(self, s_id, status="error", s_type="shares"): - """Resets the state of a share or a snapshot. + def reset_state(self, s_id, status="error", s_type="shares", + headers=None): + """Resets the state of a share, snapshot, cg, or a cgsnapshot. status: available, error, creating, deleting, error_deleting - s_type: shares, snapshots + s_type: shares, snapshots, consistency-groups, cgsnapshots """ body = {"os-reset_status": {"status": status}} body = json.dumps(body) - resp, body = self.post("%s/%s/action" % (s_type, s_id), body) + resp, body = self.post("%s/%s/action" % (s_type, s_id), body, + headers=headers, extra_headers=True) self.expected_success(202, resp.status) return body - def force_delete(self, s_id, s_type="shares"): + def force_delete(self, s_id, s_type="shares", headers=None): """Force delete share or snapshot. s_type: shares, snapshots """ body = {"os-force_delete": None} body = json.dumps(body) - resp, body = self.post("%s/%s/action" % (s_type, s_id), body) + resp, body = self.post("%s/%s/action" % (s_type, s_id), body, + headers=headers, extra_headers=True) self.expected_success(202, resp.status) return body @@ -857,3 +925,143 @@ class SharesClient(rest_client.RestClient): resp, body = self.get(uri) self.expected_success(200, resp.status) return json.loads(body) + +############### + + def create_consistency_group(self, name=None, description=None, + share_type_ids=(), share_network_id=None, + source_cgsnapshot_id=None): + """Create a new consistency group.""" + uri = 'consistency-groups' + post_body = {} + if name: + post_body['name'] = name + if description: + post_body['description'] = description + if share_type_ids: + post_body['share_types'] = share_type_ids + if source_cgsnapshot_id: + post_body['source_cgsnapshot_id'] = source_cgsnapshot_id + if share_network_id: + post_body['share_network_id'] = share_network_id + body = json.dumps({'consistency_group': post_body}) + resp, body = self.post(uri, body, headers=EXPERIMENTAL, + extra_headers=True) + self.expected_success(202, resp.status) + return self._parse_resp(body) + + def delete_consistency_group(self, consistency_group_id): + """Delete a consistency group.""" + uri = 'consistency-groups/%s' % consistency_group_id + resp, body = self.delete(uri, headers=EXPERIMENTAL, + extra_headers=True) + self.expected_success(202, resp.status) + return body + + def list_consistency_groups(self, detailed=False, params=None): + """Get list of consistency groups w/o filters.""" + uri = 'consistency-groups%s' % ('/detail' if detailed else '') + uri += '?%s' % (urllib.urlencode(params) if params else '') + resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def get_consistency_group(self, consistency_group_id): + """Get consistency group info.""" + uri = 'consistency-groups/%s' % consistency_group_id + resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def update_consistency_group(self, consistency_group_id, name=None, + description=None, **kwargs): + """Update an existing consistency group.""" + uri = 'consistency-groups/%s' % consistency_group_id + post_body = {} + if name: + post_body['name'] = name + if description: + post_body['description'] = description + if kwargs: + post_body.update(kwargs) + body = json.dumps({'consistency_group': post_body}) + resp, body = self.put(uri, body, headers=EXPERIMENTAL, + extra_headers=True) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def consistency_group_reset_state(self, id, status): + self.reset_state(id, status=status, + s_type='consistency-groups', headers=EXPERIMENTAL) + + def consistency_group_force_delete(self, id, status): + self.force_delete(id, status=status, + s_type='consistency-groups', headers=EXPERIMENTAL) + +############### + + def create_cgsnapshot(self, consistency_group_id, + name=None, description=None): + """Create a new cgsnapshot of an existing consistency group.""" + uri = 'cgsnapshots' + post_body = {'consistency_group_id': consistency_group_id} + if name: + post_body['name'] = name + if description: + post_body['description'] = description + body = json.dumps({'cgsnapshot': post_body}) + resp, body = self.post(uri, body, headers=EXPERIMENTAL, + extra_headers=True) + self.expected_success(202, resp.status) + return self._parse_resp(body) + + def delete_cgsnapshot(self, cgsnapshot_id): + """Delete an existing cgsnapshot.""" + uri = 'cgsnapshots/%s' % cgsnapshot_id + resp, body = self.delete(uri, headers=EXPERIMENTAL, extra_headers=True) + self.expected_success(202, resp.status) + return body + + def list_cgsnapshots(self, detailed=False, params=None): + """Get list of cgsnapshots w/o filters.""" + uri = 'cgsnapshots/detail' if detailed else 'cgsnapshots' + uri += '?%s' % (urllib.urlencode(params) if params else '') + resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def list_cgsnapshot_members(self, cgsnapshot_id): + """Get list of members of a cgsnapshots.""" + uri = 'cgsnapshots/%s/members' % cgsnapshot_id + resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def get_cgsnapshot(self, cgsnapshot_id): + """Get cgsnapshot info.""" + uri = 'cgsnapshots/%s' % cgsnapshot_id + resp, body = self.get(uri, headers=EXPERIMENTAL, extra_headers=True) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def update_cgsnapshot(self, cgsnapshot_id, name=None, description=None): + """Update an existing cgsnapshot.""" + uri = 'cgsnapshots/%s' % cgsnapshot_id + post_body = {} + if name: + post_body['name'] = name + if description: + post_body['description'] = description + body = json.dumps({'cgsnapshot': post_body}) + resp, body = self.put(uri, body, headers=EXPERIMENTAL, + extra_headers=True) + self.expected_success(200, resp.status) + return self._parse_resp(body) + + def cgsnapshot_reset_state(self, id, status): + self.reset_state(id, status=status, + s_type='cgsnapshots', headers=EXPERIMENTAL) + + def cgsnapshot_force_delete(self, id, status): + self.force_delete(id, status=status, + s_type='cgsnapshots', headers=EXPERIMENTAL) diff --git a/manila_tempest_tests/share_exceptions.py b/manila_tempest_tests/share_exceptions.py index 33478cd159..aa688e4383 100644 --- a/manila_tempest_tests/share_exceptions.py +++ b/manila_tempest_tests/share_exceptions.py @@ -24,6 +24,11 @@ class ShareInstanceBuildErrorException(exceptions.TempestException): message = "Share instance %(id)s failed to build and is in ERROR status" +class ConsistencyGroupBuildErrorException(exceptions.TempestException): + message = ("Consistency group %(consistency_group_id)s failed to build " + "and is in ERROR status") + + class AccessRuleBuildErrorException(exceptions.TempestException): message = "Share's rule with id %(rule_id)s is in ERROR status" @@ -32,6 +37,11 @@ class SnapshotBuildErrorException(exceptions.TempestException): message = "Snapshot %(snapshot_id)s failed to build and is in ERROR status" +class CGSnapshotBuildErrorException(exceptions.TempestException): + message = ("CGSnapshot %(cgsnapshot_id)s failed to build and is in ERROR " + "status") + + class ShareProtocolNotSpecified(exceptions.TempestException): message = "Share can not be created, share protocol is not specified" diff --git a/manila_tempest_tests/tests/api/admin/test_consistency_group_actions.py b/manila_tempest_tests/tests/api/admin/test_consistency_group_actions.py new file mode 100644 index 0000000000..c1fdb15c3a --- /dev/null +++ b/manila_tempest_tests/tests/api/admin/test_consistency_group_actions.py @@ -0,0 +1,118 @@ +# Copyright 2015 Andrew Kerr +# 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 tempest import test +from tempest_lib.common.utils import data_utils +import testtools + +from manila_tempest_tests.tests.api import base + +CONF = config.CONF + + +@testtools.skipUnless(CONF.share.run_consistency_group_tests, + 'Consistency Group tests disabled.') +class ConsistencyGroupActionsTest(base.BaseSharesAdminTest): + + @classmethod + def resource_setup(cls): + super(ConsistencyGroupActionsTest, cls).resource_setup() + # Create 2 share_types + name = data_utils.rand_name("tempest-manila") + extra_specs = cls.add_required_extra_specs_to_dict() + share_type = cls.create_share_type(name, extra_specs=extra_specs) + cls.share_type = share_type['share_type'] + + name = data_utils.rand_name("tempest-manila") + share_type = cls.create_share_type(name, extra_specs=extra_specs) + cls.share_type2 = share_type['share_type'] + + # Create a consistency group + cls.consistency_group = cls.create_consistency_group( + share_type_ids=[cls.share_type['id'], cls.share_type2['id']]) + + @test.attr(type=["gate", ]) + def test_create_cg_from_cgsnapshot_with_multiple_share_types(self): + # Create cgsnapshot + cgsnapshot = self.create_cgsnapshot_wait_for_active( + self.consistency_group["id"], cleanup_in_class=False) + + new_consistency_group = self.create_consistency_group( + cleanup_in_class=False, source_cgsnapshot_id=cgsnapshot['id']) + + # Verify share_types are the same + expected_types = sorted(self.consistency_group['share_types']) + actual_types = sorted(new_consistency_group['share_types']) + self.assertEqual(expected_types, actual_types, + 'Expected share types of %s, but got %s.' % ( + expected_types, actual_types)) + + @test.attr(type=["gate", ]) + def test_create_cg_from_multi_typed_populated_cgsnapshot(self): + share_name = data_utils.rand_name("tempest-share-name") + share_desc = data_utils.rand_name("tempest-share-description") + share_size = 1 + share = self.create_share( + cleanup_in_class=False, + name=share_name, + description=share_desc, + size=share_size, + consistency_group_id=self.consistency_group['id'], + share_type_id=self.share_type['id'] + ) + + share_name2 = data_utils.rand_name("tempest-share-name") + share_desc2 = data_utils.rand_name("tempest-share-description") + share_size2 = 1 + share2 = self.create_share( + cleanup_in_class=False, + name=share_name2, + description=share_desc2, + size=share_size2, + consistency_group_id=self.consistency_group['id'], + share_type_id=self.share_type2['id'] + ) + + cg_shares = self.shares_client.list_shares(detailed=True, params={ + 'consistency_group_id': self.consistency_group['id']}) + + cg_share_ids = [s['id'] for s in cg_shares] + for share_id in [share['id'], share2['id']]: + self.assertIn(share_id, cg_share_ids, 'Share %s not in ' + 'consistency group %s.' % + (share_id, self.consistency_group['id'])) + + cgsnap_name = data_utils.rand_name("tempest-cgsnap-name") + cgsnap_desc = data_utils.rand_name("tempest-cgsnap-description") + cgsnapshot = self.create_cgsnapshot_wait_for_active( + self.consistency_group["id"], + name=cgsnap_name, + description=cgsnap_desc, + cleanup_in_class=False) + + self.create_consistency_group( + cleanup_in_class=False, source_cgsnapshot_id=cgsnapshot['id']) + + # TODO(akerr): Skip until bug 1483886 is resolved + # Verify that the new shares correspond to correct share types + # expected_share_types = [self.share_type['id'], self.share_type2[ + # 'id']] + # actual_share_types = [s['share_type'] for s in new_cg_shares] + # self.assertEqual(sorted(expected_share_types), + # sorted(actual_share_types), + # 'Expected shares of types %s, got %s.' % ( + # sorted(expected_share_types), + # sorted(actual_share_types))) diff --git a/manila_tempest_tests/tests/api/admin/test_consistency_groups.py b/manila_tempest_tests/tests/api/admin/test_consistency_groups.py new file mode 100644 index 0000000000..4fbe7f9a27 --- /dev/null +++ b/manila_tempest_tests/tests/api/admin/test_consistency_groups.py @@ -0,0 +1,66 @@ +# Copyright 2015 Andrew Kerr +# 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 tempest import test +from tempest_lib.common.utils import data_utils +import testtools + +from manila_tempest_tests.tests.api import base + +CONF = config.CONF +CG_REQUIRED_ELEMENTS = {"id", "name", "description", "created_at", "status", + "share_types", "project_id", "host", "links"} + + +@testtools.skipUnless(CONF.share.run_consistency_group_tests, + 'Consistency Group tests disabled.') +class ConsistencyGroupsTest(base.BaseSharesAdminTest): + + @classmethod + def resource_setup(cls): + super(ConsistencyGroupsTest, cls).resource_setup() + # Create 2 share_types + name = data_utils.rand_name("tempest-manila") + extra_specs = cls.add_required_extra_specs_to_dict() + share_type = cls.create_share_type(name, extra_specs=extra_specs) + cls.share_type = share_type['share_type'] + + name = data_utils.rand_name("tempest-manila") + share_type = cls.create_share_type(name, extra_specs=extra_specs) + cls.share_type2 = share_type['share_type'] + + @test.attr(type=["gate", ]) + def test_create_cg_with_multiple_share_types(self): + # Create a consistency group + consistency_group = self.create_consistency_group( + cleanup_in_class=False, share_type_ids=[self.share_type['id'], + self.share_type2['id']]) + + self.assertTrue(CG_REQUIRED_ELEMENTS.issubset( + consistency_group.keys()), + 'At least one expected element missing from consistency group ' + 'response. Expected %(expected)s, got %(actual)s.' % { + "expected": CG_REQUIRED_ELEMENTS, + "actual": consistency_group.keys()}) + + actual_share_types = consistency_group['share_types'] + expected_share_types = [self.share_type['id'], self.share_type2['id']] + self.assertEqual(sorted(expected_share_types), + sorted(actual_share_types), + 'Incorrect share types applied to consistency group ' + '%s. Expected %s, got %s' % (consistency_group['id'], + expected_share_types, + actual_share_types)) diff --git a/manila_tempest_tests/tests/api/admin/test_consistency_groups_negative.py b/manila_tempest_tests/tests/api/admin/test_consistency_groups_negative.py new file mode 100644 index 0000000000..a7a715f3d8 --- /dev/null +++ b/manila_tempest_tests/tests/api/admin/test_consistency_groups_negative.py @@ -0,0 +1,270 @@ +# Copyright 2015 Andrew Kerr +# 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 tempest import test +from tempest_lib.common.utils import data_utils +from tempest_lib import exceptions +import testtools + +from manila_tempest_tests.tests.api import base + +CONF = config.CONF + + +@testtools.skipUnless(CONF.share.run_consistency_group_tests, + 'Consistency Group tests disabled.') +class ConsistencyGroupsNegativeTest(base.BaseSharesAdminTest): + + @classmethod + def resource_setup(cls): + super(ConsistencyGroupsNegativeTest, cls).resource_setup() + # Create share_type + name = data_utils.rand_name("tempest-manila") + extra_specs = cls.add_required_extra_specs_to_dict() + share_type = cls.create_share_type(name, extra_specs=extra_specs) + cls.share_type = share_type['share_type'] + + # Create a consistency group + cls.consistency_group = cls.create_consistency_group( + share_type_ids=[cls.share_type['id']]) + + # Create share inside consistency group + cls.share_name = data_utils.rand_name("tempest-share-name") + cls.share_desc = data_utils.rand_name("tempest-share-description") + cls.share_size = 1 + cls.share = cls.create_share( + name=cls.share_name, + description=cls.share_desc, + size=cls.share_size, + consistency_group_id=cls.consistency_group['id'], + share_type_id=cls.share_type['id'], + ) + + # Create a cgsnapshot of the consistency group + cls.cgsnap_name = data_utils.rand_name("tempest-cgsnap-name") + cls.cgsnap_desc = data_utils.rand_name("tempest-cgsnap-description") + cls.cgsnapshot = cls.create_cgsnapshot_wait_for_active( + cls.consistency_group["id"], + name=cls.cgsnap_name, + description=cls.cgsnap_desc) + + @test.attr(type=["negative", "gate", ]) + def test_delete_share_type_in_use_by_cg(self): + # Attempt delete of share type + self.assertRaises(exceptions.BadRequest, + self.shares_client.delete_share_type, + self.share_type['id']) + + @test.attr(type=["negative", "gate", ]) + def test_create_share_of_unsupported_type_in_cg(self): + # Attempt to create share of default type in the cg + self.assertRaises(exceptions.BadRequest, + self.shares_client.create_share, size=1, + consistency_group_id=self.consistency_group['id']) + + @test.attr(type=["negative", "gate", ]) + def test_create_share_in_cg_that_is_not_available(self): + consistency_group = self.create_consistency_group( + cleanup_in_class=False) + self.addCleanup(self.shares_client.consistency_group_reset_state, + consistency_group['id'], + status='available') + # creating + self.shares_client.consistency_group_reset_state( + consistency_group['id'], status='creating') + self.shares_client.wait_for_consistency_group_status( + consistency_group['id'], 'creating') + self.assertRaises(exceptions.BadRequest, self.create_share, + name=self.share_name, + description=self.share_desc, + size=self.share_size, + consistency_group_id=consistency_group['id'], + cleanup_in_class=False) + # deleting + self.shares_client.consistency_group_reset_state( + consistency_group['id'], status='deleting') + self.shares_client.wait_for_consistency_group_status( + consistency_group['id'], 'deleting') + self.assertRaises(exceptions.BadRequest, self.create_share, + name=self.share_name, + description=self.share_desc, + size=self.share_size, + consistency_group_id=consistency_group['id'], + cleanup_in_class=False) + # error + self.shares_client.consistency_group_reset_state( + consistency_group['id'], status='error') + self.shares_client.wait_for_consistency_group_status( + consistency_group['id'], 'error') + self.assertRaises(exceptions.BadRequest, self.create_share, + name=self.share_name, + description=self.share_desc, + size=self.share_size, + consistency_group_id=consistency_group['id'], + cleanup_in_class=False) + + @test.attr(type=["negative", "gate", ]) + def test_create_cgsnapshot_of_cg_that_is_not_available(self): + consistency_group = self.create_consistency_group( + cleanup_in_class=False) + self.addCleanup(self.shares_client.consistency_group_reset_state, + consistency_group['id'], + status='available') + # creating + self.shares_client.consistency_group_reset_state( + consistency_group['id'], status='creating') + self.shares_client.wait_for_consistency_group_status( + consistency_group['id'], 'creating') + self.assertRaises(exceptions.Conflict, + self.create_cgsnapshot_wait_for_active, + consistency_group['id'], + cleanup_in_class=False) + # deleting + self.shares_client.consistency_group_reset_state( + consistency_group['id'], status='deleting') + self.shares_client.wait_for_consistency_group_status( + consistency_group['id'], 'deleting') + self.assertRaises(exceptions.Conflict, + self.create_cgsnapshot_wait_for_active, + consistency_group['id'], + cleanup_in_class=False) + # error + self.shares_client.consistency_group_reset_state( + consistency_group['id'], status='error') + self.shares_client.wait_for_consistency_group_status( + consistency_group['id'], 'error') + self.assertRaises(exceptions.Conflict, + self.create_cgsnapshot_wait_for_active, + consistency_group['id'], + cleanup_in_class=False) + + @test.attr(type=["negative", "gate", ]) + def test_create_cgsnapshot_of_cg_with_share_in_error_state(self): + consistency_group = self.create_consistency_group() + share_name = data_utils.rand_name("tempest-share-name") + share_desc = data_utils.rand_name("tempest-share-description") + share_size = 1 + share = self.create_share( + name=share_name, + description=share_desc, + size=share_size, + consistency_group_id=consistency_group['id'], + cleanup_in_class=False, + ) + self.shares_client.reset_state(s_id=share['id']) + self.shares_client.wait_for_share_status(share['id'], 'error') + self.assertRaises(exceptions.Conflict, + self.create_cgsnapshot_wait_for_active, + consistency_group['id'], + cleanup_in_class=False) + + @test.attr(type=["negative", "gate", ]) + def test_delete_cgsnapshot_not_in_available_or_error(self): + cgsnapshot = self.create_cgsnapshot_wait_for_active( + self.consistency_group['id'], cleanup_in_class=False) + self.addCleanup(self.shares_client.cgsnapshot_reset_state, + cgsnapshot['id'], + status='available') + + # creating + self.shares_client.cgsnapshot_reset_state(cgsnapshot['id'], + status='creating') + self.shares_client.wait_for_cgsnapshot_status(cgsnapshot['id'], + 'creating') + self.assertRaises(exceptions.Conflict, + self.shares_client.delete_cgsnapshot, + cgsnapshot['id']) + # deleting + self.shares_client.cgsnapshot_reset_state(cgsnapshot['id'], + status='deleting') + self.shares_client.wait_for_cgsnapshot_status(cgsnapshot['id'], + 'deleting') + self.assertRaises(exceptions.Conflict, + self.shares_client.delete_cgsnapshot, + cgsnapshot['id']) + + @test.attr(type=["negative", "gate", ]) + def test_delete_cg_not_in_available_or_error(self): + consistency_group = self.create_consistency_group( + cleanup_in_class=False) + self.addCleanup(self.shares_client.consistency_group_reset_state, + consistency_group['id'], + status='available') + # creating + self.shares_client.consistency_group_reset_state( + consistency_group['id'], status='creating') + self.shares_client.wait_for_consistency_group_status( + consistency_group['id'], 'creating') + self.assertRaises(exceptions.Conflict, + self.shares_client.delete_consistency_group, + consistency_group['id']) + # deleting + self.shares_client.consistency_group_reset_state( + consistency_group['id'], status='deleting') + self.shares_client.wait_for_consistency_group_status( + consistency_group['id'], 'deleting') + self.assertRaises(exceptions.Conflict, + self.shares_client.delete_consistency_group, + consistency_group['id']) + + @test.attr(type=["negative", "gate", ]) + def test_create_cg_with_conflicting_share_types(self): + # Create conflicting share types + name = data_utils.rand_name("tempest-manila") + extra_specs = {"driver_handles_share_servers": False} + share_type = self.create_share_type(name, extra_specs=extra_specs) + single_tenant_share_type = share_type['share_type'] + + name = data_utils.rand_name("tempest-manila") + extra_specs = {"driver_handles_share_servers": True} + share_type = self.create_share_type(name, extra_specs=extra_specs) + multi_tenant_share_type = share_type['share_type'] + + self.assertRaises(exceptions.BadRequest, + self.create_consistency_group, + share_type_ids=[single_tenant_share_type['id'], + multi_tenant_share_type['id']], + cleanup_in_class=False) + + @test.attr(type=["negative", "gate", ]) + def test_create_cg_with_multi_tenant_share_type_and_no_share_network(self): + # Create multi tenant share type + name = data_utils.rand_name("tempest-manila") + extra_specs = {"driver_handles_share_servers": True} + share_type = self.create_share_type(name, extra_specs=extra_specs) + multi_tenant_share_type = share_type['share_type'] + + def create_cg(): + cg = self.shares_client.create_consistency_group( + share_type_ids=[multi_tenant_share_type['id']]) + resource = { + "type": "consistency_group", + "id": cg["id"], + "client": self.shares_client} + self.method_resources.insert(0, resource) + return cg + + self.assertRaises(exceptions.BadRequest, create_cg) + + @test.attr(type=["negative", "gate", ]) + def test_update_cg_share_types(self): + consistency_group = self.create_consistency_group( + cleanup_in_class=False) + + self.assertRaises(exceptions.BadRequest, + self.shares_client.update_consistency_group, + consistency_group['id'], + share_types=[self.share_type['id']]) diff --git a/manila_tempest_tests/tests/api/base.py b/manila_tempest_tests/tests/api/base.py index 6444ccc37a..65e299da25 100644 --- a/manila_tempest_tests/tests/api/base.py +++ b/manila_tempest_tests/tests/api/base.py @@ -280,7 +280,8 @@ class BaseSharesTest(test.BaseTestCase): def _create_share(cls, share_protocol=None, size=1, name=None, snapshot_id=None, description=None, metadata=None, share_network_id=None, share_type_id=None, - client=None, cleanup_in_class=True, is_public=False): + consistency_group_id=None, client=None, + cleanup_in_class=True, is_public=False): client = client or cls.shares_client description = description or "Tempest's share" share_network_id = share_network_id or client.share_network_id or None @@ -296,8 +297,12 @@ class BaseSharesTest(test.BaseTestCase): 'share_type_id': share_type_id, 'is_public': is_public, } + if consistency_group_id: + kwargs['consistency_group_id'] = consistency_group_id + share = client.create_share(**kwargs) - resource = {"type": "share", "id": share["id"], "client": client} + resource = {"type": "share", "id": share["id"], "client": client, + "consistency_group_id": consistency_group_id} cleanup_list = (cls.class_resources if cleanup_in_class else cls.method_resources) cleanup_list.insert(0, resource) @@ -375,6 +380,42 @@ class BaseSharesTest(test.BaseTestCase): return [d["share"] for d in data] + @classmethod + def create_consistency_group(cls, client=None, cleanup_in_class=True, + share_network_id=None, **kwargs): + client = client or cls.shares_client + kwargs['share_network_id'] = (share_network_id or + client.share_network_id or None) + consistency_group = client.create_consistency_group(**kwargs) + resource = { + "type": "consistency_group", + "id": consistency_group["id"], + "client": client} + if cleanup_in_class: + cls.class_resources.insert(0, resource) + else: + cls.method_resources.insert(0, resource) + + if kwargs.get('source_cgsnapshot_id'): + new_cg_shares = client.list_shares( + detailed=True, + params={'consistency_group_id': consistency_group['id']}) + + for share in new_cg_shares: + resource = {"type": "share", + "id": share["id"], + "client": client, + "consistency_group_id": share.get( + 'consistency_group_id')} + if cleanup_in_class: + cls.class_resources.insert(0, resource) + else: + cls.method_resources.insert(0, resource) + + client.wait_for_consistency_group_status(consistency_group['id'], + 'available') + return consistency_group + @classmethod def create_snapshot_wait_for_active(cls, share_id, name=None, description=None, force=False, @@ -396,6 +437,27 @@ class BaseSharesTest(test.BaseTestCase): client.wait_for_snapshot_status(snapshot["id"], "available") return snapshot + @classmethod + def create_cgsnapshot_wait_for_active(cls, consistency_group_id, + name=None, description=None, + client=None, cleanup_in_class=True): + client = client or cls.shares_client + if description is None: + description = "Tempest's cgsnapshot" + cgsnapshot = client.create_cgsnapshot(consistency_group_id, name=name, + description=description) + resource = { + "type": "cgsnapshot", + "id": cgsnapshot["id"], + "client": client, + } + if cleanup_in_class: + cls.class_resources.insert(0, resource) + else: + cls.method_resources.insert(0, resource) + client.wait_for_cgsnapshot_status(cgsnapshot["id"], "available") + return cgsnapshot + @classmethod def create_share_network(cls, client=None, cleanup_in_class=False, **kwargs): @@ -494,7 +556,11 @@ class BaseSharesTest(test.BaseTestCase): client = res["client"] with handle_cleanup_exceptions(): if res["type"] is "share": - client.delete_share(res_id) + params = None + cg_id = res.get('consistency_group_id') + if cg_id: + params = {'consistency_group_id': cg_id} + client.delete_share(res_id, params=params) client.wait_for_resource_deletion(share_id=res_id) elif res["type"] is "snapshot": client.delete_snapshot(res_id) @@ -508,6 +574,12 @@ class BaseSharesTest(test.BaseTestCase): elif res["type"] is "share_type": client.delete_share_type(res_id) client.wait_for_resource_deletion(st_id=res_id) + elif res["type"] is "consistency_group": + client.delete_consistency_group(res_id) + client.wait_for_resource_deletion(cg_id=res_id) + elif res["type"] is "cgsnapshot": + client.delete_cgsnapshot(res_id) + client.wait_for_resource_deletion(cgsnapshot_id=res_id) else: LOG.warn("Provided unsupported resource type for " "cleanup '%s'. Skipping." % res["type"]) diff --git a/manila_tempest_tests/tests/api/test_consistency_group_actions.py b/manila_tempest_tests/tests/api/test_consistency_group_actions.py new file mode 100644 index 0000000000..81bff8ac2b --- /dev/null +++ b/manila_tempest_tests/tests/api/test_consistency_group_actions.py @@ -0,0 +1,371 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Andrew Kerr +# 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 tempest import test +from tempest_lib.common.utils import data_utils +import testtools + +from manila_tempest_tests.tests.api import base + +CONF = config.CONF + +CG_SIMPLE_KEYS = {"id", "name", "links"} +CG_DETAIL_REQUIRED_KEYS = {"id", "name", "description", "created_at", "status", + "project_id", "host", "links"} +CGSNAPSHOT_SIMPLE_KEYS = {"id", "name", "links"} +CGSNAPSHOT_DETAIL_REQUIRED_KEYS = {"id", "name", "description", "created_at", + "status", "project_id", "links"} + + +@testtools.skipUnless(CONF.share.run_consistency_group_tests, + 'Consistency Group tests disabled.') +class ConsistencyGroupActionsTest(base.BaseSharesTest): + """Covers consistency group functionality.""" + + @classmethod + def resource_setup(cls): + super(ConsistencyGroupActionsTest, cls).resource_setup() + # Create consistency group + cls.cg_name = data_utils.rand_name("tempest-cg-name") + cls.cg_desc = data_utils.rand_name("tempest-cg-description") + cls.consistency_group = cls.create_consistency_group( + name=cls.cg_name, + description=cls.cg_desc, + ) + + # Create 2 shares inside consistency group + cls.share_name = data_utils.rand_name("tempest-share-name") + cls.share_desc = data_utils.rand_name("tempest-share-description") + cls.share_size = 1 + cls.share = cls.create_share( + name=cls.share_name, + description=cls.share_desc, + size=cls.share_size, + consistency_group_id=cls.consistency_group['id'], + metadata={'key': 'value'}, + ) + + cls.share_name2 = data_utils.rand_name("tempest-share-name") + cls.share_desc2 = data_utils.rand_name("tempest-share-description") + cls.share_size2 = 2 + cls.share2 = cls.create_share( + name=cls.share_name2, + description=cls.share_desc2, + size=cls.share_size2, + consistency_group_id=cls.consistency_group['id'], + ) + + cls.cgsnap_name = data_utils.rand_name("tempest-cgsnap-name") + cls.cgsnap_desc = data_utils.rand_name("tempest-cgsnap-description") + cls.cgsnapshot = cls.create_cgsnapshot_wait_for_active( + cls.consistency_group["id"], + name=cls.cgsnap_name, + description=cls.cgsnap_desc) + + # Create second consistency group for purposes of sorting and snapshot + # filtering + cls.cg_name2 = data_utils.rand_name("tempest-cg-name") + cls.cg_desc2 = data_utils.rand_name("tempest-cg-description") + cls.consistency_group2 = cls.create_consistency_group( + name=cls.cg_name2, + description=cls.cg_desc2, + ) + + # Create 1 share in second consistency group + cls.share_name3 = data_utils.rand_name("tempest-share-name") + cls.share_desc3 = data_utils.rand_name("tempest-share-description") + cls.share3 = cls.create_share( + name=cls.share_name3, + description=cls.share_desc3, + size=cls.share_size, + consistency_group_id=cls.consistency_group2['id'], + ) + + cls.cgsnap_name2 = data_utils.rand_name("tempest-cgsnap-name") + cls.cgsnap_desc2 = data_utils.rand_name("tempest-cgsnap-description") + cls.cgsnapshot2 = cls.create_cgsnapshot_wait_for_active( + cls.consistency_group2['id'], + name=cls.cgsnap_name2, + description=cls.cgsnap_desc2) + + @test.attr(type=["gate", ]) + def test_get_consistency_group(self): + + # Get consistency group + consistency_group = self.shares_client.get_consistency_group( + self.consistency_group['id']) + + # Verify keys + actual_keys = set(consistency_group.keys()) + self.assertTrue(CG_DETAIL_REQUIRED_KEYS.issubset(actual_keys), + 'Not all required keys returned for consistency ' + 'group %s. Expected at least: %s, found %s' % ( + consistency_group['id'], + CG_DETAIL_REQUIRED_KEYS, + actual_keys)) + + # Verify values + msg = "Expected name: '%s', actual name: '%s'" % ( + self.cg_name, consistency_group["name"]) + self.assertEqual(self.cg_name, str(consistency_group["name"]), msg) + + msg = "Expected description: '%s', actual description: '%s'" % ( + self.cg_desc, consistency_group["description"]) + self.assertEqual(self.cg_desc, str(consistency_group["description"]), + msg) + + @test.attr(type=["gate", ]) + def test_get_share(self): + + # Get share + share = self.shares_client.get_share(self.share['id']) + + # Verify keys + expected_keys = {"status", "description", "links", "availability_zone", + "created_at", "export_location", "share_proto", + "name", "snapshot_id", "id", "size", + "consistency_group_id"} + actual_keys = set(share.keys()) + self.assertTrue(expected_keys.issubset(actual_keys), + 'Not all required keys returned for share %s. ' + 'Expected at least: %s, found %s' % (share['id'], + expected_keys, + actual_keys)) + + # Verify values + msg = "Expected name: '%s', actual name: '%s'" % (self.share_name, + share["name"]) + self.assertEqual(self.share_name, str(share["name"]), msg) + + msg = "Expected description: '%s', actual description: '%s'" % ( + self.share_desc, share["description"]) + self.assertEqual(self.share_desc, str(share["description"]), msg) + + msg = "Expected size: '%s', actual size: '%s'" % (self.share_size, + share["size"]) + self.assertEqual(self.share_size, int(share["size"]), msg) + + msg = "Expected consistency_group_id: '%s', actual value: '%s'" % ( + self.consistency_group["id"], share["consistency_group_id"]) + self.assertEqual( + self.consistency_group["id"], share["consistency_group_id"], msg) + + @test.attr(type=["gate", ]) + def test_list_consistency_groups(self): + + # List consistency groups + consistency_groups = self.shares_client.list_consistency_groups() + + # Verify keys + [self.assertEqual(CG_SIMPLE_KEYS, set(cg.keys())) for cg in + consistency_groups] + + # Consistency group ids are in list exactly once + for cg_id in [self.consistency_group["id"], + self.consistency_group2["id"]]: + gen = [cgid["id"] for cgid in consistency_groups + if cgid["id"] == cg_id] + msg = ("Expected id %s exactly once in consistency group list" % + cg_id) + self.assertEqual(1, len(gen), msg) + + @test.attr(type=["gate", ]) + def test_list_consistency_groups_with_detail(self): + + # List consistency groups + consistency_groups = self.shares_client.list_consistency_groups( + detailed=True) + + # Verify keys + [self.assertTrue(CG_DETAIL_REQUIRED_KEYS.issubset(set(cg.keys()))) + for cg in consistency_groups] + + # Consistency group ids are in list exactly once + for cg_id in [self.consistency_group["id"], + self.consistency_group2["id"]]: + gen = [cgid["id"] for cgid in consistency_groups + if cgid["id"] == cg_id] + msg = ("Expected id %s exactly once in consistency group list" % + cg_id) + self.assertEqual(1, len(gen), msg) + + @test.attr(type=["gate", ]) + def test_filter_shares_by_consistency_group_id(self): + + shares = self.shares_client.list_shares(detailed=True, params={ + 'consistency_group_id': self.consistency_group['id']}) + + share_ids = [share['id'] for share in shares] + + self.assertEqual(2, len(shares), + 'Incorrect number of shares returned. Expected 2, ' + 'got %s' % len(shares)) + self.assertIn(self.share['id'], share_ids, + 'Share %s expected in returned list, but got %s' + % (self.share['id'], share_ids)) + self.assertIn(self.share2['id'], share_ids, + 'Share %s expected in returned list, but got %s' + % (self.share['id'], share_ids)) + + @test.attr(type=["gate", ]) + def test_get_cgsnapshot(self): + # Get consistency group + consistency_group = self.shares_client.get_consistency_group( + self.consistency_group['id']) + + # Verify keys + actual_keys = set(consistency_group.keys()) + self.assertTrue(CG_DETAIL_REQUIRED_KEYS.issubset(actual_keys), + 'Not all required keys returned for consistency ' + 'group %s. Expected at least: %s, found %s' % ( + consistency_group['id'], + CG_DETAIL_REQUIRED_KEYS, + actual_keys)) + + # Verify values + msg = "Expected name: '%s', actual name: '%s'" % ( + self.cg_name, consistency_group["name"]) + self.assertEqual(self.cg_name, str(consistency_group["name"]), msg) + + msg = "Expected description: '%s', actual description: '%s'" % ( + self.cg_desc, consistency_group["description"]) + self.assertEqual(self.cg_desc, str(consistency_group["description"]), + msg) + + @test.attr(type=["gate", ]) + def test_get_cgsnapshot_members(self): + + cgsnapshot_members = self.shares_client.list_cgsnapshot_members( + self.cgsnapshot['id']) + member_share_ids = [member['share_id'] for member in + cgsnapshot_members] + self.assertEqual(2, len(cgsnapshot_members), + 'Unexpected number of cgsnapshot members. Expected ' + '2, got %s.' % len(cgsnapshot_members)) + # Verify each share is represented in the cgsnapshot appropriately + for share_id in [self.share['id'], self.share2['id']]: + self.assertIn(share_id, member_share_ids, + 'Share missing %s missing from cgsnapshot. Found %s.' + % (share_id, member_share_ids)) + for share in [self.share, self.share2]: + for member in cgsnapshot_members: + if share['id'] == member['share_id']: + self.assertEqual(share['size'], member['size']) + self.assertEqual(share['share_proto'], + member['share_protocol']) + # TODO(akerr): Add back assert when bug 1483886 is fixed + # self.assertEqual(share['share_type'], + # member['share_type_id']) + + @test.attr(type=["gate", "smoke", ]) + def test_create_consistency_group_from_populated_cgsnapshot(self): + + cgsnapshot_members = self.shares_client.list_cgsnapshot_members( + self.cgsnapshot['id']) + + new_consistency_group = self.create_consistency_group( + cleanup_in_class=False, source_cgsnapshot_id=self.cgsnapshot['id']) + + new_shares = self.shares_client.list_shares( + params={'consistency_group_id': new_consistency_group['id']}, + detailed=True) + + # Verify each new share is available + for share in new_shares: + self.assertEqual('available', share['status'], + 'Share %s is not in available status.' + % share['id']) + + # Verify each cgsnapshot member is represented in the new cg + # appropriately + share_source_member_ids = [share['source_cgsnapshot_member_id'] for + share in new_shares] + for member in cgsnapshot_members: + self.assertIn(member['id'], share_source_member_ids, + 'cgsnapshot member %s not represented by ' + 'consistency group %s.' % ( + member['id'], new_consistency_group['id'])) + for share in new_shares: + if share['source_cgsnapshot_member_id'] == member['id']: + self.assertEqual(member['size'], share['size']) + self.assertEqual(member['share_protocol'], + share['share_proto']) + # TODO(akerr): Add back assert when bug 1483886 is fixed + # self.assertEqual(member['share_type_id'], + # share['share_type']) + + +@testtools.skipUnless(CONF.share.run_consistency_group_tests, + 'Consistency Group tests disabled.') +class ConsistencyGroupRenameTest(base.BaseSharesTest): + + @classmethod + def resource_setup(cls): + super(ConsistencyGroupRenameTest, cls).resource_setup() + + # Create consistency group + cls.cg_name = data_utils.rand_name("tempest-cg-name") + cls.cg_desc = data_utils.rand_name("tempest-cg-description") + cls.consistency_group = cls.create_consistency_group( + name=cls.cg_name, + description=cls.cg_desc, + ) + + @test.attr(type=["gate", ]) + def test_update_consistency_group(self): + + # Get consistency_group + consistency_group = self.shares_client.get_consistency_group( + self.consistency_group['id']) + self.assertEqual(self.cg_name, consistency_group["name"]) + self.assertEqual(self.cg_desc, consistency_group["description"]) + + # Update consistency_group + new_name = data_utils.rand_name("tempest-new-name") + new_desc = data_utils.rand_name("tempest-new-description") + updated = self.shares_client.update_consistency_group( + consistency_group["id"], name=new_name, description=new_desc) + self.assertEqual(new_name, updated["name"]) + self.assertEqual(new_desc, updated["description"]) + + # Get consistency_group + consistency_group = self.shares_client.get_consistency_group( + self.consistency_group['id']) + self.assertEqual(new_name, consistency_group["name"]) + self.assertEqual(new_desc, consistency_group["description"]) + + @test.attr(type=["gate", ]) + def test_create_update_read_consistency_group_with_unicode(self): + value1 = u'ಠ_ಠ' + value2 = u'ಠ_ರೃ' + # Create consistency_group + consistency_group = self.create_consistency_group( + cleanup_in_class=False, name=value1, description=value1) + self.assertEqual(value1, consistency_group["name"]) + self.assertEqual(value1, consistency_group["description"]) + + # Update consistency_group + updated = self.shares_client.update_consistency_group( + consistency_group["id"], name=value2, description=value2) + self.assertEqual(value2, updated["name"]) + self.assertEqual(value2, updated["description"]) + + # Get consistency_group + consistency_group = self.shares_client.get_consistency_group( + consistency_group['id']) + self.assertEqual(value2, consistency_group["name"]) + self.assertEqual(value2, consistency_group["description"]) diff --git a/manila_tempest_tests/tests/api/test_consistency_groups.py b/manila_tempest_tests/tests/api/test_consistency_groups.py new file mode 100644 index 0000000000..1ba3902bb4 --- /dev/null +++ b/manila_tempest_tests/tests/api/test_consistency_groups.py @@ -0,0 +1,130 @@ +# Copyright 2015 Andrew Kerr +# 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 # noqa +from tempest import test # noqa +from tempest_lib import exceptions as lib_exc # noqa +import testtools # noqa + +from manila_tempest_tests.tests.api import base + +CONF = config.CONF +CG_REQUIRED_ELEMENTS = {"id", "name", "description", "created_at", "status", + "share_types", "project_id", "host", "links"} +CGSNAPSHOT_REQUIRED_ELEMENTS = {"id", "name", "description", "created_at", + "status", "project_id", "links"} + + +@testtools.skipUnless(CONF.share.run_consistency_group_tests, + 'Consistency Group tests disabled.') +class ConsistencyGroupsTest(base.BaseSharesTest): + """Covers consistency group functionality.""" + + @test.attr(type=["gate", ]) + def test_create_populate_delete_consistency_group(self): + # Create a consistency group + consistency_group = self.create_consistency_group( + cleanup_in_class=False) + self.assertTrue(CG_REQUIRED_ELEMENTS.issubset( + consistency_group.keys()), + 'At least one expected element missing from consistency group ' + 'response. Expected %(expected)s, got %(actual)s.' % { + "expected": CG_REQUIRED_ELEMENTS, + "actual": consistency_group.keys()}) + # Populate + share = self.create_share(consistency_group_id=consistency_group['id'], + cleanup_in_class=False) + # Delete + params = {"consistency_group_id": consistency_group['id']} + self.shares_client.delete_share(share['id'], params=params) + self.shares_client.wait_for_resource_deletion(share_id=share['id']) + self.shares_client.delete_consistency_group(consistency_group['id']) + self.shares_client.wait_for_resource_deletion( + cg_id=consistency_group['id']) + + # Verify + self.assertRaises(lib_exc.NotFound, + self.shares_client.get_consistency_group, + consistency_group['id']) + self.assertRaises(lib_exc.NotFound, + self.shares_client.get_share, + share['id']) + + @test.attr(type=["gate", ]) + def test_create_delete_empty_cgsnapshot(self): + # Create base consistency group + consistency_group = self.create_consistency_group( + cleanup_in_class=False) + # Create cgsnapshot + cgsnapshot = self.create_cgsnapshot_wait_for_active( + consistency_group["id"], cleanup_in_class=False) + + self.assertTrue(CGSNAPSHOT_REQUIRED_ELEMENTS.issubset( + cgsnapshot.keys()), + 'At least one expected element missing from cgsnapshot response. ' + 'Expected %(expected)s, got %(actual)s.' % { + "expected": CGSNAPSHOT_REQUIRED_ELEMENTS, + "actual": cgsnapshot.keys()}) + + cgsnapshot_members = self.shares_client.list_cgsnapshot_members( + cgsnapshot['id']) + + self.assertEmpty(cgsnapshot_members, + 'Expected 0 cgsnapshot members, got %s' % len( + cgsnapshot_members)) + + # delete snapshot + self.shares_client.delete_cgsnapshot(cgsnapshot["id"]) + self.shares_client.wait_for_resource_deletion( + cgsnapshot_id=cgsnapshot["id"]) + self.assertRaises(lib_exc.NotFound, + self.shares_client.get_cgsnapshot, cgsnapshot['id']) + + @test.attr(type=["gate", "smoke", ]) + def test_create_consistency_group_from_empty_cgsnapshot(self): + # Create base consistency group + consistency_group = self.create_consistency_group( + cleanup_in_class=False) + + # Create cgsnapshot + cgsnapshot = self.create_cgsnapshot_wait_for_active( + consistency_group["id"], cleanup_in_class=False) + + cgsnapshot_members = self.shares_client.list_cgsnapshot_members( + cgsnapshot['id']) + + self.assertEmpty(cgsnapshot_members, + 'Expected 0 cgsnapshot members, got %s' % len( + cgsnapshot_members)) + + new_consistency_group = self.create_consistency_group( + cleanup_in_class=False, source_cgsnapshot_id=cgsnapshot['id']) + + new_shares = self.shares_client.list_shares( + params={'consistency_group_id': new_consistency_group['id']}) + + self.assertEmpty(new_shares, + 'Expected 0 new shares, got %s' % len(new_shares)) + + msg = 'Expected cgsnapshot_id %s as source of share %s' % ( + cgsnapshot['id'], new_consistency_group['source_cgsnapshot_id']) + self.assertEqual(new_consistency_group['source_cgsnapshot_id'], + cgsnapshot['id'], msg) + + msg = 'Unexpected share_types on new consistency group. Expected %s, ' \ + 'got %s.' % (consistency_group['share_types'], + new_consistency_group['share_types']) + self.assertEqual(sorted(consistency_group['share_types']), + sorted(new_consistency_group['share_types']), msg) diff --git a/manila_tempest_tests/tests/api/test_consistency_groups_negative.py b/manila_tempest_tests/tests/api/test_consistency_groups_negative.py new file mode 100644 index 0000000000..7c813c0e57 --- /dev/null +++ b/manila_tempest_tests/tests/api/test_consistency_groups_negative.py @@ -0,0 +1,205 @@ +# Copyright 2015 Andrew Kerr +# 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 tempest import test +from tempest_lib.common.utils import data_utils +from tempest_lib import exceptions as lib_exc +import testtools + +from manila_tempest_tests.tests.api import base + +CONF = config.CONF + + +@testtools.skipUnless(CONF.share.run_consistency_group_tests, + 'Consistency Group tests disabled.') +class ConsistencyGroupsNegativeTest(base.BaseSharesTest): + + @classmethod + def resource_setup(cls): + super(ConsistencyGroupsNegativeTest, cls).resource_setup() + # Create a consistency group + cls.cg_name = data_utils.rand_name("tempest-cg-name") + cls.cg_desc = data_utils.rand_name("tempest-cg-description") + cls.consistency_group = cls.create_consistency_group( + name=cls.cg_name, + description=cls.cg_desc + ) + # Create a share in the consistency group + cls.share_name = data_utils.rand_name("tempest-share-name") + cls.share_desc = data_utils.rand_name("tempest-share-description") + cls.share_size = 1 + cls.share = cls.create_share( + name=cls.share_name, + description=cls.share_desc, + size=cls.share_size, + consistency_group_id=cls.consistency_group['id'] + ) + # Create a cgsnapshot of the consistency group + cls.cgsnap_name = data_utils.rand_name("tempest-cgsnap-name") + cls.cgsnap_desc = data_utils.rand_name("tempest-cgsnap-description") + cls.cgsnapshot = cls.create_cgsnapshot_wait_for_active( + cls.consistency_group["id"], + name=cls.cgsnap_name, + description=cls.cgsnap_desc) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_create_cg_with_invalid_source_cgsnapshot_id_value( + self): + self.assertRaises(lib_exc.BadRequest, + self.create_consistency_group, + source_cgsnapshot_id='foobar', + cleanup_in_class=False) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_create_cg_with_nonexistent_source_cgsnapshot_id_value(self): + self.assertRaises(lib_exc.BadRequest, + self.create_consistency_group, + source_cgsnapshot_id=self.share['id'], + cleanup_in_class=False) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_create_cg_with_invalid_share_network_id_value( + self): + self.assertRaises(lib_exc.BadRequest, + self.create_consistency_group, + share_network_id='foobar', + cleanup_in_class=False) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_create_cg_with_nonexistent_share_network_id_value(self): + self.assertRaises(lib_exc.BadRequest, + self.create_consistency_group, + share_network_id=self.share['id'], + cleanup_in_class=False) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_create_cg_with_invalid_share_type_id_value( + self): + self.assertRaises(lib_exc.BadRequest, + self.create_consistency_group, + share_type_ids=['foobar'], + cleanup_in_class=False) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_create_cg_with_nonexistent_share_type_id_value(self): + self.assertRaises(lib_exc.BadRequest, + self.create_consistency_group, + share_type_ids=[self.share['id']], + cleanup_in_class=False) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_create_cgsnapshot_with_invalid_cg_id_value( + self): + self.assertRaises(lib_exc.BadRequest, + self.create_cgsnapshot_wait_for_active, + 'foobar', + cleanup_in_class=False) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_create_cgsnapshot_with_nonexistent_cg_id_value(self): + self.assertRaises(lib_exc.BadRequest, + self.create_cgsnapshot_wait_for_active, + self.share['id'], + cleanup_in_class=False) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_get_cg_with_wrong_id(self): + self.assertRaises(lib_exc.NotFound, + self.shares_client.get_consistency_group, + "wrong_consistency_group_id") + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_get_cg_without_passing_cg_id(self): + self.assertRaises(lib_exc.NotFound, + self.shares_client.get_consistency_group, '') + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_update_cg_with_wrong_id(self): + self.assertRaises(lib_exc.NotFound, + self.shares_client.update_consistency_group, + 'wrong_consistency_group_id', + name='new_name', + description='new_description') + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_delete_cg_with_wrong_id(self): + self.assertRaises(lib_exc.NotFound, + self.shares_client.delete_consistency_group, + "wrong_consistency_group_id") + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_delete_cg_without_passing_cg_id(self): + self.assertRaises(lib_exc.NotFound, + self.shares_client.delete_consistency_group, '') + + @test.attr(type=["negative", "gate", ]) + def test_delete_cg_in_use_by_cgsnapshot(self): + # Attempt delete of share type + self.assertRaises(lib_exc.Conflict, + self.shares_client.delete_consistency_group, + self.consistency_group['id']) + + @test.attr(type=["negative", "gate", ]) + def test_delete_share_in_use_by_cgsnapshot(self): + # Attempt delete of share type + params = {'consistency_group_id': self.share['consistency_group_id']} + self.assertRaises(lib_exc.Forbidden, + self.shares_client.delete_share, + self.share['id'], + params=params) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_delete_cg_containing_a_share(self): + self.assertRaises(lib_exc.Conflict, + self.shares_client.delete_consistency_group, + self.consistency_group['id']) + # Verify consistency group is not put into error state from conflict + cg = self.shares_client.get_consistency_group( + self.consistency_group['id']) + self.assertEqual('available', cg['status']) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_filter_shares_on_invalid_cg_id(self): + shares = self.shares_client.list_shares(detailed=True, params={ + 'consistency_group_id': 'foobar'}) + + self.assertEqual(0, len(shares), 'Incorrect number of shares ' + 'returned. Expected 0, got %s.' % + len(shares)) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_filter_shares_on_nonexistent_cg_id(self): + shares = self.shares_client.list_shares(detailed=True, params={ + 'consistency_group_id': self.share['id']}) + + self.assertEqual(0, len(shares), 'Incorrect number of shares ' + 'returned. Expected 0, got %s.' % + len(shares)) + + @test.attr(type=["negative", "smoke", "gate", ]) + def test_filter_shares_on_empty_cg_id(self): + consistency_group = self.create_consistency_group( + name='tempest_cg', + description='tempest_cg_desc', + cleanup_in_class=False, + ) + shares = self.shares_client.list_shares(detailed=True, params={ + 'consistency_group_id': consistency_group['id']}) + + self.assertEqual(0, len(shares), 'Incorrect number of shares ' + 'returned. Expected 0, got %s.' % + len(shares))