Nova: test live migration with serial console

Nova offers a "serial console" as an alternative to graphical consoles
like VNC/SPICE/RDP. This is useful for platforms which don't have
graphical consoles, for example the "IBM system z" platform.

This change introduces a test which ensures that the interaction with
the serial console is possible before and after a live-migration.
As the unified remote console API is available since microversion 2.6,
I use this as a base for the tests. This made id necessary to update
the schemas.

This change introduces a config option to enable new test cases for
the serial console.
A Nova change (see I7af395a867e0657c26fa064d2b0134345cd96814),
which uses the hook for live-migration testing, will use the config
option of this change to alter the testing system on the fly to
enable the testing of the serial console.

Closes-Bug: #1560358
Needed-By: I7af395a867e0657c26fa064d2b0134345cd96814
Change-Id: I020fd94d970ad0cdf7ab65d7656da6ca5766094b
diff --git a/releasenotes/notes/add-compute-feature-serial-console-45583c4341e34fc9.yaml b/releasenotes/notes/add-compute-feature-serial-console-45583c4341e34fc9.yaml
new file mode 100644
index 0000000..18fd5ad
--- /dev/null
+++ b/releasenotes/notes/add-compute-feature-serial-console-45583c4341e34fc9.yaml
@@ -0,0 +1,7 @@
+---
+features:
+  - |
+    A new boolean config option ``serial_console`` is added to the section
+    ``compute-feature-enabled``. If enabled, tests, which validate the
+    behavior of Nova's *serial console* feature (an alternative to VNC,
+    RDP, SPICE) can be executed.
diff --git a/tempest/api/compute/admin/test_live_migration.py b/tempest/api/compute/admin/test_live_migration.py
index 0ceb13c..466de32 100644
--- a/tempest/api/compute/admin/test_live_migration.py
+++ b/tempest/api/compute/admin/test_live_migration.py
@@ -13,10 +13,13 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import time
+
 from oslo_log import log as logging
 import testtools
 
 from tempest.api.compute import base
+from tempest.common import compute
 from tempest.common import waiters
 from tempest import config
 from tempest.lib import decorators
@@ -169,6 +172,80 @@
         self.assertEqual(target_host, self._get_host_for_server(server_id))
 
 
+class LiveBlockMigrationRemoteConsolesV26TestJson(LiveBlockMigrationTestJSON):
+    min_microversion = '2.6'
+    max_microversion = 'latest'
+
+    @decorators.idempotent_id('6190af80-513e-4f0f-90f2-9714e84955d7')
+    @testtools.skipUnless(CONF.compute_feature_enabled.serial_console,
+                          'Serial console not supported.')
+    @testtools.skipUnless(
+        test.is_scheduler_filter_enabled("DifferentHostFilter"),
+        'DifferentHostFilter is not available.')
+    def test_live_migration_serial_console(self):
+        """Test the live-migration of an instance which has a serial console
+
+        The serial console feature of an instance uses ports on the host.
+        These ports need to be updated when they are already in use by
+        another instance on the target host. This test checks if this
+        update behavior is correctly done, by connecting to the serial
+        consoles of the instances before and after the live migration.
+        """
+        server01_id = self.create_test_server(wait_until='ACTIVE')['id']
+        hints = {'different_host': server01_id}
+        server02_id = self.create_test_server(scheduler_hints=hints,
+                                              wait_until='ACTIVE')['id']
+        host01_id = self._get_host_for_server(server01_id)
+        host02_id = self._get_host_for_server(server02_id)
+        self.assertNotEqual(host01_id, host02_id)
+
+        # At this step we have 2 instances on different hosts, both with
+        # serial consoles, both with port 10000 (the default value).
+        # https://bugs.launchpad.net/nova/+bug/1455252 describes the issue
+        # when live-migrating in such a scenario.
+
+        self._verify_console_interaction(server01_id)
+        self._verify_console_interaction(server02_id)
+
+        self._migrate_server_to(server01_id, host02_id)
+        waiters.wait_for_server_status(self.servers_client,
+                                       server01_id, 'ACTIVE')
+        self.assertEqual(host02_id, self._get_host_for_server(server01_id))
+        self._verify_console_interaction(server01_id)
+        # At this point, both instances have a valid serial console
+        # connection, which means the ports got updated.
+
+    def _verify_console_interaction(self, server_id):
+        body = self.servers_client.get_remote_console(server_id,
+                                                      console_type='serial',
+                                                      protocol='serial')
+        console_url = body['remote_console']['url']
+        data = "test_live_migration_serial_console"
+        console_output = ''
+        t = 0.0
+        interval = 0.1
+
+        ws = compute.create_websocket(console_url)
+        try:
+            # NOTE (markus_z): It can take a long time until the terminal
+            # of the instance is available for interaction. Hence the
+            # long timeout value.
+            while data not in console_output and t <= 120.0:
+                try:
+                    ws.send_frame(data)
+                    recieved = ws.receive_frame()
+                    console_output += recieved
+                except Exception:
+                    # In case we had an issue with send/receive on the
+                    # websocket connection, we create a new one.
+                    ws = compute.create_websocket(console_url)
+                time.sleep(interval)
+                t += interval
+        finally:
+            ws.close()
+        self.assertIn(data, console_output)
+
+
 class LiveAutoBlockMigrationV225TestJSON(LiveBlockMigrationTestJSON):
     min_microversion = '2.25'
     max_microversion = 'latest'
