Tests for Nova instance diagnostics (microversion v2.48)

Existing instance diagnostics tests were moved to the new files
to test diagnostics for different microversions (2.1 and 2.48).

Depends-On: If0b1493cc5c1c7f0d9896dd68342ad4dea4f7da2

Change-Id: I7757c5beeea3d3b0bc15a51cafc5ea2ada65e76c
diff --git a/doc/source/microversion_testing.rst b/doc/source/microversion_testing.rst
index 9690923..d6d90ba 100644
--- a/doc/source/microversion_testing.rst
+++ b/doc/source/microversion_testing.rst
@@ -330,6 +330,10 @@
 
  .. _2.47: http://docs.openstack.org/developer/nova/api_microversion_history.html#id42
 
+ * `2.48`_
+
+ .. _2.48: http://docs.openstack.org/developer/nova/api_microversion_history.html#id43
+
 * Volume
 
  * `3.3`_
diff --git a/releasenotes/notes/add-server-diagnostics-validation-schema-b5a3c55b45aa718a.yaml b/releasenotes/notes/add-server-diagnostics-validation-schema-b5a3c55b45aa718a.yaml
new file mode 100644
index 0000000..e0ac87c
--- /dev/null
+++ b/releasenotes/notes/add-server-diagnostics-validation-schema-b5a3c55b45aa718a.yaml
@@ -0,0 +1,4 @@
+---
+features:
+  - |
+    Add validation schema for Nova server diagnostics API
\ No newline at end of file
diff --git a/tempest/api/compute/admin/test_server_diagnostics.py b/tempest/api/compute/admin/test_server_diagnostics.py
new file mode 100644
index 0000000..005efdd
--- /dev/null
+++ b/tempest/api/compute/admin/test_server_diagnostics.py
@@ -0,0 +1,76 @@
+# Copyright 2017 Mirantis Inc.
+#
+#    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 time
+
+from tempest.api.compute import base
+from tempest.lib import decorators
+
+
+class ServerDiagnosticsTest(base.BaseV2ComputeAdminTest):
+    min_microversion = None
+    max_microversion = '2.47'
+
+    @classmethod
+    def setup_clients(cls):
+        super(ServerDiagnosticsTest, cls).setup_clients()
+        cls.client = cls.os_admin.servers_client
+
+    @decorators.idempotent_id('31ff3486-b8a0-4f56-a6c0-aab460531db3')
+    def test_get_server_diagnostics(self):
+        server_id = self.create_test_server(wait_until='ACTIVE')['id']
+        diagnostics = self.client.show_server_diagnostics(server_id)
+
+        # NOTE(snikitin): Before microversion 2.48 response data from each
+        # hypervisor (libvirt, xen, vmware) was different. None of the fields
+        # were equal. As this test is common for libvirt, xen and vmware CI
+        # jobs we can't check any field in the response because all fields are
+        # different.
+        self.assertNotEmpty(diagnostics)
+
+
+class ServerDiagnosticsV248Test(base.BaseV2ComputeAdminTest):
+    min_microversion = '2.48'
+    max_microversion = 'latest'
+
+    @classmethod
+    def setup_clients(cls):
+        super(ServerDiagnosticsV248Test, cls).setup_clients()
+        cls.client = cls.os_admin.servers_client
+
+    @decorators.idempotent_id('64d0d48c-dff1-11e6-bf01-fe55135034f3')
+    def test_get_server_diagnostics(self):
+        server_id = self.create_test_server(wait_until='ACTIVE')['id']
+        # Response status and filed types will be checked by json schema
+        self.client.show_server_diagnostics(server_id)
+
+        # NOTE(snikitin): This is a special case for Xen hypervisor. In Xen
+        # case we're getting diagnostics stats from the RRDs which are updated
+        # every 5 seconds. It means that diagnostics information may be
+        # incomplete during first 5 seconds of VM life. In such cases methods
+        # which get diagnostics stats from Xen may raise exceptions or
+        # return `NaN` values. Such behavior must be handled correctly.
+        # Response must contain all diagnostics fields (may be with `None`
+        # values) and response status must be 200. Line above checks it by
+        # json schema.
+        time.sleep(10)
+        diagnostics = self.client.show_server_diagnostics(server_id)
+
+        # NOTE(snikitin): After 10 seconds diagnostics fields must contain
+        # not None values. But we will check only "memory_details.maximum"
+        # field because only this field meets all the following conditions:
+        # 1) This field may be unset because of Xen 5 seconds timeout.
+        # 2) This field is present in responses from all three supported
+        #    hypervisors (libvirt, xen, vmware).
+        self.assertIsNotNone(diagnostics['memory_details']['maximum'])
diff --git a/tempest/api/compute/admin/test_server_diagnostics_negative.py b/tempest/api/compute/admin/test_server_diagnostics_negative.py
new file mode 100644
index 0000000..d5b6674
--- /dev/null
+++ b/tempest/api/compute/admin/test_server_diagnostics_negative.py
@@ -0,0 +1,40 @@
+# Copyright 2017 Mirantis Inc.
+#
+#    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.compute import base
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+
+class ServerDiagnosticsNegativeTest(base.BaseV2ComputeAdminTest):
+    min_microversion = None
+    max_microversion = '2.47'
+
+    @classmethod
+    def setup_clients(cls):
+        super(ServerDiagnosticsNegativeTest, cls).setup_clients()
+        cls.client = cls.servers_client
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('e84e2234-60d2-42fa-8b30-e2d3049724ac')
+    def test_get_server_diagnostics_by_non_admin(self):
+        # Non-admin user cannot view server diagnostics according to policy
+        server_id = self.create_test_server(wait_until='ACTIVE')['id']
+        self.assertRaises(lib_exc.Forbidden,
+                          self.client.show_server_diagnostics, server_id)
+
+
+class ServerDiagnosticsNegativeV248Test(ServerDiagnosticsNegativeTest):
+    min_microversion = '2.48'
+    max_microversion = 'latest'
diff --git a/tempest/api/compute/admin/test_servers.py b/tempest/api/compute/admin/test_servers.py
index 789049b..0521cca 100644
--- a/tempest/api/compute/admin/test_servers.py
+++ b/tempest/api/compute/admin/test_servers.py
@@ -164,17 +164,6 @@
         server = self.client.show_server(self.s1_id)['server']
         self.assertEqual(server['status'], 'ACTIVE')
 
