Add extra apis to volume v3 services client

Just like compute services client (Nova), volume services client (Cinder)
also has some extra apis, such as 'enable_service', 'disable_service',
'disable_log_reason', 'freeze_host' and 'thaw_host'. This patch supplements
these five apis to volume v3 services client.

As it maybe dangerous for Tempest gate jobs to test these apis, only some
negative tests are provided.

Including:

[1] Add the apis to volume v3 services_client
[2] Add unit tests for these apis
[3] Add release note
[4] Add negative tests

Change-Id: Ic7c170122321483a89d399f67ce4441b00dfc781
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/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/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/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)