Merge "Add status check for creating volume backup"
diff --git a/releasenotes/notes/add-extra-apis-to-volume-v3-services-client-bf9b235cf5a611fe.yaml b/releasenotes/notes/add-extra-apis-to-volume-v3-services-client-bf9b235cf5a611fe.yaml
new file mode 100644
index 0000000..03d0ae8
--- /dev/null
+++ b/releasenotes/notes/add-extra-apis-to-volume-v3-services-client-bf9b235cf5a611fe.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Add ``enable_service``, ``disable_service`` , ``disable_log_reason``,
+    ``freeze_host`` and ``thaw_host`` API endpoints to volume v3
+    ``services_client``.
diff --git a/releasenotes/notes/add-port-profile-config-option-2610b2fa67027960.yaml b/releasenotes/notes/add-port-profile-config-option-2610b2fa67027960.yaml
index b54ee8b..19d47d1 100644
--- a/releasenotes/notes/add-port-profile-config-option-2610b2fa67027960.yaml
+++ b/releasenotes/notes/add-port-profile-config-option-2610b2fa67027960.yaml
@@ -1,11 +1,9 @@
 ---
-prelude: >
-    When using OVS HW offload feature we need to create
-    Neutron port with a certain capability. This is done
-    by creating Neutron port with binding profile. To be
-    able to test this we need profile capability support
-    in Tempest as well.
 features:
   - A new config option 'port_profile' is added to the section
     'network' to specify capabilities of the port.
-    By default this is set to {}.
+    By default this is set to {}. When using OVS HW offload
+    feature we need to create Neutron port with a certain
+    capability. This is done by creating Neutron port with
+    binding profile. To be able to test this we need profile
+    capability support in Tempest as well.
diff --git a/tempest/api/volume/admin/test_group_snapshots.py b/tempest/api/volume/admin/test_group_snapshots.py
index 45f4caa..731a055 100644
--- a/tempest/api/volume/admin/test_group_snapshots.py
+++ b/tempest/api/volume/admin/test_group_snapshots.py
@@ -157,6 +157,57 @@
         waiters.wait_for_volume_resource_status(
             self.groups_client, grp2['id'], 'available')
 
+    @decorators.idempotent_id('7d7fc000-0b4c-4376-a372-544116d2e127')
+    @decorators.related_bug('1739031')
+    def test_delete_group_snapshots_following_updated_volumes(self):
+        volume_type = self.create_volume_type()
+
+        group_type = self.create_group_type()
+
+        # Create a volume group
+        grp = self.create_group(group_type=group_type['id'],
+                                volume_types=[volume_type['id']])
+
+        # Note: When dealing with consistency groups all volumes must
+        # reside on the same backend. Adding volumes to the same consistency
+        # group from multiple backends isn't supported. In order to ensure all
+        # volumes share the same backend, all volumes must share same
+        # volume-type and group id.
+        volume_list = []
+        for _ in range(2):
+            volume = self.create_volume(volume_type=volume_type['id'],
+                                        group_id=grp['id'])
+            volume_list.append(volume['id'])
+
+        for vol in volume_list:
+            self.groups_client.update_group(grp['id'],
+                                            remove_volumes=vol)
+            waiters.wait_for_volume_resource_status(
+                self.groups_client, grp['id'], 'available')
+
+            self.groups_client.update_group(grp['id'],
+                                            add_volumes=vol)
+            waiters.wait_for_volume_resource_status(
+                self.groups_client, grp['id'], 'available')
+
+        # Verify the created volumes are associated with consistency group
+        vols = self.volumes_client.list_volumes(detail=True)['volumes']
+        grp_vols = [v for v in vols if v['group_id'] == grp['id']]
+        self.assertEqual(2, len(grp_vols))
+
+        # Create a snapshot group
+        group_snapshot = self._create_group_snapshot(group_id=grp['id'])
+        snapshots = self.snapshots_client.list_snapshots(
+            detail=True)['snapshots']
+
+        for snap in snapshots:
+            if snap['volume_id'] in volume_list:
+                waiters.wait_for_volume_resource_status(
+                    self.snapshots_client, snap['id'], 'available')
+
+        # Delete a snapshot group
+        self._delete_group_snapshot(group_snapshot)
+
 
 class GroupSnapshotsV319Test(BaseGroupSnapshotsTest):
     _api_version = 3