diff --git a/tempest/config.py b/tempest/config.py
index 00c69b0..11c1b90 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -399,6 +399,11 @@
                 default=False,
                 help='Enable RDP console. This configuration value should '
                      'be same as [nova.rdp]->enabled in nova.conf'),
+    cfg.BoolOpt('serial_console',
+                default=False,
+                help='Enable serial console. This configuration value '
+                     'should be the same as [nova.serial_console]->enabled '
+                     'in nova.conf'),
     cfg.BoolOpt('rescue',
                 default=True,
                 help='Does the test environment support instance rescue '
diff --git a/tempest/lib/api_schema/response/compute/v2_6/__init__.py b/tempest/lib/api_schema/response/compute/v2_6/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_6/__init__.py
diff --git a/tempest/lib/api_schema/response/compute/v2_6/servers.py b/tempest/lib/api_schema/response/compute/v2_6/servers.py
new file mode 100644
index 0000000..29b3e86
--- /dev/null
+++ b/tempest/lib/api_schema/response/compute/v2_6/servers.py
@@ -0,0 +1,48 @@
+# Copyright 2016 IBM Corp.
+#
+#    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_3 import servers
+
+list_servers = copy.deepcopy(servers.list_servers)
+get_server = copy.deepcopy(servers.get_server)
+list_servers_detail = copy.deepcopy(servers.list_servers_detail)
+
+# NOTE: The consolidated remote console API got introduced with v2.6
+# with bp/consolidate-console-api. See Nova commit 578bafeda
+get_remote_consoles = {
+    'status_code': [200],
+    'response_body': {
+        'type': 'object',
+        'properties': {
+            'remote_console': {
+                'type': 'object',
+                'properties': {
+                    'protocol': {'enum': ['vnc', 'rdp', 'serial', 'spice']},
+                    'type': {'enum': ['novnc', 'xpvnc', 'rdp-html5',
+                                      'spice-html5', 'serial']},
+                    'url': {
+                        'type': 'string',
+                        'format': 'uri'
+                    }
+                },
+                'additionalProperties': False,
+                'required': ['protocol', 'type', 'url']
+            }
+        },
+        'additionalProperties': False,
+        'required': ['remote_console']
+    }
+}
diff --git a/tempest/lib/api_schema/response/compute/v2_9/servers.py b/tempest/lib/api_schema/response/compute/v2_9/servers.py
index 470190c..e260e48 100644
--- a/tempest/lib/api_schema/response/compute/v2_9/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_9/servers.py
@@ -14,7 +14,7 @@
 
 import copy
 
-from tempest.lib.api_schema.response.compute.v2_3 import servers
+from tempest.lib.api_schema.response.compute.v2_6 import servers
 
 list_servers = copy.deepcopy(servers.list_servers)
 
diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py
index 0d355a1..ff65b25 100644
--- a/tempest/lib/services/compute/servers_client.py
+++ b/tempest/lib/services/compute/servers_client.py
@@ -27,6 +27,7 @@
 from tempest.lib.api_schema.response.compute.v2_19 import servers as schemav219
 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_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
 from tempest.lib.services.compute import base_compute_client
@@ -37,7 +38,8 @@
 
     schema_versions_info = [
         {'min': None, 'max': '2.2', 'schema': schema},
-        {'min': '2.3', 'max': '2.8', 'schema': schemav23},
+        {'min': '2.3', 'max': '2.5', 'schema': schemav23},
+        {'min': '2.6', 'max': '2.8', 'schema': schemav26},
         {'min': '2.9', 'max': '2.15', 'schema': schemav29},
         {'min': '2.16', 'max': '2.18', 'schema': schemav216},
         {'min': '2.19', 'max': '2.25', 'schema': schemav219},
@@ -598,6 +600,29 @@
         return self.action(server_id, 'os-getConsoleOutput',
                            schema.get_console_output, **kwargs)
 
+    def get_remote_console(self, server_id, console_type, protocol, **kwargs):
+        """Get a remote console.
+
+        For a full list of available parameters, please refer to the official
+        API reference:
+        TODO (markus_z) The api-ref for that isn't yet available, update this
+        here when the docs in Nova are updated. The old API is at
+        http://developer.openstack.org/api-ref/compute/#get-serial-console-os-getserialconsole-action
+        """
+        param = {
+            'remote_console': {
+                'type': console_type,
+                'protocol': protocol,
+            }
+        }
+        post_body = json.dumps(param)
+        resp, body = self.post("servers/%s/remote-consoles" % server_id,
+                               post_body)
+        body = json.loads(body)
+        schema = self.get_schema(self.schema_versions_info)
+        self.validate_response(schema.get_remote_consoles, resp, body)
+        return rest_client.ResponseBody(resp, body)
+
     def list_virtual_interfaces(self, server_id):
         """List the virtual interfaces used in an instance."""
         resp, body = self.get('/'.join(['servers', server_id,
diff --git a/tempest/tests/lib/services/compute/test_servers_client.py b/tempest/tests/lib/services/compute/test_servers_client.py
index a277dfe..a857329 100644
--- a/tempest/tests/lib/services/compute/test_servers_client.py
+++ b/tempest/tests/lib/services/compute/test_servers_client.py
@@ -1168,3 +1168,34 @@
             tag=self.FAKE_TAGS[0],
             status=204,
             to_utf=bytes_body)
+
+
+class TestServersClientMinV26(base.BaseServiceTest):
+
+    def setUp(self):
+        super(TestServersClientMinV26, self).setUp()
+        fake_auth = fake_auth_provider.FakeAuthProvider()
+        self.client = servers_client.ServersClient(fake_auth, 'compute',
+                                                   'regionOne')
+        base_compute_client.COMPUTE_MICROVERSION = '2.6'
+        self.server_id = "920eaac8-a284-4fd1-9c2c-b30f0181b125"
+
+    def tearDown(self):
+        super(TestServersClientMinV26, self).tearDown()
+        base_compute_client.COMPUTE_MICROVERSION = None
+
+    def test_get_remote_consoles(self):
+        self.check_service_client_function(
+            self.client.get_remote_console,
+            'tempest.lib.common.rest_client.RestClient.post',
+            {
+                'remote_console': {
+                    'protocol': 'serial',
+                    'type': 'serial',
+                    'url': 'ws://127.0.0.1:6083/?token=IllAllowIt'
+                    }
+            },
+            server_id=self.server_id,
+            console_type='serial',
+            protocol='serial',
+            )