Add update_service to compute services_client library

This patchset adds update_service to compute module's services_client
library. This API is introduced in microversion 2.53 and supersedes
the following APIs:

    * ``PUT /os-services/disable`` (``disable_service``)
    * ``PUT /os-services/disable-log-reason`` (``disable_log_reason``)
    * ``PUT /os-services/enable`` (``enable_service``)
    * ``PUT /os-services/force-down`` (``update_forced_down``)

Negative tests were added for all the APIs above. The negative
tests only test the microversion >= 2.53 case: the new
udpate_service API is called (also with bad parameters).

The v2_11 schema was updated to reference all the unchanged
APIs from v2_1 for the compute services api_schema. Also,
the v2_53 schema was introduced for the new update_service
API and it also references all the unchanged APIs from v2_1.

Finally, unit tests and releasenotes are included.

Change-Id: I5e7b81496cbb87cda81413124b5f82bd5356e666
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index 073a0bb..6dd00d3 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -358,6 +358,10 @@
 
   .. _2.49: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id44
 
+  * `2.53`_
+
+  .. _2.53: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#maximum-in-pike
+
   * `2.54`_
 
   .. _2.54: https://docs.openstack.org/nova/latest/reference/api-microversion-history.html#id49
diff --git a/releasenotes/notes/tempest-lib-compute-update-service-6019d2dcfe4a1c5d.yaml b/releasenotes/notes/tempest-lib-compute-update-service-6019d2dcfe4a1c5d.yaml
new file mode 100644
index 0000000..d67cdb8
--- /dev/null
+++ b/releasenotes/notes/tempest-lib-compute-update-service-6019d2dcfe4a1c5d.yaml
@@ -0,0 +1,11 @@
+---
+features:
+  - |
+    The ``update_service`` API is added to the ``services_client`` compute
+    library. This API is introduced in microversion 2.53 and supersedes
+    the following APIs:
+
+    * ``PUT /os-services/disable`` (``disable_service``)
+    * ``PUT /os-services/disable-log-reason`` (``disable_log_reason``)
+    * ``PUT /os-services/enable`` (``enable_service``)
+    * ``PUT /os-services/force-down`` (``update_forced_down``)
diff --git a/tempest/api/compute/admin/test_services_negative.py b/tempest/api/compute/admin/test_services_negative.py
index 201670a..993c8ec 100644
--- a/tempest/api/compute/admin/test_services_negative.py
+++ b/tempest/api/compute/admin/test_services_negative.py
@@ -13,12 +13,14 @@
 #    under the License.
 
 from tempest.api.compute import base
+from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 from tempest.lib import exceptions as lib_exc
 
 
 class ServicesAdminNegativeTestJSON(base.BaseV2ComputeAdminTest):
     """Tests Services API. List and Enable/Disable require admin privileges."""
+    max_microversion = '2.52'
 
     @classmethod
     def setup_clients(cls):
@@ -35,7 +37,8 @@
     @decorators.attr(type=['negative'])
     @decorators.idempotent_id('d0884a69-f693-4e79-a9af-232d15643bf7')
     def test_get_service_by_invalid_params(self):
-        # return all services if send the request with invalid parameter
+        # Expect all services to be returned when the request contains invalid
+        # parameters.
         services = self.client.list_services()['services']
         services_xxx = (self.client.list_services(xxx='nova-compute')
                         ['services'])
@@ -58,3 +61,45 @@
         services = self.client.list_services(host='xxx',
                                              binary=binary_name)['services']
         self.assertEmpty(services)
+
+
+class ServicesAdminNegativeV253TestJSON(ServicesAdminNegativeTestJSON):
+    min_microversion = '2.53'
+    max_microversion = 'latest'
+
+    # NOTE(felipemonteiro): This class tests the services APIs response schema
+    # for the 2.53 microversion. Schema testing is done for `list_services`
+    # tests.
+
+    @classmethod
+    def resource_setup(cls):
+        super(ServicesAdminNegativeV253TestJSON, cls).resource_setup()
+        # Nova returns 400 if `binary` is not nova-compute.
+        cls.binary = 'nova-compute'
+        cls.fake_service_id = data_utils.rand_uuid()
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('508671aa-c929-4479-bd10-8680d40dd0a6')
+    def test_enable_service_with_invalid_service_id(self):
+        self.assertRaises(lib_exc.NotFound,
+                          self.client.update_service,
+                          service_id=self.fake_service_id,
+                          status='enabled')
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('a9eeeade-42b3-419f-87aa-c9342aa068cf')
+    def test_disable_service_with_invalid_service_id(self):
+        self.assertRaises(lib_exc.NotFound,
+                          self.client.update_service,
+                          service_id=self.fake_service_id,
+                          status='disabled')
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('f46a9d91-1e85-4b96-8e7a-db7706fa2e9a')
+    def test_disable_log_reason_with_invalid_service_id(self):
+        # disabled_reason requires that status='disabled' be provided.
+        self.assertRaises(lib_exc.NotFound,
+                          self.client.update_service,
+                          service_id=self.fake_service_id,
+                          status='disabled',
+                          disabled_reason='maintenance')
diff --git a/tempest/lib/api_schema/response/compute/v2_11/services.py b/tempest/lib/api_schema/response/compute/v2_11/services.py
index 18b833b..9ece1f9 100644
--- a/tempest/lib/api_schema/response/compute/v2_11/services.py
+++ b/tempest/lib/api_schema/response/compute/v2_11/services.py
@@ -44,3 +44,10 @@
         'required': ['service']
     }
 }