diff --git a/tempest/api/volume/admin/test_volume_services_negative.py b/tempest/api/volume/admin/test_volume_services_negative.py
new file mode 100644
index 0000000..6f3dbc6
--- /dev/null
+++ b/tempest/api/volume/admin/test_volume_services_negative.py
@@ -0,0 +1,65 @@
+# Copyright 2018 FiberHome Telecommunication Technologies CO.,LTD
+# 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.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+
+class VolumeServicesNegativeTest(base.BaseVolumeAdminTest):
+
+    @classmethod
+    def resource_setup(cls):
+        super(VolumeServicesNegativeTest, cls).resource_setup()
+        cls.services = cls.admin_volume_services_client.list_services()[
+            'services']
+        cls.host = cls.services[0]['host']
+        cls.binary = cls.services[0]['binary']
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('3246ce65-ba70-4159-aa3b-082c28e4b484')
+    def test_enable_service_with_invalid_host(self):
+        self.assertRaises(lib_exc.NotFound,
+                          self.admin_volume_services_client.enable_service,
+                          host='invalid_host', binary=self.binary)
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('c571f179-c6e6-4c50-a0ab-368b628a8ac1')
+    def test_disable_service_with_invalid_binary(self):
+        self.assertRaises(lib_exc.NotFound,
+                          self.admin_volume_services_client.disable_service,
+                          host=self.host, binary='invalid_binary')
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('77767b36-5e8f-4c68-a0b5-2308cc21ec64')
+    def test_disable_log_reason_with_no_reason(self):
+        self.assertRaises(lib_exc.BadRequest,
+                          self.admin_volume_services_client.disable_log_reason,
+                          host=self.host, binary=self.binary,
+                          disabled_reason=None)
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('712bfab8-1f44-4eb5-a632-fa70bf78f05e')
+    def test_freeze_host_with_invalid_host(self):
+        self.assertRaises(lib_exc.BadRequest,
+                          self.admin_volume_services_client.freeze_host,
+                          host='invalid_host')
+
+    @decorators.attr(type='negative')
+    @decorators.idempotent_id('7c6287c9-d655-47e1-9a11-76f6657a6dce')
+    def test_thaw_host_with_invalid_host(self):
+        self.assertRaises(lib_exc.BadRequest,
+                          self.admin_volume_services_client.thaw_host,
+                          host='invalid_host')
diff --git a/tempest/api/volume/admin/test_volumes_backup.py b/tempest/api/volume/admin/test_volumes_backup.py
index 3c23718..45060d0 100644
--- a/tempest/api/volume/admin/test_volumes_backup.py
+++ b/tempest/api/volume/admin/test_volumes_backup.py
@@ -107,16 +107,18 @@
         self.addCleanup(self.volumes_client.delete_volume,
                         restore['volume_id'])
         self.assertEqual(backup['id'], restore['backup_id'])
-        waiters.wait_for_volume_resource_status(self.volumes_client,
-                                                restore['volume_id'],
-                                                'available')
+
+        # When restore operation is performed then, backup['id']
+        # goes to 'restoring' state so we need to wait for
+        # backup['id'] to become 'available'.
+        waiters.wait_for_volume_resource_status(
+            self.backups_client, backup['id'], 'available')
+        waiters.wait_for_volume_resource_status(
+            self.volumes_client, restore['volume_id'], 'available')
 
         # Verify if restored volume is there in volume list
         volumes = self.volumes_client.list_volumes()['volumes']
         self.assertIn(restore['volume_id'], [v['id'] for v in volumes])
-        waiters.wait_for_volume_resource_status(self.admin_backups_client,
-                                                import_backup['id'],
-                                                'available')
 
     @decorators.idempotent_id('47a35425-a891-4e13-961c-c45deea21e94')
     def test_volume_backup_reset_status(self):
