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 0000000000..18fd5adfac --- /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 0ceb13c109..466de3292b 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 @@ class LiveBlockMigrationTestJSON(base.BaseV2ComputeAdminTest): 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 00c69b0141..11c1b907bd 100644 --- a/tempest/config.py +++ b/tempest/config.py @@ -399,6 +399,11 @@ ComputeFeaturesGroup = [ 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 0000000000..e69de29bb2 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 0000000000..29b3e86008 --- /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 470190c2c7..e260e48ceb 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 0d355a1d40..ff65b25185 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_16 import servers as schemav216 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 @@ class ServersClient(base_compute_client.BaseComputeClient): 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 @@ class ServersClient(base_compute_client.BaseComputeClient): 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 a277dfe591..a8573292c8 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 @@ class TestServersClient(base.BaseServiceTest): 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', + )