+
+# **** Schemas unchanged in microversion 2.11 since microversion 2.1 ****
+# Note(felipemonteiro): Below are the unchanged schema in this microversion. We
+# need to keep this schema in this file to have the generic way to select the
+# right schema based on self.schema_versions_info mapping in service client.
+enable_disable_service = copy.deepcopy(services.enable_disable_service)
+disable_log_reason = copy.deepcopy(services.disable_log_reason)
diff --git a/tempest/lib/api_schema/response/compute/v2_53/__init__.py b/tempest/lib/api_schema/response/compute/v2_53/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_53/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_53/services.py b/tempest/lib/api_schema/response/compute/v2_53/services.py
new file mode 100644
index 0000000..aa132a9
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_53/services.py
@@ -0,0 +1,70 @@
+# Copyright 2018 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 copy
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+from tempest.lib.api_schema.response.compute.v2_11 import services \
+    as servicesv211
+
+# ***************** Schemas changed in microversion 2.53 *****************
+
+# NOTE(felipemonteiro): This is schema for microversion 2.53 which includes:
+#
+# * changing the service 'id' to 'string' type only
+# * adding update_service which supersedes enable_service, disable_service,
+#   disable_log_reason, update_forced_down.
+
+list_services = copy.deepcopy(servicesv211.list_services)
+# The ID of the service is a uuid, so v2.1 pattern does not apply.
+list_services['response_body']['properties']['services']['items'][
+    'properties']['id'] = {'type': 'string', 'format': 'uuid'}
+
+update_service = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'service': {
+                'type': 'object',
+                'properties': {
+                    'id': {'type': 'string', 'format': 'uuid'},
+                    'binary': {'type': 'string'},
+                    'disabled_reason': {'type': 'string'},
+                    'host': {'type': 'string'},
+                    'state': {'type': 'string'},
+                    'status': {'type': 'string'},
+                    'updated_at': parameter_types.date_time,
+                    'zone': {'type': 'string'},
+                    'forced_down': {'type': 'boolean'}
+                },
+                'additionalProperties': False,
+                'required': ['id', 'binary', 'disabled_reason', 'host',
+                             'state', 'status', 'updated_at', 'zone',
+                             'forced_down']
+            }
+        },
+        'additionalProperties': False,
+        'required': ['service']
+    }
+}
+
+# **** Schemas unchanged in microversion 2.53 since microversion 2.11 ****
+# Note(felipemonteiro): Below are the unchanged schema in this microversion. We
+# need to keep this schema in this file to have the generic way to select the
+# right schema based on self.schema_versions_info mapping in service client.
+enable_disable_service = copy.deepcopy(servicesv211.enable_disable_service)
+update_forced_down = copy.deepcopy(servicesv211.update_forced_down)
+disable_log_reason = copy.deepcopy(servicesv211.disable_log_reason)
diff --git a/tempest/lib/services/compute/services_client.py b/tempest/lib/services/compute/services_client.py
index b046c35..d52de3a 100644
--- a/tempest/lib/services/compute/services_client.py
+++ b/tempest/lib/services/compute/services_client.py
@@ -20,6 +20,8 @@
 from tempest.lib.api_schema.response.compute.v2_1 import services as schema
 from tempest.lib.api_schema.response.compute.v2_11 import services \
     as schemav211
+from tempest.lib.api_schema.response.compute.v2_53 import services \
+    as schemav253
 from tempest.lib.common import rest_client
 from tempest.lib.services.compute import base_compute_client
 
@@ -28,7 +30,8 @@
 
     schema_versions_info = [
         {'min': None, 'max': '2.10', 'schema': schema},
-        {'min': '2.11', 'max': None, 'schema': schemav211}]
+        {'min': '2.11', 'max': '2.52', 'schema': schemav211},
+        {'min': '2.53', 'max': None, 'schema': schemav253}]
 
     def list_services(self, **params):
         """Lists all running Compute services for a tenant.
@@ -47,9 +50,30 @@
         self.validate_response(_schema.list_services, resp, body)
         return rest_client.ResponseBody(resp, body)
 
+    def update_service(self, service_id, **kwargs):
+        """Update a compute service.
+
+        Update a compute service to enable or disable scheduling, including
+        recording a reason why a compute service was disabled from scheduling.
+
+        This API is available starting with microversion 2.53.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        https://developer.openstack.org/api-ref/compute/#update-compute-service
+        """
+        put_body = json.dumps(kwargs)
+        resp, body = self.put('os-services/%s' % service_id, put_body)
+        body = json.loads(body)
+        _schema = self.get_schema(self.schema_versions_info)
+        self.validate_response(_schema.update_service, resp, body)
+        return rest_client.ResponseBody(resp, body)
+
     def enable_service(self, **kwargs):
         """Enable service on a host.
 
