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
This commit is contained in:
parent
a552cb93ae
commit
fbbf01b2c7
@ -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.
|
||||
|
153
tempest/api/compute/admin/test_spice.py
Normal file
153
tempest/api/compute/admin/test_spice.py
Normal file
@ -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)
|
@ -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 '
|
||||
|
@ -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'
|
||||
|
78
tempest/lib/api_schema/response/compute/v2_98/servers.py
Normal file
78
tempest/lib/api_schema/response/compute/v2_98/servers.py
Normal file
@ -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']
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user