Merge "Adding and removing bytes_body param from unit tests"
diff --git a/releasenotes/notes/add-volume-groups-tempest-tests-dd7b2abfe2b48427.yaml b/releasenotes/notes/add-volume-groups-tempest-tests-dd7b2abfe2b48427.yaml
new file mode 100644
index 0000000..898d366
--- /dev/null
+++ b/releasenotes/notes/add-volume-groups-tempest-tests-dd7b2abfe2b48427.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Add groups and group_types clients for the volume service as library.
+    Add tempest tests for create group, delete group, show group, and
+    list group volume APIs.
diff --git a/releasenotes/notes/set-cinder-api-v3-option-true-1b3e61e3129b7c00.yaml b/releasenotes/notes/set-cinder-api-v3-option-true-1b3e61e3129b7c00.yaml
new file mode 100644
index 0000000..6959ca7
--- /dev/null
+++ b/releasenotes/notes/set-cinder-api-v3-option-true-1b3e61e3129b7c00.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+  - |
+    The volume config option 'api_v3' default is changed to
+    ``True`` because the volume v3 API is CURRENT.
diff --git a/tempest/api/compute/servers/test_device_tagging.py b/tempest/api/compute/servers/test_device_tagging.py
index 9ab508d..7ee1b02 100644
--- a/tempest/api/compute/servers/test_device_tagging.py
+++ b/tempest/api/compute/servers/test_device_tagging.py
@@ -84,10 +84,14 @@
                 if d['mac'] == self.net_2_200_mac:
                     self.assertEqual(d['tags'], ['net-2-200'])
 
-        found_devices = [d['tags'][0] for d in md_dict['devices']]
-        self.assertItemsEqual(found_devices, ['port-1', 'port-2', 'net-1',
-                                              'net-2-100', 'net-2-200',
-                                              'boot', 'other'])
+            # A hypervisor may present multiple paths to a tagged disk, so
+            # there may be duplicated tags in the metadata, use set() to
+            # remove duplicated tags.
+            found_devices = [d['tags'][0] for d in md_dict['devices']]
+            self.assertEqual(set(found_devices), set(['port-1', 'port-2',
+                                                      'net-1', 'net-2-100',
+                                                      'net-2-200', 'boot',
+                                                      'other']))
 
     @decorators.idempotent_id('a2e65a6c-66f1-4442-aaa8-498c31778d96')
     @test.services('network', 'volume', 'image')
diff --git a/tempest/api/volume/admin/test_groups.py b/tempest/api/volume/admin/test_groups.py
new file mode 100644
index 0000000..8609bdb
--- /dev/null
+++ b/tempest/api/volume/admin/test_groups.py
@@ -0,0 +1,109 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# 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.api.volume import base
+from tempest.common import waiters
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+CONF = config.CONF
+
+
+class GroupsTest(base.BaseVolumeAdminTest):
+    _api_version = 3
+    min_microversion = '3.14'
+    max_microversion = 'latest'
+
+    def _delete_group(self, grp_id, delete_volumes=True):
+        self.admin_groups_client.delete_group(grp_id, delete_volumes)
+        vols = self.admin_volume_client.list_volumes(detail=True)['volumes']
+        for vol in vols:
+            if vol['group_id'] == grp_id:
+                self.admin_volume_client.wait_for_resource_deletion(vol['id'])
+        self.admin_groups_client.wait_for_resource_deletion(grp_id)
+
+    @decorators.idempotent_id('4b111d28-b73d-4908-9bd2-03dc2992e4d4')
+    def test_group_create_show_list_delete(self):
+        # Create volume type
+        volume_type = self.create_volume_type()
+
+        # Create group type
+        group_type = self.create_group_type()
+
+        # Create group
+        grp1_name = data_utils.rand_name('Group1')
+        grp1 = self.admin_groups_client.create_group(
+            group_type=group_type['id'],
+            volume_types=[volume_type['id']],
+            name=grp1_name)['group']
+        waiters.wait_for_volume_resource_status(
+            self.admin_groups_client, grp1['id'], 'available')
+        grp1_id = grp1['id']
+
+        grp2_name = data_utils.rand_name('Group2')
+        grp2 = self.admin_groups_client.create_group(
+            group_type=group_type['id'],
+            volume_types=[volume_type['id']],
+            name=grp2_name)['group']
+        waiters.wait_for_volume_resource_status(
+            self.admin_groups_client, grp2['id'], 'available')
+        grp2_id = grp2['id']
+
+        # Create volume
+        vol1_name = data_utils.rand_name("volume")
+        params = {'name': vol1_name,
+                  'volume_type': volume_type['id'],
+                  'group_id': grp1['id'],
+                  'size': CONF.volume.volume_size}
+        vol1 = self.admin_volume_client.create_volume(**params)['volume']
+        self.assertEqual(grp1['id'], vol1['group_id'])
+        waiters.wait_for_volume_resource_status(
+            self.admin_volume_client, vol1['id'], 'available')
+        vol1_id = vol1['id']
+
+        # Get a given group
+        grp1 = self.admin_groups_client.show_group(grp1['id'])['group']
+        self.assertEqual(grp1_name, grp1['name'])
+        self.assertEqual(grp1_id, grp1['id'])
+
+        grp2 = self.admin_groups_client.show_group(grp2['id'])['group']
+        self.assertEqual(grp2_name, grp2['name'])
+        self.assertEqual(grp2_id, grp2['id'])
+
+        # Get all groups with detail
+        grps = self.admin_groups_client.list_groups(
+            detail=True)['groups']
+        filtered_grps = [g for g in grps if g['id'] in [grp1_id, grp2_id]]
+        self.assertEqual(2, len(filtered_grps))
+        for grp in filtered_grps:
+            self.assertEqual([volume_type['id']], grp['volume_types'])
+            self.assertEqual(group_type['id'], grp['group_type'])
+
+        vols = self.admin_volume_client.list_volumes(
+            detail=True)['volumes']
+        filtered_vols = [v for v in vols if v['id'] in [vol1_id]]
+        self.assertEqual(1, len(filtered_vols))
+        for vol in filtered_vols:
+            self.assertEqual(grp1_id, vol['group_id'])
+
+        # Delete group
+        # grp1 has a volume so delete_volumes flag is set to True by default
+        self._delete_group(grp1_id)
+        # grp2 is empty so delete_volumes flag can be set to False
+        self._delete_group(grp2_id, delete_volumes=False)
+        grps = self.admin_groups_client.list_groups(
+            detail=True)['groups']
+        self.assertEmpty(grps)
diff --git a/tempest/api/volume/admin/test_snapshot_manage.py b/tempest/api/volume/admin/test_snapshot_manage.py
index 6c09042..9ff7160 100644
--- a/tempest/api/volume/admin/test_snapshot_manage.py
+++ b/tempest/api/volume/admin/test_snapshot_manage.py
@@ -18,6 +18,7 @@
 from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