-    @decorators.skip_because(bug="1240043")
-    @decorators.idempotent_id('31ff3486-b8a0-4f56-a6c0-aab460531db3')
-    def test_get_server_diagnostics_by_admin(self):
-        # Retrieve server diagnostics by admin user
-        diagnostic = self.client.show_server_diagnostics(self.s1_id)
-        basic_attrs = ['rx_packets', 'rx_errors', 'rx_drop',
-                       'tx_packets', 'tx_errors', 'tx_drop',
-                       'read_req', 'write_req', 'cpu', 'memory']
-        for key in basic_attrs:
-            self.assertIn(key, str(diagnostic.keys()))
-
     @decorators.idempotent_id('682cb127-e5bb-4f53-87ce-cb9003604442')
     def test_rebuild_server_in_error_state(self):
         # The server in error state should be rebuilt using the provided
diff --git a/tempest/api/compute/admin/test_servers_negative.py b/tempest/api/compute/admin/test_servers_negative.py
index ca53696..9023759 100644
--- a/tempest/api/compute/admin/test_servers_negative.py
+++ b/tempest/api/compute/admin/test_servers_negative.py
@@ -108,14 +108,6 @@
                           self.client.reset_state, '999', state='error')
 
     @decorators.attr(type=['negative'])
-    @decorators.idempotent_id('e84e2234-60d2-42fa-8b30-e2d3049724ac')
-    def test_get_server_diagnostics_by_non_admin(self):
-        # Non-admin user can not view server diagnostics according to policy
-        self.assertRaises(lib_exc.Forbidden,
-                          self.non_adm_client.show_server_diagnostics,
-                          self.s1_id)
-
-    @decorators.attr(type=['negative'])
     @decorators.idempotent_id('46a4e1ca-87ae-4d28-987a-1b6b136a0221')
     def test_migrate_non_existent_server(self):
         # migrate a non existent server
diff --git a/tempest/lib/api_schema/response/compute/v2_1/servers.py b/tempest/lib/api_schema/response/compute/v2_1/servers.py
index 33a7757..7360396 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/servers.py
@@ -582,3 +582,10 @@
         'required': ['adminPass']
     }
 }