diff --git a/tempest/lib/services/volume/v1/encryption_types_client.py b/tempest/lib/services/volume/v1/encryption_types_client.py
index 0fac6bd..1fde79f 100644
--- a/tempest/lib/services/volume/v1/encryption_types_client.py
+++ b/tempest/lib/services/volume/v1/encryption_types_client.py
@@ -38,7 +38,7 @@
     def show_encryption_type(self, volume_type_id):
         """Get the volume encryption type for the specified volume type.
 
-        volume_type_id: Id of volume_type.
+        :param volume_type_id: Id of volume type.
         """
         url = "/types/%s/encryption" % volume_type_id
         resp, body = self.get(url)
@@ -61,7 +61,7 @@
         return rest_client.ResponseBody(resp, body)
 
     def delete_encryption_type(self, volume_type_id):
-        """Delete the encryption type for the specified volume-type."""
+        """Delete the encryption type for the specified volume type."""
         resp, body = self.delete(
             "/types/%s/encryption/provider" % volume_type_id)
         self.expected_success(202, resp.status)
diff --git a/tempest/lib/services/volume/v3/encryption_types_client.py b/tempest/lib/services/volume/v3/encryption_types_client.py
index 7443a87..03de187 100644
--- a/tempest/lib/services/volume/v3/encryption_types_client.py
+++ b/tempest/lib/services/volume/v3/encryption_types_client.py
@@ -38,7 +38,7 @@
     def show_encryption_type(self, volume_type_id):
         """Get the volume encryption type for the specified volume type.
 
-        volume_type_id: Id of volume_type.
+        :param volume_type_id: Id of volume type.
         """
         url = "/types/%s/encryption" % volume_type_id
         resp, body = self.get(url)
diff --git a/tempest/lib/services/volume/v3/services_client.py b/tempest/lib/services/volume/v3/services_client.py
index 09036a4..22155a9 100644
--- a/tempest/lib/services/volume/v3/services_client.py
+++ b/tempest/lib/services/volume/v3/services_client.py
@@ -20,9 +20,15 @@
 
 
 class ServicesClient(rest_client.RestClient):
-    """Client class to send CRUD Volume API requests"""
+    """Client class to send CRUD Volume Services API requests"""
 
     def list_services(self, **params):
+        """List all Cinder services.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#list-all-cinder-services
+        """
         url = 'os-services'
         if params:
             url += '?%s' % urllib.urlencode(params)
@@ -31,3 +37,66 @@
         body = json.loads(body)
         self.expected_success(200, resp.status)
         return rest_client.ResponseBody(resp, body)
+
+    def enable_service(self, **kwargs):
+        """Enable service on a host.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#enable-a-cinder-service
+        """
+        put_body = json.dumps(kwargs)
+        resp, body = self.put('os-services/enable', put_body)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def disable_service(self, **kwargs):
+        """Disable service on a host.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#disable-a-cinder-service
+        """
+        put_body = json.dumps(kwargs)
+        resp, body = self.put('os-services/disable', put_body)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def disable_log_reason(self, **kwargs):
+        """Disable scheduling for a volume service and log disabled reason.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#log-disabled-cinder-service-information
+        """
+        put_body = json.dumps(kwargs)
+        resp, body = self.put('os-services/disable-log-reason', put_body)
+        body = json.loads(body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp, body)
+
+    def freeze_host(self, **kwargs):
+        """Freeze a Cinder backend host.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#freeze-a-cinder-backend-host
+        """
+        put_body = json.dumps(kwargs)
+        resp, _ = self.put('os-services/freeze', put_body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp)
+
+    def thaw_host(self, **kwargs):
+        """Thaw a Cinder backend host.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#thaw-a-cinder-backend-host
+        """
+        put_body = json.dumps(kwargs)
+        resp, _ = self.put('os-services/thaw', put_body)
+        self.expected_success(200, resp.status)
+        return rest_client.ResponseBody(resp)
diff --git a/tempest/lib/services/volume/v3/snapshots_client.py b/tempest/lib/services/volume/v3/snapshots_client.py
index 298925a..f79bcd8 100644
--- a/tempest/lib/services/volume/v3/snapshots_client.py
+++ b/tempest/lib/services/volume/v3/snapshots_client.py
@@ -176,11 +176,12 @@
         return rest_client.ResponseBody(resp, body)
 
     def update_snapshot_metadata_item(self, snapshot_id, id, **kwargs):