+from tempest.lib import exceptions
 
 CONF = config.CONF
 
@@ -38,8 +39,9 @@
             raise cls.skipException("Manage snapshot tests are disabled")
 
         if len(CONF.volume.manage_snapshot_ref) != 2:
-            raise cls.skipException("Manage snapshot ref is not correctly "
-                                    "configured")
+            msg = ("Manage snapshot ref is not correctly configured, "
+                   "it should be a list of two elements")
+            raise exceptions.InvalidConfiguration(msg)
 
     @decorators.idempotent_id('0132f42d-0147-4b45-8501-cc504bbf7810')
     def test_unmanage_manage_snapshot(self):
diff --git a/tempest/api/volume/admin/test_volume_manage.py b/tempest/api/volume/admin/test_volume_manage.py
index a039085..4b352e0 100644
--- a/tempest/api/volume/admin/test_volume_manage.py
+++ b/tempest/api/volume/admin/test_volume_manage.py
@@ -18,6 +18,7 @@
 from tempest import config
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
+from tempest.lib import exceptions
 
 CONF = config.CONF
 
@@ -32,8 +33,9 @@
             raise cls.skipException("Manage volume tests are disabled")
 
         if len(CONF.volume.manage_volume_ref) != 2:
-            raise cls.skipException("Manage volume ref is not correctly "
-                                    "configured")
+            msg = ("Manage volume ref is not correctly configured, "
+                   "it should be a list of two elements")
+            raise exceptions.InvalidConfiguration(msg)
 
     @decorators.idempotent_id('70076c71-0ce1-4208-a8ff-36a66e65cc1e')
     def test_unmanage_manage_volume(self):
diff --git a/tempest/api/volume/base.py b/tempest/api/volume/base.py
index 8d66156..394c453 100644
--- a/tempest/api/volume/base.py
+++ b/tempest/api/volume/base.py
@@ -71,6 +71,8 @@
 
         cls.snapshots_client = cls.os_primary.snapshots_v2_client
         cls.volumes_client = cls.os_primary.volumes_v2_client
+        if cls._api_version == 3:
+            cls.volumes_client = cls.os_primary.volumes_v3_client
         cls.backups_client = cls.os_primary.backups_v2_client
         cls.volumes_extension_client =\
             cls.os_primary.volumes_v2_extension_client
@@ -79,9 +81,7 @@
         cls.volume_limits_client = cls.os_primary.volume_v2_limits_client
         cls.messages_client = cls.os_primary.volume_v3_messages_client
         cls.versions_client = cls.os_primary.volume_v3_versions_client
-
-        if cls._api_version == 3:
-            cls.volumes_client = cls.os_primary.volumes_v3_client
+        cls.groups_client = cls.os_primary.groups_v3_client
 
     def setUp(self):
         super(BaseVolumeTest, self).setUp()
@@ -253,6 +253,8 @@
         cls.admin_volume_types_client = cls.os_admin.volume_types_v2_client
         cls.admin_volume_manage_client = cls.os_admin.volume_manage_v2_client
         cls.admin_volume_client = cls.os_admin.volumes_v2_client
+        if cls._api_version == 3:
+            cls.admin_volume_client = cls.os_admin.volumes_v3_client
         cls.admin_hosts_client = cls.os_admin.volume_hosts_v2_client
         cls.admin_snapshot_manage_client = \
             cls.os_admin.snapshot_manage_v2_client
@@ -269,9 +271,8 @@
         cls.admin_scheduler_stats_client = \
             cls.os_admin.volume_scheduler_stats_v2_client
         cls.admin_messages_client = cls.os_admin.volume_v3_messages_client
-
-        if cls._api_version == 3:
-            cls.admin_volume_client = cls.os_admin.volumes_v3_client
+        cls.admin_groups_client = cls.os_admin.groups_v3_client
+        cls.admin_group_types_client = cls.os_admin.group_types_v3_client
 
     @classmethod
     def resource_setup(cls):
