Add functional tests for Manila consistency groups

This commit adds tempest functional tests for Manila consistency
groups and cgsnapshot objects and actions. By default these tests are
enabled; 3rd party CI systems should disabled these tests through the
RUN_MANILA_CG_TESTS environment variable if their driver does not
support consistency groups.

Partially implements bp manila-consistency-groups
Change-Id: I3297e02ad53c328f0bfe5245fefdb6af80552b4a
diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py
index 2c5e809..046f8cd 100644
--- a/manila_tempest_tests/config.py
+++ b/manila_tempest_tests/config.py
@@ -139,6 +139,11 @@
                 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 5ce561e..cf58d8c 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 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 @@
     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 @@
             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 @@
         """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 @@
         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 @@
                            (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 @@
         """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 @@
         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 @@
             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 @@
         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 @@
         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 33478cd..aa688e4 100644
--- a/manila_tempest_tests/share_exceptions.py
+++ b/manila_tempest_tests/share_exceptions.py
@@ -24,6 +24,11 @@
     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 @@
     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 0000000..c1fdb15
--- /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 0000000..4fbe7f9
--- /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 0000000..a7a715f
--- /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 6444ccc..65e299d 100644
--- a/manila_tempest_tests/tests/api/base.py
+++ b/manila_tempest_tests/tests/api/base.py
@@ -280,7 +280,8 @@
     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 @@
             '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)
@@ -376,6 +381,42 @@
         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,
                                         client=None, cleanup_in_class=True):
@@ -397,6 +438,27 @@
         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):
         if client is None:
@@ -494,7 +556,11 @@
                 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 @@
                     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 0000000..81bff8a
--- /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 0000000..1ba3902
--- /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 0000000..7c813c0
--- /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))