+
+show_server_diagnostics = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object'
+    }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_48/__init__.py b/tempest/lib/api_schema/response/compute/v2_48/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_48/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_48/servers.py b/tempest/lib/api_schema/response/compute/v2_48/servers.py
new file mode 100644
index 0000000..5904758
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_48/servers.py
@@ -0,0 +1,115 @@
+# Copyright 2017 Mirantis Inc.
+#
+#    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_47 import servers as servers247
+
+
+show_server_diagnostics = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'state': {
+                'type': 'string', 'enum': [
+                    'pending', 'running', 'paused', 'shutdown', 'crashed',
+                    'suspended']
+            },
+            'driver': {
+                'type': 'string', 'enum': [
+                    'libvirt', 'xenapi', 'vmwareapi', 'ironic', 'hyperv']
+            },
+            'hypervisor': {'type': ['string', 'null']},
+            'hypervisor_os': {'type': ['string', 'null']},
+            'uptime': {'type': ['integer', 'null']},
+            'config_drive': {'type': 'boolean'},
+            'num_cpus': {'type': 'integer'},
+            'num_nics': {'type': 'integer'},
+            'num_disks': {'type': 'integer'},
+            'memory_details': {
+                'type': 'object',
+                'properties': {
+                    'maximum': {'type': ['integer', 'null']},
+                    'used': {'type': ['integer', 'null']}
+                },
+                'additionalProperties': False,
+                'required': ['maximum', 'used']
+            },
+            'cpu_details': {
+                'type': 'array',
+                'items': {
+                    'type': 'object',
+                    'properties': {
+                        'id': {'type': ['integer', 'null']},
+                        'time': {'type': ['integer', 'null']},
+                        'utilisation': {'type': ['integer', 'null']}
+                    },
+                    'additionalProperties': False,
+                    'required': ['id', 'time', 'utilisation']
+                }
+            },
+            'nic_details': {
+                'type': 'array',
+                'items': {
+                    'type': 'object',
+                    'properties': {
+                        'mac_address': {'oneOf': [parameter_types.mac_address,
+                                                  {'type': 'null'}]},
+                        'rx_octets': {'type': ['integer', 'null']},
+                        'rx_errors': {'type': ['integer', 'null']},
+                        'rx_drop': {'type': ['integer', 'null']},
+                        'rx_packets': {'type': ['integer', 'null']},
+                        'rx_rate': {'type': ['integer', 'null']},
+                        'tx_octets': {'type': ['integer', 'null']},
+                        'tx_errors': {'type': ['integer', 'null']},
+                        'tx_drop': {'type': ['integer', 'null']},
+                        'tx_packets': {'type': ['integer', 'null']},
+                        'tx_rate': {'type': ['integer', 'null']}
+                    },
+                    'additionalProperties': False,
+                    'required': ['mac_address', 'rx_octets', 'rx_errors',
+                                 'rx_drop',
+                                 'rx_packets', 'rx_rate', 'tx_octets',
+                                 'tx_errors',
+                                 'tx_drop', 'tx_packets', 'tx_rate']
+                }
+            },
+            'disk_details': {
+                'type': 'array',
+                'items': {
+                    'type': 'object',
+                    'properties': {
+                        'read_bytes': {'type': ['integer', 'null']},
+                        'read_requests': {'type': ['integer', 'null']},
+                        'write_bytes': {'type': ['integer', 'null']},
+                        'write_requests': {'type': ['integer', 'null']},
+                        'errors_count': {'type': ['integer', 'null']}
+                    },
+                    'additionalProperties': False,
+                    'required': ['read_bytes', 'read_requests', 'write_bytes',
+                                 'write_requests', 'errors_count']
+                }
+            }
+        },
+        'additionalProperties': False,
+        'required': [
+            'state', 'driver', 'hypervisor', 'hypervisor_os', 'uptime',
+            'config_drive', 'num_cpus', 'num_nics', 'num_disks',
+            'memory_details', 'cpu_details', 'nic_details', 'disk_details'],
+    }
+}
+
+get_server = copy.deepcopy(servers247.get_server)
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index e93cdbf..598d5a6 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -28,6 +28,7 @@
 from tempest.lib.api_schema.response.compute.v2_26 import servers as schemav226
 from tempest.lib.api_schema.response.compute.v2_3 import servers as schemav23
 from tempest.lib.api_schema.response.compute.v2_47 import servers as schemav247
+from tempest.lib.api_schema.response.compute.v2_48 import servers as schemav248
 from tempest.lib.api_schema.response.compute.v2_6 import servers as schemav26
 from tempest.lib.api_schema.response.compute.v2_9 import servers as schemav29
 from tempest.lib.common import rest_client
@@ -45,7 +46,8 @@
         {'min': '2.16', 'max': '2.18', 'schema': schemav216},
         {'min': '2.19', 'max': '2.25', 'schema': schemav219},
         {'min': '2.26', 'max': '2.46', 'schema': schemav226},
-        {'min': '2.47', 'max': None, 'schema': schemav247}]
+        {'min': '2.47', 'max': '2.47', 'schema': schemav247},
+        {'min': '2.48', 'max': None, 'schema': schemav248}]
 
     def __init__(self, auth_provider, service, region,
                  enable_instance_password=True, **kwargs):
@@ -658,7 +660,10 @@
     def show_server_diagnostics(self, server_id):
         """Get the usage data for a server."""
         resp, body = self.get("servers/%s/diagnostics" % server_id)
-        return rest_client.ResponseBody(resp, json.loads(body))
+        body = json.loads(body)
+        schema = self.get_schema(self.schema_versions_info)
+        self.validate_response(schema.show_server_diagnostics, resp, body)
+        return rest_client.ResponseBody(resp, body)
 
     def list_instance_actions(self, server_id):
         """List the provided server action."""