@@ -305,6 +306,16 @@
         cls.volume_types.append(volume_type['id'])
         return volume_type
 
+    def create_group_type(self, name=None, **kwargs):
+        """Create a test group-type"""
+        name = name or data_utils.rand_name(
+            self.__class__.__name__ + '-group-type')
+        group_type = self.admin_group_types_client.create_group_type(
+            name=name, **kwargs)['group_type']
+        self.addCleanup(self.admin_group_types_client.delete_group_type,
+                        group_type['id'])
+        return group_type
+
     @classmethod
     def clear_qos_specs(cls):
         for qos_id in cls.qos_specs:
diff --git a/tempest/clients.py b/tempest/clients.py
index 8ad6e92..d29bef9 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -255,6 +255,8 @@
             self.volume_v2.QuotaClassesClient()
         self.volumes_extension_client = self.volume_v1.ExtensionsClient()
         self.volumes_v2_extension_client = self.volume_v2.ExtensionsClient()
+        self.groups_v3_client = self.volume_v3.GroupsClient()
+        self.group_types_v3_client = self.volume_v3.GroupTypesClient()
         self.volume_availability_zone_client = \
             self.volume_v1.AvailabilityZoneClient()
         self.volume_v2_availability_zone_client = \
diff --git a/tempest/common/compute.py b/tempest/common/compute.py
index 9110c4a..9f467fe 100644
--- a/tempest/common/compute.py
+++ b/tempest/common/compute.py
@@ -204,6 +204,19 @@
                         except Exception:
                             LOG.exception('Deleting server %s failed',
                                           server['id'])
+                    for server in servers:
+                        # NOTE(artom) If the servers were booted with volumes
+                        # and with delete_on_termination=False we need to wait
+                        # for the servers to go away before proceeding with
+                        # cleanup, otherwise we'll attempt to delete the
+                        # volumes while they're still attached to servers that
+                        # are in the process of being deleted.
+                        try:
+                            waiters.wait_for_server_termination(
+                                clients.servers_client, server['id'])
+                        except Exception:
+                            LOG.exception('Server %s failed to delete in time',
+                                          server['id'])
 
     return body, servers
 
@@ -252,16 +265,34 @@
     def __init__(self, client_socket, url):
         """Contructor for the WebSocket wrapper to the socket."""
         self._socket = client_socket
+        # cached stream for early frames.
+        self.cached_stream = b''
         # Upgrade the HTTP connection to a WebSocket
         self._upgrade(url)
 
+    def _recv(self, recv_size):
+        """Wrapper to receive data from the cached stream or socket."""
+        if recv_size <= 0:
+            return None
+
+        data_from_cached = b''
+        data_from_socket = b''
+        if len(self.cached_stream) > 0:
+            read_from_cached = min(len(self.cached_stream), recv_size)
+            data_from_cached += self.cached_stream[:read_from_cached]
+            self.cached_stream = self.cached_stream[read_from_cached:]
+            recv_size -= read_from_cached
+        if recv_size > 0:
+            data_from_socket = self._socket.recv(recv_size)
+        return data_from_cached + data_from_socket
+
     def receive_frame(self):
         """Wrapper for receiving data to parse the WebSocket frame format"""
         # We need to loop until we either get some bytes back in the frame
         # or no data was received (meaning the socket was closed).  This is
         # done to handle the case where we get back some empty frames
         while True:
-            header = self._socket.recv(2)
+            header = self._recv(2)
             # If we didn't receive any data, just return None
             if not header:
                 return None
@@ -270,7 +301,7 @@
             # that only the 2nd byte contains the length, and since the
             # server doesn't do masking, we can just read the data length
             if ord_func(header[1]) & 127 > 0:
-                return self._socket.recv(ord_func(header[1]) & 127)
+                return self._recv(ord_func(header[1]) & 127)
 
     def send_frame(self, data):
         """Wrapper for sending data to add in the WebSocket frame format."""
@@ -318,6 +349,15 @@
         self._socket.sendall(reqdata.encode('utf8'))
         self.response = data = self._socket.recv(4096)
         # Loop through & concatenate all of the data in the response body
-        while data and self.response.find(b'\r\n\r\n') < 0:
+        end_loc = self.response.find(b'\r\n\r\n')
+        while data and end_loc < 0:
             data = self._socket.recv(4096)
             self.response += data