-        """Update metadata item for the snapshot."""
-        # TODO(piyush): 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.
-        # LP: https://bugs.launchpad.net/openstack-api-site/+bug/1529064
+        """Update metadata for the snapshot for a specific key.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/block-storage/v3/#update-a-snapshot-s-metadata-for-a-specific-key
+        """
         put_body = json.dumps(kwargs)
         url = "snapshots/%s/metadata/%s" % (snapshot_id, id)
         resp, body = self.put(url, put_body)
diff --git a/tempest/lib/services/volume/v3/types_client.py b/tempest/lib/services/volume/v3/types_client.py
index 6d9d03a..13ecd15 100644
--- a/tempest/lib/services/volume/v3/types_client.py
+++ b/tempest/lib/services/volume/v3/types_client.py
@@ -21,7 +21,7 @@
 
 
 class TypesClient(rest_client.RestClient):
-    """Client class to send CRUD Volume API requests"""
+    """Client class to send CRUD Volume Types API requests"""
 
     def is_resource_deleted(self, id):
         try:
@@ -36,7 +36,7 @@
         return 'volume-type'
 
     def list_volume_types(self, **params):
-        """List all the volume_types created.
+        """List all the volume types created.
 
         For a full list of available parameters, please refer to the official
         API reference:
@@ -52,7 +52,7 @@
         return rest_client.ResponseBody(resp, body)
 
     def show_volume_type(self, volume_type_id):
-        """Returns the details of a single volume_type.
+        """Returns the details of a single volume type.
 
         For a full list of available parameters, please refer to the official
         API reference:
@@ -78,7 +78,7 @@
         return rest_client.ResponseBody(resp, body)
 
     def delete_volume_type(self, volume_type_id):
-        """Deletes the Specified Volume_type.
+        """Deletes the specified volume type.
 
         For a full list of available parameters, please refer to the official
         API reference:
@@ -89,11 +89,11 @@
         return rest_client.ResponseBody(resp, body)
 
     def list_volume_types_extra_specs(self, volume_type_id, **params):
-        """List all the volume_types extra specs created.
+        """List all the volume type extra specs created.
 
-        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/v3/#show-all-extra-specifications-for-volume-type
         """
         url = 'types/%s/extra_specs' % volume_type_id
         if params:
@@ -105,7 +105,7 @@
         return rest_client.ResponseBody(resp, body)
 
     def show_volume_type_extra_specs(self, volume_type_id, extra_specs_name):
-        """Returns the details of a single volume_type extra spec."""
+        """Returns the details of a single volume type extra spec."""
         url = "types/%s/extra_specs/%s" % (volume_type_id, extra_specs_name)
         resp, body = self.get(url)
         body = json.loads(body)
@@ -113,10 +113,10 @@
         return rest_client.ResponseBody(resp, body)
 
     def create_volume_type_extra_specs(self, volume_type_id, extra_specs):
-        """Creates a new Volume_type extra spec.
+        """Creates new volume type extra specs.
 
-        volume_type_id: Id of volume_type.
-        extra_specs: A dictionary of values to be used as extra_specs.
+        :param volume_type_id: Id of volume type.
+        :param extra_specs: A dictionary of values to be used as extra_specs.
         """
         url = "types/%s/extra_specs" % volume_type_id
         post_body = json.dumps({'extra_specs': extra_specs})
@@ -126,7 +126,7 @@
         return rest_client.ResponseBody(resp, body)
 
     def delete_volume_type_extra_specs(self, volume_type_id, extra_spec_name):
-        """Deletes the Specified Volume_type extra spec."""
+        """Deletes the specified volume type extra spec."""
         resp, body = self.delete("types/%s/extra_specs/%s" % (
             volume_type_id, extra_spec_name))
         self.expected_success(202, resp.status)