+        ``update_service`` supersedes this API starting with microversion 2.53.
+
         For a full list of available parameters, please refer to the official
         API reference:
         https://developer.openstack.org/api-ref/compute/#enable-scheduling-for-a-compute-service
@@ -63,6 +87,8 @@
     def disable_service(self, **kwargs):
         """Disable service on a host.
 
+        ``update_service`` supersedes this API starting with microversion 2.53.
+
         For a full list of available parameters, please refer to the official
         API reference:
         https://developer.openstack.org/api-ref/compute/#disable-scheduling-for-a-compute-service
@@ -76,6 +102,8 @@
     def disable_log_reason(self, **kwargs):
         """Disables scheduling for a Compute service and logs reason.
 
+        ``update_service`` supersedes this API starting with microversion 2.53.
+
         For a full list of available parameters, please refer to the official
         API reference:
         https://developer.openstack.org/api-ref/compute/#disable-scheduling-for-a-compute-service-and-log-disabled-reason
@@ -89,6 +117,8 @@
     def update_forced_down(self, **kwargs):
         """Set or unset ``forced_down`` flag for the service.
 
+        ``update_service`` supersedes this API starting with microversion 2.53.
+
         For a full list of available parameters, please refer to the official
         API reference:
         https://developer.openstack.org/api-ref/compute/#update-forced-down
diff --git a/tempest/tests/lib/services/compute/test_services_client.py b/tempest/tests/lib/services/compute/test_services_client.py
index 2dd981c..ba432e3 100644
--- a/tempest/tests/lib/services/compute/test_services_client.py
+++ b/tempest/tests/lib/services/compute/test_services_client.py
@@ -56,6 +56,20 @@
         }
     }
 
+    FAKE_UPDATE_SERVICE = {
+        "service": {
+            "id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339",
+            "binary": "nova-compute",
+            "disabled_reason": "test2",
+            "host": "host1",
+            "state": "down",
+            "status": "disabled",
+            "updated_at": "2012-10-29T13:42:05.000000",
+            "forced_down": False,
+            "zone": "nova"
+        }
+    }
+
     def setUp(self):
         super(TestServicesClient, self).setUp()
         fake_auth = fake_auth_provider.FakeAuthProvider()
@@ -119,6 +133,28 @@
             binary="controller",
             disabled_reason='test reason')
 
+    def _test_update_service(self, bytes_body=False, status=None,
+                             disabled_reason=None, forced_down=None):
+        resp_body = copy.deepcopy(self.FAKE_UPDATE_SERVICE)
+        kwargs = {}
+
+        if status is not None:
+            kwargs['status'] = status
+        if disabled_reason is not None:
+            kwargs['disabled_reason'] = disabled_reason
+        if forced_down is not None:
+            kwargs['forced_down'] = forced_down
+
+        resp_body['service'].update(kwargs)
+
+        self.check_service_client_function(
+            self.client.update_service,
+            'tempest.lib.common.rest_client.RestClient.put',
+            resp_body,
+            bytes_body,
+            service_id=resp_body['service']['id'],
+            **kwargs)
+
     def test_log_reason_disabled_service_with_str_body(self):
         self._test_log_reason_disabled_service()
 
@@ -144,3 +180,36 @@
                        new_callable=mock.PropertyMock(return_value='2.11'))
     def test_update_forced_down_with_bytes_body(self, _):
         self._test_update_forced_down(bytes_body=True)
+
+    @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+                       new_callable=mock.PropertyMock(return_value='2.53'))
+    def test_update_service_disable_scheduling_with_str_body(self, _):
+        self._test_update_service(status='disabled',
+                                  disabled_reason='maintenance')
+
+    @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+                       new_callable=mock.PropertyMock(return_value='2.53'))
+    def test_update_service_disable_scheduling_with_bytes_body(self, _):
+        self._test_update_service(status='disabled',
+                                  disabled_reason='maintenance',
+                                  bytes_body=True)
+
+    @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+                       new_callable=mock.PropertyMock(return_value='2.53'))
+    def test_update_service_enable_scheduling_with_str_body(self, _):
+        self._test_update_service(status='enabled')
+
+    @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+                       new_callable=mock.PropertyMock(return_value='2.53'))
+    def test_update_service_enable_scheduling_with_bytes_body(self, _):
+        self._test_update_service(status='enabled', bytes_body=True)
+
+    @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+                       new_callable=mock.PropertyMock(return_value='2.53'))
+    def test_update_service_forced_down_with_str_body(self, _):
+        self._test_update_service(forced_down=True)
+
+    @mock.patch.object(base_compute_client, 'COMPUTE_MICROVERSION',
+                       new_callable=mock.PropertyMock(return_value='2.53'))
+    def test_update_service_forced_down_with_bytes_body(self, _):
+        self._test_update_service(forced_down=True, bytes_body=True)