+            end_loc = self.response.find(b'\r\n\r\n')
+
+        if len(self.response) > end_loc + 4:
+            # In case some frames (e.g. the first RFP negotiation) have
+            # arrived, cache it for next reading.
+            self.cached_stream = self.response[end_loc + 4:]
+            # ensure response ends with '\r\n\r\n'.
+            self.response = self.response[:end_loc + 4]
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index 9c83c99..93e6fbf 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -186,8 +186,9 @@
     resources. The function extracts the name of the desired resource from
     the client class name of the resource.
     """
-    resource_name = re.findall(r'(Volume|Snapshot|Backup)',
-                               client.__class__.__name__)[0].lower()
+    resource_name = re.findall(
+        r'(Volume|Snapshot|Backup|Group)',
+        client.__class__.__name__)[0].lower()
     show_resource = getattr(client, 'show_' + resource_name)
     resource_status = show_resource(resource_id)[resource_name]['status']
     start = int(time.time())
diff --git a/tempest/config.py b/tempest/config.py
index fbe0a1b..7b96281 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -832,7 +832,7 @@
                 default=True,
                 help="Is the v2 volume API enabled"),
     cfg.BoolOpt('api_v3',
-                default=False,
+                default=True,
                 help="Is the v3 volume API enabled")
 ]
 
diff --git a/tempest/lib/services/network/base.py b/tempest/lib/services/network/base.py
index b6f9c91..fe8b244 100644
--- a/tempest/lib/services/network/base.py
+++ b/tempest/lib/services/network/base.py
@@ -54,7 +54,8 @@
         self.expected_success(200, resp.status)
         return rest_client.ResponseBody(resp, body)
 
-    def create_resource(self, uri, post_data, expect_empty_body=False):
+    def create_resource(self, uri, post_data, expect_empty_body=False,
+                        expect_response_code=201):
         req_uri = self.uri_prefix + uri
         req_post_data = json.dumps(post_data)
         resp, body = self.post(req_uri, req_post_data)
@@ -65,10 +66,11 @@
             body = json.loads(body)
         else:
             body = None
-        self.expected_success(201, resp.status)
+        self.expected_success(expect_response_code, resp.status)
         return rest_client.ResponseBody(resp, body)
 
-    def update_resource(self, uri, post_data, expect_empty_body=False):
+    def update_resource(self, uri, post_data, expect_empty_body=False,
+                        expect_response_code=200):
         req_uri = self.uri_prefix + uri
         req_post_data = json.dumps(post_data)
         resp, body = self.put(req_uri, req_post_data)
@@ -79,5 +81,5 @@
             body = json.loads(body)
         else:
             body = None
-        self.expected_success(200, resp.status)
+        self.expected_success(expect_response_code, resp.status)
         return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/network/tags_client.py b/tempest/lib/services/network/tags_client.py
index 20c2c11..5d49a79 100644
--- a/tempest/lib/services/network/tags_client.py
+++ b/tempest/lib/services/network/tags_client.py
@@ -27,14 +27,10 @@
         For more information, please refer to the official API reference:
         http://developer.openstack.org/api-ref/networking/v2/index.html#add-a-tag
         """
-        # NOTE(felipemonteiro): Cannot use ``update_resource`` method because
-        # this API requires self.put but returns 201 instead of 200 expected
-        # by ``update_resource``.
-        uri = '%s/%s/%s/tags/%s' % (
-            self.uri_prefix, resource_type, resource_id, tag)
-        resp, _ = self.put(uri, json.dumps({}))
-        self.expected_success(201, resp.status)
-        return rest_client.ResponseBody(resp)
+        uri = '/%s/%s/tags/%s' % (resource_type, resource_id, tag)
+        return self.update_resource(
+            uri, json.dumps({}), expect_response_code=201,
+            expect_empty_body=True)
 
     def check_tag_existence(self, resource_type, resource_id, tag):
         """Confirm that a given tag is set on the resource.
diff --git a/tempest/lib/services/volume/v2/encryption_types_client.py b/tempest/lib/services/volume/v2/encryption_types_client.py
index eeff537..20f3356 100755
--- a/tempest/lib/services/volume/v2/encryption_types_client.py
+++ b/tempest/lib/services/volume/v2/encryption_types_client.py
@@ -50,9 +50,9 @@
     def create_encryption_type(self, volume_type_id, **kwargs):
         """Create encryption type.
 
-        TODO: Current api-site doesn't contain this API description.
-        After fixing the api-site, we need to fix here also for putting
-        the link to api-site.
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v2/#create-an-encryption-type-for-v2
         """
         url = "/types/%s/encryption" % volume_type_id
         post_body = json.dumps({'encryption': kwargs})
@@ -71,9 +71,9 @@
     def update_encryption_type(self, volume_type_id, **kwargs):
         """Update an encryption type for an existing volume type.
 
-        TODO: Current api-site doesn't contain this API description.
-        After fixing the api-site, we need to fix here also for putting
-        the link to api-site.
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v2/#update-an-encryption-type-for-v2
         """
         url = "/types/%s/encryption/provider" % volume_type_id
         put_body = json.dumps({'encryption': kwargs})
diff --git a/tempest/lib/services/volume/v2/hosts_client.py b/tempest/lib/services/volume/v2/hosts_client.py
index 8fcf4d0..f44bda3 100644
--- a/tempest/lib/services/volume/v2/hosts_client.py
+++ b/tempest/lib/services/volume/v2/hosts_client.py
@@ -24,8 +24,12 @@
     api_version = "v2"
 
     def list_hosts(self, **params):
-        """Lists all hosts."""
+        """Lists all hosts.
 
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v2/#list-all-hosts
+        """
         url = 'os-hosts'
         if params:
             url += '?%s' % urllib.urlencode(params)
diff --git a/tempest/lib/services/volume/v2/snapshots_client.py b/tempest/lib/services/volume/v2/snapshots_client.py
index 983ed89..5f4e7de 100644
--- a/tempest/lib/services/volume/v2/snapshots_client.py
+++ b/tempest/lib/services/volume/v2/snapshots_client.py
@@ -124,7 +124,12 @@
         return rest_client.ResponseBody(resp, body)
 
     def create_snapshot_metadata(self, snapshot_id, metadata):
-        """Create metadata for the snapshot."""
+        """Create metadata for the snapshot.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        http://developer.openstack.org/api-ref/block-storage/v2/#create-snapshot-metadata
+        """
         put_body = json.dumps({'metadata': metadata})
         url = "snapshots/%s/metadata" % snapshot_id
         resp, body = self.post(url, put_body)
diff --git a/tempest/lib/services/volume/v2/volumes_client.py b/tempest/lib/services/volume/v2/volumes_client.py
index f4e7c6a..86e3836 100644
--- a/tempest/lib/services/volume/v2/volumes_client.py
+++ b/tempest/lib/services/volume/v2/volumes_client.py
@@ -72,6 +72,10 @@
         """List all the volumes created.
 
         Params can be a string (must be urlencoded) or a dictionary.