@@ -149,10 +149,10 @@
                                        extra_specs):
         """Update a volume_type extra spec.
 
-        volume_type_id: Id of volume_type.
-        extra_spec_name: Name of the extra spec to be updated.
-        extra_spec: A dictionary of with key as extra_spec_name and the
-                     updated value.
+        :param volume_type_id: Id of volume type.
+        :param extra_spec_name: Name of the extra spec to be updated.
+        :param extra_specs: A dictionary of with key as extra_spec_name and the
+                            updated value.
         For a full list of available parameters, please refer to the official
         API reference:
         https://developer.openstack.org/api-ref/block-storage/v3/index.html#update-extra-specification-for-volume-type
diff --git a/tempest/tests/lib/services/volume/v3/test_services_client.py b/tempest/tests/lib/services/volume/v3/test_services_client.py
new file mode 100644
index 0000000..f65228f
--- /dev/null
+++ b/tempest/tests/lib/services/volume/v3/test_services_client.py
@@ -0,0 +1,214 @@
+# Copyright 2018 FiberHome Telecommunication Technologies CO.,LTD
+# 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 copy
+
+import mock
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.services.volume.v3 import services_client
+from tempest.tests.lib import fake_auth_provider
+from tempest.tests.lib.services import base
+
+
+class TestServicesClient(base.BaseServiceTest):
+
+    FAKE_SERVICE_LIST = {
+        "services": [
+            {
+                "status": "enabled",
+                "binary": "cinder-backup",
+                "zone": "nova",
+                "state": "up",
+                "updated_at": "2017-07-20T07:20:17.000000",
+                "host": "fake-host",
+                "disabled_reason": None
+            },
+            {
+                "status": "enabled",
+                "binary": "cinder-scheduler",
+                "zone": "nova",
+                "state": "up",
+                "updated_at": "2017-07-20T07:20:24.000000",
+                "host": "fake-host",
+                "disabled_reason": None
+            },
+            {
+                "status": "enabled",
+                "binary": "cinder-volume",
+                "zone": "nova",
+                "frozen": False,
+                "state": "up",
+                "updated_at": "2017-07-20T07:20:20.000000",
+                "host": "fake-host@lvm",
+                "replication_status": "disabled",
+                "active_backend_id": None,
+                "disabled_reason": None
+            }
+        ]
+    }
+
+    FAKE_SERVICE_REQUEST = {
+        "host": "fake-host",
+        "binary": "cinder-volume"
+    }
+
+    FAKE_SERVICE_RESPONSE = {
+        "disabled": False,
+        "status": "enabled",
+        "host": "fake-host@lvm",
+        "service": "",
+        "binary": "cinder-volume",
+        "disabled_reason": None
+    }
+
+    def setUp(self):
+        super(TestServicesClient, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = services_client.ServicesClient(fake_auth,
+                                                     'volume',
+                                                     'regionOne')
+
+    def _test_list_services(self, bytes_body=False,
+                            mock_args='os-services', **params):
+        self.check_service_client_function(
+            self.client.list_services,
+            'tempest.lib.common.rest_client.RestClient.get',
+            self.FAKE_SERVICE_LIST,
+            to_utf=bytes_body,
+            mock_args=[mock_args],
+            **params)
+
+    def _test_enable_service(self, bytes_body=False):
+        resp_body = self.FAKE_SERVICE_RESPONSE
+        kwargs = self.FAKE_SERVICE_REQUEST
+        payload = json.dumps(kwargs, sort_keys=True)
+        json_dumps = json.dumps
+
+        # NOTE: Use sort_keys for json.dumps so that the expected and actual
+        # payloads are guaranteed to be identical for mock_args assert check.
+        with mock.patch.object(services_client.json, 'dumps') as mock_dumps:
+            mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+            self.check_service_client_function(
+                self.client.enable_service,
+                'tempest.lib.common.rest_client.RestClient.put',
+                resp_body,
+                to_utf=bytes_body,
+                mock_args=['os-services/enable', payload],
+                **kwargs)
+
+    def _test_disable_service(self, bytes_body=False):
+        resp_body = copy.deepcopy(self.FAKE_SERVICE_RESPONSE)
+        resp_body.pop('disabled_reason')
+        resp_body['disabled'] = True
+        resp_body['status'] = 'disabled'
+        kwargs = self.FAKE_SERVICE_REQUEST
+        payload = json.dumps(kwargs, sort_keys=True)
+        json_dumps = json.dumps
+
+        # NOTE: Use sort_keys for json.dumps so that the expected and actual
+        # payloads are guaranteed to be identical for mock_args assert check.
+        with mock.patch.object(services_client.json, 'dumps') as mock_dumps:
+            mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+            self.check_service_client_function(
+                self.client.disable_service,
+                'tempest.lib.common.rest_client.RestClient.put',
+                resp_body,
+                to_utf=bytes_body,
+                mock_args=['os-services/disable', payload],
+                **kwargs)
+
+    def _test_disable_log_reason(self, bytes_body=False):
+        resp_body = copy.deepcopy(self.FAKE_SERVICE_RESPONSE)
+        resp_body['disabled_reason'] = "disabled for test"
+        resp_body['disabled'] = True
+        resp_body['status'] = 'disabled'
+        kwargs = copy.deepcopy(self.FAKE_SERVICE_REQUEST)
+        kwargs.update({"disabled_reason": "disabled for test"})
+        payload = json.dumps(kwargs, sort_keys=True)
+        json_dumps = json.dumps
+
+        # NOTE: Use sort_keys for json.dumps so that the expected and actual
+        # payloads are guaranteed to be identical for mock_args assert check.
+        with mock.patch.object(services_client.json, 'dumps') as mock_dumps:
+            mock_dumps.side_effect = lambda d: json_dumps(d, sort_keys=True)
+
+            self.check_service_client_function(
+                self.client.disable_log_reason,
+                'tempest.lib.common.rest_client.RestClient.put',
+                resp_body,
+                to_utf=bytes_body,
+                mock_args=['os-services/disable-log-reason', payload],
+                **kwargs)
+
+    def _test_freeze_host(self, bytes_body=False):
+        kwargs = {'host': 'host1@lvm'}
+        self.check_service_client_function(
+            self.client.freeze_host,
+            'tempest.lib.common.rest_client.RestClient.put',
+            {},
+            bytes_body,
+            **kwargs)
+
+    def _test_thaw_host(self, bytes_body=False):
+        kwargs = {'host': 'host1@lvm'}
+        self.check_service_client_function(
+            self.client.thaw_host,
+            'tempest.lib.common.rest_client.RestClient.put',
+            {},
+            bytes_body,
+            **kwargs)
+
+    def test_list_services_with_str_body(self):
+        self._test_list_services()
+
+    def test_list_services_with_bytes_body(self):
+        self._test_list_services(bytes_body=True)
+
+    def test_list_services_with_params(self):
+        mock_args = 'os-services?host=fake-host'
+        self._test_list_services(mock_args=mock_args, host='fake-host')
+
+    def test_enable_service_with_str_body(self):
+        self._test_enable_service()
+
+    def test_enable_service_with_bytes_body(self):
+        self._test_enable_service(bytes_body=True)
+
+    def test_disable_service_with_str_body(self):
+        self._test_disable_service()
+
+    def test_disable_service_with_bytes_body(self):
+        self._test_disable_service(bytes_body=True)
+
+    def test_disable_log_reason_with_str_body(self):
+        self._test_disable_log_reason()
+
+    def test_disable_log_reason_with_bytes_body(self):
+        self._test_disable_log_reason(bytes_body=True)
+
+    def test_freeze_host_with_str_body(self):
+        self._test_freeze_host()
+
+    def test_freeze_host_with_bytes_body(self):
+        self._test_freeze_host(bytes_body=True)
+
+    def test_thaw_host_with_str_body(self):
+        self._test_thaw_host()
+
+    def test_thaw_host_with_bytes_body(self):
+        self._test_thaw_host(bytes_body=True)