From fbbf01b2c7989a0ed1877acb5b370ee1ee6af0ff Mon Sep 17 00:00:00 2001 From: Michael Still Date: Wed, 12 Feb 2025 18:05:57 +1100 Subject: [PATCH] Add a spice-direct tempest test. This test works through the full spice-direct console flow: - create an instance - request a console token - turn that console token into connection details - connect with those details and ensure you get a SPICE protocol handshake back Change-Id: I9c4d1f05622d9a26db9edd2119eb03fdde726630 --- ...e-rdp-console-config-f2af173552axfb72.yaml | 10 +- tempest/api/compute/admin/test_spice.py | 153 ++++++++++++++++++ tempest/config.py | 8 +- .../response/compute/v2_6/servers.py | 3 +- .../response/compute/v2_98/__init__.py | 0 .../response/compute/v2_98/servers.py | 78 +++++++++ .../lib/services/compute/servers_client.py | 18 ++- 7 files changed, 259 insertions(+), 11 deletions(-) create mode 100644 tempest/api/compute/admin/test_spice.py create mode 100644 tempest/lib/api_schema/response/compute/v2_98/__init__.py create mode 100644 tempest/lib/api_schema/response/compute/v2_98/servers.py diff --git a/releasenotes/notes/deprecate-spice-rdp-console-config-f2af173552axfb72.yaml b/releasenotes/notes/deprecate-spice-rdp-console-config-f2af173552axfb72.yaml index 58b161fd9f..313b2764c0 100644 --- a/releasenotes/notes/deprecate-spice-rdp-console-config-f2af173552axfb72.yaml +++ b/releasenotes/notes/deprecate-spice-rdp-console-config-f2af173552axfb72.yaml @@ -1,6 +1,10 @@ --- deprecations: - | - The config options ``CONF.compute.spice_console`` and ``CONF.compute.rdp_console`` - are deprecated because test cases using them are removed. - We can add them back when adding the test cases again. + The config option ``CONF.compute.rdp_console`` + is deprecated because test cases using it have been removed. + We can add it back when adding the test cases again. + - | + The config option ``CONF.compute.spice_console`` was previously listed as + deprecated, but is now back in active use to support the testing of SPICE consoles + in Nova. diff --git a/tempest/api/compute/admin/test_spice.py b/tempest/api/compute/admin/test_spice.py new file mode 100644 index 0000000000..d56bb2f62c --- /dev/null +++ b/tempest/api/compute/admin/test_spice.py @@ -0,0 +1,153 @@ +# 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 socket +import struct +import urllib.parse as urlparse + +from tempest.api.compute import base +from tempest import config +from tempest.lib import decorators + +CONF = config.CONF + + +class SpiceDirectConsoleTestJSON(base.BaseV2ComputeAdminTest): + """Test the spice-direct console""" + + create_default_network = True + + min_microversion = '2.98' + max_microversion = 'latest' + + # SPICE client protocol constants + magic = b'REDQ' + major = 2 + minor = 2 + main_channel = 1 + common_caps = 11 # AuthSelection, AuthSpice, MiniHeader + channel_caps = 9 # SemiSeamlessMigrate, SeamlessMigrate + + @classmethod + def skip_checks(cls): + super().skip_checks() + if not CONF.compute_feature_enabled.spice_console: + raise cls.skipException('SPICE console feature is disabled.') + + def tearDown(self): + super().tearDown() + # NOTE(zhufl): Because server_check_teardown will raise Exception + # which will prevent other cleanup steps from being executed, so + # server_check_teardown should be called after super's tearDown. + self.server_check_teardown() + + @classmethod + def setup_clients(cls): + super().setup_clients() + cls.client = cls.servers_client + + @classmethod + def resource_setup(cls): + super().resource_setup() + cls.server = cls.create_test_server(wait_until="ACTIVE") + + @decorators.idempotent_id('80f4460d-1a06-403c-9e93-cf434c70be05') + def test_spice_direct(self): + """Test accessing spice-direct console of server""" + + # Request a spice-direct console and validate the result. Any user can + # do this. + body = self.servers_client.get_remote_console( + self.server['id'], console_type='spice-direct', protocol='spice') + + console_url = body['remote_console']['url'] + parts = urlparse.urlparse(console_url) + qparams = urlparse.parse_qs(parts.query) + self.assertIn('token', qparams) + self.assertNotEmpty(qparams['token']) + self.assertEqual(1, len(qparams['token'])) + + self.assertEqual('spice', body['remote_console']['protocol']) + self.assertEqual('spice-direct', body['remote_console']['type']) + + # For reasons best know to the python developers, the qparams values + # are lists as documented at + # https://docs.python.org/3/library/urllib.parse.html + token = qparams['token'][0] + + # Turn that console token into hypervisor connection details. Only + # admins can do this because its expected that the request is coming + # from a proxy and we don't want to expose intimate hypervisor details + # to all users. + body = self.admin_servers_client.get_console_auth_token_details( + token) + + console = body['console'] + self.assertEqual(self.server['id'], console['instance_uuid']) + self.assertIn('port', console) + self.assertIn('tls_port', console) + self.assertIsNone(console['internal_access_path']) + + # Connect to the specified non-TLS port and verify we get back + # a SPICE protocol greeting + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((console['host'], console['port'])) + + # Send a client greeting + # + # ---- SpiceLinkMess ---- + # 4s UINT32 magic value, must be REDQ + # I UINT32 major_version, must be 2 + # I UINT32 minor_version, must be 2 + # I UINT32 size number of bytes following this field to the end + # of this message. + # I UINT32 connection_id. In case of a new session (i.e., channel + # type is SPICE_CHANNEL_MAIN) this field is set to zero, + # and in response the server will allocate session id + # and will send it via the SpiceLinkReply message. In + # case of all other channel types, this field will be + # equal to the allocated session id. + # B UINT8 channel_type, we use main + # B UINT8 channel_id to connect to + # I UINT32 num_common_caps number of common client channel + # capabilities words + # I UINT32 num_channel_caps number of specific client channel + # capabilities words + # I UINT32 caps_offset location of the start of the capabilities + # vector given by the bytes offset from the “size” + # member (i.e., from the address of the “connection_id” + # member). + # ... capabilities + sock.sendall(struct.pack( + '<4sIIIIBBIIIII', self.magic, self.major, self.minor, 42 - 16, + 0, self.main_channel, 0, 1, 1, 18, self.common_caps, + self.channel_caps)) + + # ---- SpiceLinkReply ---- + # 4s UINT32 magic value, must be equal to SPICE_MAGIC + # I UINT32 major_version, must be equal to SPICE_VERSION_MAJOR + # I UINT32 minor_version, must be equal to SPICE_VERSION_MINOR + # I UINT32 size number of bytes following this field to the end + # of this message. + # I UINT32 error code + # ... + buffered = sock.recv(20) + self.assertIsNotNone(buffered) + self.assertEqual(20, len(buffered)) + + magic, major, minor, _, error = struct.unpack_from('<4sIIII', buffered) + self.assertEqual(b'REDQ', magic) + self.assertEqual(2, major) + self.assertEqual(2, minor) + self.assertEqual(0, error) diff --git a/tempest/config.py b/tempest/config.py index 771972022d..f9a08eaacf 100644 --- a/tempest/config.py +++ b/tempest/config.py @@ -540,12 +540,8 @@ ComputeFeaturesGroup = [ 'be same as nova.conf: vnc.enabled'), cfg.BoolOpt('spice_console', default=False, - help='Enable Spice console. This configuration value should ' - 'be same as nova.conf: spice.enabled', - deprecated_for_removal=True, - deprecated_reason="This config option is not being used " - "in Tempest, we can add it back when " - "adding the test cases."), + help='Enable SPICE console. This configuration value should ' + 'be same as nova.conf: spice.enabled'), cfg.BoolOpt('serial_console', default=False, help='Enable serial console. This configuration value ' diff --git a/tempest/lib/api_schema/response/compute/v2_6/servers.py b/tempest/lib/api_schema/response/compute/v2_6/servers.py index e6b2c32b45..05ab616839 100644 --- a/tempest/lib/api_schema/response/compute/v2_6/servers.py +++ b/tempest/lib/api_schema/response/compute/v2_6/servers.py @@ -46,7 +46,8 @@ get_remote_consoles = { 'properties': { 'protocol': {'enum': ['vnc', 'rdp', 'serial', 'spice']}, 'type': {'enum': ['novnc', 'xvpvnc', 'rdp-html5', - 'spice-html5', 'serial']}, + 'spice-html5', + 'serial']}, 'url': { 'type': 'string', 'format': 'uri' diff --git a/tempest/lib/api_schema/response/compute/v2_98/__init__.py b/tempest/lib/api_schema/response/compute/v2_98/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tempest/lib/api_schema/response/compute/v2_98/servers.py b/tempest/lib/api_schema/response/compute/v2_98/servers.py new file mode 100644 index 0000000000..828dda1b92 --- /dev/null +++ b/tempest/lib/api_schema/response/compute/v2_98/servers.py @@ -0,0 +1,78 @@ +# 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_96 import servers + +# NOTE: 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. +# ****** Schemas unchanged since microversion 2.96 ****** +list_servers = copy.deepcopy(servers.list_servers) +get_server = copy.deepcopy(servers.get_server) +list_servers_detail = copy.deepcopy(servers.list_servers_detail) +update_server = copy.deepcopy(servers.update_server) +rebuild_server = copy.deepcopy(servers.rebuild_server) +rebuild_server_with_admin_pass = copy.deepcopy( + servers.rebuild_server_with_admin_pass) +show_server_diagnostics = copy.deepcopy(servers.show_server_diagnostics) +attach_volume = copy.deepcopy(servers.attach_volume) +show_volume_attachment = copy.deepcopy(servers.show_volume_attachment) +list_volume_attachments = copy.deepcopy(servers.list_volume_attachments) +show_instance_action = copy.deepcopy(servers.show_instance_action) +create_backup = copy.deepcopy(servers.create_backup) + +console_auth_tokens = { + 'status_code': [200], + 'response_body': { + 'type': 'object', + 'properties': { + 'console': { + 'type': 'object', + 'properties': { + 'instance_uuid': {'type': 'string'}, + 'host': {'type': 'string'}, + 'port': {'type': 'integer'}, + 'tls_port': {'type': ['integer', 'null']}, + 'internal_access_path': {'type': ['string', 'null']} + } + } + } + } +} + +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', 'xvpvnc', 'rdp-html5', + 'spice-html5', 'spice-direct', + 'serial']}, + 'url': { + 'type': 'string', + 'format': 'uri' + } + }, + 'additionalProperties': False, + 'required': ['protocol', 'type', 'url'] + } + }, + 'additionalProperties': False, + 'required': ['remote_console'] + } +} diff --git a/tempest/lib/services/compute/servers_client.py b/tempest/lib/services/compute/servers_client.py index e91c87a9ae..03562aa923 100644 --- a/tempest/lib/services/compute/servers_client.py +++ b/tempest/lib/services/compute/servers_client.py @@ -46,6 +46,7 @@ from tempest.lib.api_schema.response.compute.v2_8 import servers as schemav28 from tempest.lib.api_schema.response.compute.v2_89 import servers as schemav289 from tempest.lib.api_schema.response.compute.v2_9 import servers as schemav29 from tempest.lib.api_schema.response.compute.v2_96 import servers as schemav296 +from tempest.lib.api_schema.response.compute.v2_98 import servers as schemav298 from tempest.lib.common import rest_client from tempest.lib.services.compute import base_compute_client @@ -77,7 +78,9 @@ class ServersClient(base_compute_client.BaseComputeClient): {'min': '2.75', 'max': '2.78', 'schema': schemav275}, {'min': '2.79', 'max': '2.88', 'schema': schemav279}, {'min': '2.89', 'max': '2.95', 'schema': schemav289}, - {'min': '2.96', 'max': None, 'schema': schemav296}] + {'min': '2.96', 'max': '2.97', 'schema': schemav296}, + {'min': '2.98', 'max': None, 'schema': schemav298}, + ] def __init__(self, auth_provider, service, region, enable_instance_password=True, **kwargs): @@ -680,6 +683,19 @@ class ServersClient(base_compute_client.BaseComputeClient): self.validate_response(schema.get_remote_consoles, resp, body) return rest_client.ResponseBody(resp, body) + def get_console_auth_token_details(self, token): + """Turn a console auth token into hypervisor connection details. + + For a full list of available parameters, please refer to the official + API reference: + https://docs.openstack.org/api-ref/compute/#show-console-connection-information + """ + resp, body = self.get('/os-console-auth-tokens/%s' % token) + body = json.loads(body) + schema = self.get_schema(self.schema_versions_info) + self.validate_response(schema.console_auth_tokens, resp, body) + return rest_client.ResponseBody(resp, body) + def rescue_server(self, server_id, **kwargs): """Rescue the provided server.