+        For a full list of available parameters, please refer to the official
+        API reference:
+        http://developer.openstack.org/api-ref/block-storage/v2/#list-volumes-with-details
+        http://developer.openstack.org/api-ref/block-storage/v2/#list-volumes
         """
         url = 'volumes'
         if detail:
@@ -155,7 +159,12 @@
         return rest_client.ResponseBody(resp, body)
 
     def set_bootable_volume(self, volume_id, **kwargs):
-        """set a bootable flag for a volume - true or false."""
+        """Set a bootable flag for a volume - true or false.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        http://developer.openstack.org/api-ref/block-storage/v2/#update-volume-bootable-status
+        """
         post_body = json.dumps({'os-set_bootable': kwargs})
         url = 'volumes/%s/action' % (volume_id)
         resp, body = self.post(url, post_body)
@@ -239,7 +248,12 @@
         return rest_client.ResponseBody(resp, body)
 
     def create_volume_metadata(self, volume_id, metadata):
-        """Create metadata for the volume."""
+        """Create metadata for the volume.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        http://developer.openstack.org/api-ref/block-storage/v2/#create-volume-metadata
+        """
         put_body = json.dumps({'metadata': metadata})
         url = "volumes/%s/metadata" % volume_id
         resp, body = self.post(url, put_body)
@@ -256,7 +270,12 @@
         return rest_client.ResponseBody(resp, body)
 
     def update_volume_metadata(self, volume_id, metadata):
-        """Update metadata for the volume."""
+        """Update metadata for the volume.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        http://developer.openstack.org/api-ref/block-storage/v2/#update-volume-metadata
+        """
         put_body = json.dumps({'metadata': metadata})
         url = "volumes/%s/metadata" % volume_id
         resp, body = self.put(url, put_body)
@@ -281,7 +300,12 @@
         return rest_client.ResponseBody(resp, body)
 
     def retype_volume(self, volume_id, **kwargs):
-        """Updates volume with new volume type."""
+        """Updates volume with new volume type.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v2/#retype-volume
+        """
         post_body = json.dumps({'os-retype': kwargs})
         resp, body = self.post('volumes/%s/action' % volume_id, post_body)
         self.expected_success(202, resp.status)
diff --git a/tempest/lib/services/volume/v3/__init__.py b/tempest/lib/services/volume/v3/__init__.py
index 07ae917..a351d61 100644
--- a/tempest/lib/services/volume/v3/__init__.py
+++ b/tempest/lib/services/volume/v3/__init__.py
@@ -13,8 +13,11 @@
 # the License.
 
 from tempest.lib.services.volume.v3.base_client import BaseClient
+from tempest.lib.services.volume.v3.group_types_client import GroupTypesClient
+from tempest.lib.services.volume.v3.groups_client import GroupsClient
 from tempest.lib.services.volume.v3.messages_client import MessagesClient
 from tempest.lib.services.volume.v3.versions_client import VersionsClient
 from tempest.lib.services.volume.v3.volumes_client import VolumesClient
 
-__all__ = ['MessagesClient', 'BaseClient', 'VersionsClient', 'VolumesClient']
+__all__ = ['BaseClient', 'GroupsClient', 'GroupTypesClient',
+           'MessagesClient', 'VersionsClient', 'VolumesClient']
diff --git a/tempest/lib/services/volume/v3/group_types_client.py b/tempest/lib/services/volume/v3/group_types_client.py
new file mode 100644
index 0000000..390d44d
--- /dev/null
+++ b/tempest/lib/services/volume/v3/group_types_client.py
@@ -0,0 +1,47 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
+from tempest.lib.services.volume.v3 import base_client
+
+
+class GroupTypesClient(base_client.BaseClient):
+    """Client class to send CRUD Volume V3 Group Types API requests"""
+
+    @property
+    def resource_type(self):
+        """Returns the primary type of resource this client works with."""
+        return 'group-type'
+
+    def create_group_type(self, **kwargs):
+        """Create group_type.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#create-group-type
+        """
+        post_body = json.dumps({'group_type': kwargs})
+        resp, body = self.post('group_types', post_body)
+        body = json.loads(body)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_group_type(self, group_type_id):
+        """Deletes the specified group_type."""
+        resp, body = self.delete("group_types/%s" % group_type_id)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
diff --git a/tempest/lib/services/volume/v3/groups_client.py b/tempest/lib/services/volume/v3/groups_client.py
new file mode 100644
index 0000000..c06997a
--- /dev/null
+++ b/tempest/lib/services/volume/v3/groups_client.py
@@ -0,0 +1,96 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions as lib_exc
+from tempest.lib.services.volume.v3 import base_client
+
+
+class GroupsClient(base_client.BaseClient):
+    """Client class to send CRUD Volume Group API requests"""
+
+    def create_group(self, **kwargs):
+        """Creates a group.
+
+        group_type and volume_types are required parameters in kwargs.
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#create-group
+        """
+        post_body = json.dumps({'group': kwargs})
+        resp, body = self.post('groups', post_body)
+        body = json.loads(body)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def delete_group(self, group_id, delete_volumes=True):
+        """Deletes a group.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#delete-group
+        """
+        post_body = {'delete-volumes': delete_volumes}
+        post_body = json.dumps({'delete': post_body})
+        resp, body = self.post('groups/%s/action' % group_id,
+                               post_body)
+        self.expected_success(202, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def show_group(self, group_id):
+        """Returns the details of a single group.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#show-group-details
+        """
+        url = "groups/%s" % str(group_id)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def list_groups(self, detail=False, **params):
+        """Lists information for all the tenant's groups.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#list-groups
+        https://developer.openstack.org/api-ref/block-storage/v3/#list-groups-with-details
+        """
+        url = "groups"
+        if detail:
+            url += "/detail"
+        if params:
+            url += '?%s' % urllib.urlencode(params)
+        resp, body = self.get(url)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def is_resource_deleted(self, id):
+        try:
+            self.show_group(id)
+        except lib_exc.NotFound:
+            return True
+        return False
+
+    @property
+    def resource_type(self):
+        """Returns the primary type of resource this client works with."""
+        return 'group'
diff --git a/tempest/tests/cmd/test_verify_tempest_config.py b/tempest/tests/cmd/test_verify_tempest_config.py
index 640dcd4..b0e74fb 100644
--- a/tempest/tests/cmd/test_verify_tempest_config.py
+++ b/tempest/tests/cmd/test_verify_tempest_config.py
@@ -199,7 +199,9 @@
         with mock.patch.object(verify_tempest_config,
                                'print_and_or_update') as print_mock:
             verify_tempest_config.verify_cinder_api_versions(fake_os, True)
-        print_mock.assert_not_called()
+        print_mock.assert_any_call('api_v3', 'volume-feature-enabled',
+                                   False, True)
+        self.assertEqual(1, print_mock.call_count)
 
     @mock.patch('tempest.lib.common.http.ClosingHttp.request')
     def test_verify_cinder_api_versions_no_v2(self, mock_request):
@@ -215,9 +217,7 @@
             verify_tempest_config.verify_cinder_api_versions(fake_os, True)
         print_mock.assert_any_call('api_v2', 'volume-feature-enabled',
                                    False, True)
-        print_mock.assert_any_call('api_v3', 'volume-feature-enabled',
-                                   True, True)
-        self.assertEqual(2, print_mock.call_count)
+        self.assertEqual(1, print_mock.call_count)
 
     @mock.patch('tempest.lib.common.http.ClosingHttp.request')
     def test_verify_cinder_api_versions_no_v1(self, mock_request):
@@ -231,9 +231,7 @@
         with mock.patch.object(verify_tempest_config,
                                'print_and_or_update') as print_mock:
             verify_tempest_config.verify_cinder_api_versions(fake_os, True)
-        print_mock.assert_any_call('api_v3', 'volume-feature-enabled',
-                                   True, True)
-        self.assertEqual(1, print_mock.call_count)
+        print_mock.assert_not_called()
 
     def test_verify_glance_version_no_v2_with_v1_1(self):
         def fake_get_versions():
diff --git a/tempest/tests/common/test_compute.py b/tempest/tests/common/test_compute.py
new file mode 100644
index 0000000..c108be9
--- /dev/null
+++ b/tempest/tests/common/test_compute.py
@@ -0,0 +1,106 @@
+# Copyright 2017 Citrix Systems
+# 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 six.moves.urllib import parse as urlparse
+
+import mock
+
+from tempest.common import compute
+from tempest.tests import base
+
+
+class TestCompute(base.TestCase):
+    def setUp(self):
+        super(TestCompute, self).setUp()
+        self.client_sock = mock.Mock()
+        self.url = urlparse.urlparse("http://www.fake.com:80")
+
+    def test_rfp_frame_not_cached(self):
+        # rfp negotiation frame arrived separately after upgrade
+        # response, so it's not cached.
+        RFP_VERSION = b'RFB.003.003\x0a'
+        rfp_frame_header = b'\x82\x0c'
+
+        self.client_sock.recv.side_effect = [
+            b'fake response start\r\n',
+            b'fake response end\r\n\r\n',
+            rfp_frame_header,
+            RFP_VERSION]
+        expect_response = b'fake response start\r\nfake response end\r\n\r\n'
+
+        webSocket = compute._WebSocket(self.client_sock, self.url)
+
+        self.assertEqual(webSocket.response, expect_response)
+        # no cache
+        self.assertEqual(webSocket.cached_stream, b'')
+        self.client_sock.recv.assert_has_calls([mock.call(4096),
+                                                mock.call(4096)])
+
+        self.client_sock.recv.reset_mock()
+        recv_version = webSocket.receive_frame()
+
+        self.assertEqual(recv_version, RFP_VERSION)
+        self.client_sock.recv.assert_has_calls([mock.call(2),
+                                                mock.call(12)])
+
+    def test_rfp_frame_fully_cached(self):
+        RFP_VERSION = b'RFB.003.003\x0a'
+        rfp_version_frame = b'\x82\x0c%s' % RFP_VERSION
+
+        self.client_sock.recv.side_effect = [
+            b'fake response start\r\n',
+            b'fake response end\r\n\r\n%s' % rfp_version_frame]
+        expect_response = b'fake response start\r\nfake response end\r\n\r\n'
+        webSocket = compute._WebSocket(self.client_sock, self.url)
+
+        self.client_sock.recv.assert_has_calls([mock.call(4096),
+                                                mock.call(4096)])
+        self.assertEqual(webSocket.response, expect_response)
+        self.assertEqual(webSocket.cached_stream, rfp_version_frame)
+
+        self.client_sock.recv.reset_mock()
+        recv_version = webSocket.receive_frame()
+
+        self.client_sock.recv.assert_not_called()
+        self.assertEqual(recv_version, RFP_VERSION)
+        # cached_stream should be empty in the end.
+        self.assertEqual(webSocket.cached_stream, b'')
+
+    def test_rfp_frame_partially_cached(self):
+        RFP_VERSION = b'RFB.003.003\x0a'
+        rfp_version_frame = b'\x82\x0c%s' % RFP_VERSION
+        frame_part1 = rfp_version_frame[:6]
+        frame_part2 = rfp_version_frame[6:]
+
+        self.client_sock.recv.side_effect = [
+            b'fake response start\r\n',
+            b'fake response end\r\n\r\n%s' % frame_part1,
+            frame_part2]
+        expect_response = b'fake response start\r\nfake response end\r\n\r\n'
+        webSocket = compute._WebSocket(self.client_sock, self.url)
+
+        self.client_sock.recv.assert_has_calls([mock.call(4096),
+                                                mock.call(4096)])
+        self.assertEqual(webSocket.response, expect_response)
+        self.assertEqual(webSocket.cached_stream, frame_part1)
+
+        self.client_sock.recv.reset_mock()
+
+        recv_version = webSocket.receive_frame()
+
+        self.client_sock.recv.assert_called_once_with(len(frame_part2))
+        self.assertEqual(recv_version, RFP_VERSION)
+        # cached_stream should be empty in the end.
+        self.assertEqual(webSocket.cached_stream, b'')
diff --git a/tempest/tests/lib/services/network/test_base_network_client.py b/tempest/tests/lib/services/network/test_base_network_client.py
new file mode 100644
index 0000000..e121cec
--- /dev/null
+++ b/tempest/tests/lib/services/network/test_base_network_client.py
@@ -0,0 +1,96 @@
+# Copyright 2017 AT&T Corporation.
+# All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import mock
+
+from tempest.lib.services.network import base as base_network_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib import fake_http
+from tempest.tests.lib.services import base
+
+
+class TestBaseNetworkClient(base.BaseServiceTest):
+
+    def setUp(self):
+        super(TestBaseNetworkClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = base_network_client.BaseNetworkClient(
+            fake_auth, 'compute', 'regionOne')
+
+        self.mock_expected_success = mock.patch.object(
+            self.client, 'expected_success').start()
+
+    def _assert_empty(self, resp):
+        self.assertEqual([], list(resp.keys()))
+
+    @mock.patch('tempest.lib.common.rest_client.RestClient.post')
+    def test_create_resource(self, mock_post):
+        response = fake_http.fake_http_response(headers=None, status=201)
+        mock_post.return_value = response, '{"baz": "qux"}'
+
+        post_data = {'foo': 'bar'}
+        resp = self.client.create_resource('/fake_url', post_data)
+
+        self.assertEqual({'status': '201'}, resp.response)
+        self.assertEqual("qux", resp["baz"])
+        mock_post.assert_called_once_with('v2.0/fake_url', '{"foo": "bar"}')
+        self.mock_expected_success.assert_called_once_with(
+            201, 201)
+
+    @mock.patch('tempest.lib.common.rest_client.RestClient.post')
+    def test_create_resource_expect_different_values(self, mock_post):
+        response = fake_http.fake_http_response(headers=None, status=200)
+        mock_post.return_value = response, '{}'
+
+        post_data = {'foo': 'bar'}
+        resp = self.client.create_resource('/fake_url', post_data,
+                                           expect_response_code=200,
+                                           expect_empty_body=True)
+
+        self.assertEqual({'status': '200'}, resp.response)
+        self._assert_empty(resp)
+        mock_post.assert_called_once_with('v2.0/fake_url', '{"foo": "bar"}')
+        self.mock_expected_success.assert_called_once_with(
+            200, 200)
+
+    @mock.patch('tempest.lib.common.rest_client.RestClient.put')
+    def test_update_resource(self, mock_put):
+        response = fake_http.fake_http_response(headers=None, status=200)
+        mock_put.return_value = response, '{"baz": "qux"}'
+
+        put_data = {'foo': 'bar'}
+        resp = self.client.update_resource('/fake_url', put_data)
+
+        self.assertEqual({'status': '200'}, resp.response)
+        self.assertEqual("qux", resp["baz"])
+        mock_put.assert_called_once_with('v2.0/fake_url', '{"foo": "bar"}')
+        self.mock_expected_success.assert_called_once_with(
+            200, 200)
+
+    @mock.patch('tempest.lib.common.rest_client.RestClient.put')
+    def test_update_resource_expect_different_values(self, mock_put):
+        response = fake_http.fake_http_response(headers=None, status=201)
+        mock_put.return_value = response, '{}'
+
+        put_data = {'foo': 'bar'}
+        resp = self.client.update_resource('/fake_url', put_data,
+                                           expect_response_code=201,
+                                           expect_empty_body=True)
+
+        self.assertEqual({'status': '201'}, resp.response)
+        self._assert_empty(resp)
+        mock_put.assert_called_once_with('v2.0/fake_url', '{"foo": "bar"}')
+        self.mock_expected_success.assert_called_once_with(
+            201, 201)
diff --git a/tempest/tests/lib/services/volume/v3/test_group_types_client.py b/tempest/tests/lib/services/volume/v3/test_group_types_client.py
new file mode 100644
index 0000000..95498c7
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_group_types_client.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+#
+# 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.lib.services.volume.v3 import group_types_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestGroupTypesClient(base.BaseServiceTest):
+    FAKE_CREATE_GROUP_TYPE = {
+        "group_type": {
+            "name": "group-type-001",
+            "description": "Test group type 1",
+            "group_specs": {},
+            "is_public": True,
+        }
+    }
+
+    def setUp(self):
+        super(TestGroupTypesClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = group_types_client.GroupTypesClient(fake_auth,
+                                                          'volume',
+                                                          'regionOne')
+
+    def _test_create_group_type(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_group_type,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_CREATE_GROUP_TYPE,
+            bytes_body,
+            status=202)
+
+    def test_create_group_type_with_str_body(self):
+        self._test_create_group_type()
+
+    def test_create_group_type_with_bytes_body(self):
+        self._test_create_group_type(bytes_body=True)
+
+    def test_delete_group_type(self):
+        self.check_service_client_function(
+            self.client.delete_group_type,
+            'tempest.lib.common.rest_client.RestClient.delete',
+            {},
+            group_type_id='0e58433f-d108-4bf3-a22c-34e6b71ef86b',
+            status=202)
diff --git a/tempest/tests/lib/services/volume/v3/test_groups_client.py b/tempest/tests/lib/services/volume/v3/test_groups_client.py
new file mode 100644
index 0000000..00db5b4
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_groups_client.py
@@ -0,0 +1,136 @@
+# Copyright (C) 2017 Dell Inc. or its subsidiaries.
+#
+# 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.lib.services.volume.v3 import groups_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestGroupsClient(base.BaseServiceTest):
+    FAKE_CREATE_GROUP = {
+        "group": {
+            "name": "group-001",
+            "description": "Test group 1",
+            "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+            "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+            "availability_zone": "az1",
+        }
+    }
+
+    FAKE_INFO_GROUP = {
+        "group": {
+            "id": "0e701ab8-1bec-4b9f-b026-a7ba4af13578",
+            "name": "group-001",
+            "description": "Test group 1",
+            "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+            "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+            "status": "available",
+            "availability_zone": "az1",
+            "created_at": "20127-06-20T03:50:07Z"
+        }
+    }
+
+    FAKE_LIST_GROUPS = {
+        "groups": [
+            {
+                "id": "0e701ab8-1bec-4b9f-b026-a7ba4af13578",
+                "name": "group-001",
+                "description": "Test group 1",
+                "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+                "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+                "status": "available",
+                "availability_zone": "az1",
+                "created_at": "2017-06-20T03:50:07Z",
+            },
+            {
+                "id": "e479997c-650b-40a4-9dfe-77655818b0d2",
+                "name": "group-002",
+                "description": "Test group 2",
+                "group_snapshot_id": "79c9afdb-7e46-4d71-9249-1f022886963c",
+                "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+                "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+                "status": "available",
+                "availability_zone": "az1",
+                "created_at": "2017-06-19T01:52:47Z",
+            },
+            {
+                "id": "c5c4769e-213c-40a6-a568-8e797bb691d4",
+                "name": "group-003",
+                "description": "Test group 3",
+                "source_group_id": "e92f9dc7-0b20-492d-8ab2-3ad8fdac270e",
+                "group_type": "0e58433f-d108-4bf3-a22c-34e6b71ef86b",
+                "volume_types": ["2103099d-7cc3-4e52-a2f1-23a5284416f3"],
+                "status": "available",
+                "availability_zone": "az1",
+                "created_at": "2017-06-18T06:34:32Z",
+            }
+        ]
+    }
+
+    def setUp(self):
+        super(TestGroupsClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = groups_client.GroupsClient(fake_auth,
+                                                 'volume',
+                                                 'regionOne')
+
+    def _test_create_group(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.create_group,
+            'tempest.lib.common.rest_client.RestClient.post',
+            self.FAKE_CREATE_GROUP,
+            bytes_body,
+            status=202)
+
+    def _test_show_group(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.show_group,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_INFO_GROUP,
+            bytes_body,
+            group_id="3fbbcccf-d058-4502-8844-6feeffdf4cb5")
+
+    def _test_list_groups(self, bytes_body=False):
+        self.check_service_client_function(
+            self.client.list_groups,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_LIST_GROUPS,
+            bytes_body,
+            detail=True)
+
+    def test_create_group_with_str_body(self):
+        self._test_create_group()
+
+    def test_create_group_with_bytes_body(self):
+        self._test_create_group(bytes_body=True)
+
+    def test_show_group_with_str_body(self):
+        self._test_show_group()
+
+    def test_show_group_with_bytes_body(self):
+        self._test_show_group(bytes_body=True)
+
+    def test_list_groups_with_str_body(self):
+        self._test_list_groups()
+
+    def test_list_groups_with_bytes_body(self):
+        self._test_list_groups(bytes_body=True)
+
+    def test_delete_group(self):
+        self.check_service_client_function(
+            self.client.delete_group,
+            'tempest.lib.common.rest_client.RestClient.post',
+            {},
+            group_id='0e701ab8-1bec-4b9f-b026-a7ba4af13578',
+            status=202)