Add tempest test to test NoVNC support

Currently there exists support within tempest to make sure that the
VNC Console URL can be generated, but there is no support to test out
the actual NoVNC code through the novncproxy.  This will add support
to verify that the novncproxy will return the proper NoVNC JavaScript
HTML, that the authentication token is validated, and that the basic
exchange of the RFB protocol from the client through the novncproxy
is a valid VNC session.

This test requires that the tempest configuration specifies that the
VNC Console feature is enabled, otherwise the test will be skipped.
Additional integration test environments can be switched to enable
this feature to take advantage of this test.

This support is being added in conjunction with change set 115484 to
further test the NoVNC TLS Support, but this test case will work
whether or not the communication between the novncproxy and VNC
session is using TLS or is unencrypted.

Change-Id: Ic7e149f79705cbb910c5b6524dac5cdb73d69710
Closes-Bug: 1554460
Depends-On: Idb38a3b11e2f61f23adf1ec23c04ddccd72e7539
This commit is contained in:
Michelle Mandel 2016-07-15 17:11:33 -04:00 committed by Jordan Pittier
parent 6eb4671d34
commit 1f87a5611a
1 changed files with 241 additions and 0 deletions

View File

@ -0,0 +1,241 @@
# Copyright 2016 OpenStack Foundation
# 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 six
from six.moves.urllib import parse as urlparse
import urllib3
from tempest.api.compute import base
from tempest import config
from tempest import test
CONF = config.CONF
class NoVNCConsoleTestJSON(base.BaseV2ComputeTest):
@classmethod
def skip_checks(cls):
super(NoVNCConsoleTestJSON, cls).skip_checks()
if not CONF.compute_feature_enabled.vnc_console:
raise cls.skipException('VNC Console feature is disabled.')
def setUp(self):
super(NoVNCConsoleTestJSON, self).setUp()
self._websocket = None
def tearDown(self):
self.server_check_teardown()
super(NoVNCConsoleTestJSON, self).tearDown()
if self._websocket is not None:
self._websocket.close()
@classmethod
def setup_clients(cls):
super(NoVNCConsoleTestJSON, cls).setup_clients()
cls.client = cls.servers_client
@classmethod
def resource_setup(cls):
super(NoVNCConsoleTestJSON, cls).resource_setup()
cls.server = cls.create_test_server(wait_until="ACTIVE")
def _validate_novnc_html(self, vnc_url):
"""Verify we can connect to novnc and get back the javascript."""
resp = urllib3.PoolManager().request('GET', vnc_url)
# Make sure that the GET request was accepted by the novncproxy
self.assertEqual(resp.status, 200, 'Got a Bad HTTP Response on the '
'initial call: ' + str(resp.status))
# Do some basic validation to make sure it is an expected HTML document
self.assertTrue('<html>' in resp.data and '</html>' in resp.data,
'Not a valid html document in the response.')
# Just try to make sure we got JavaScript back for noVNC, since we
# won't actually use it since not inside of a browser
self.assertTrue('noVNC' in resp.data and '<script' in resp.data,
'Not a valid noVNC javascript html document.')
def _validate_rfb_negotiation(self):
"""Verify we can connect to novnc and do the websocket connection."""
# Turn the Socket into a WebSocket to do the communication
data = self._websocket.receive_frame()
self.assertFalse(data is None or len(data) == 0,
'Token must be invalid because the connection '
'closed.')
# Parse the RFB version from the data to make sure it is valid
# and greater than or equal to 3.3
version = float("%d.%d" % (int(data[4:7], base=10),
int(data[8:11], base=10)))
self.assertTrue(version >= 3.3, 'Bad RFB Version: ' + str(version))
# Send our RFB version to the server, which we will just go with 3.3
self._websocket.send_frame(str(data))
# Get the sever authentication type and make sure None is supported
data = self._websocket.receive_frame()
self.assertIsNotNone(data, 'Expected authentication type None.')
self.assertGreaterEqual(
len(data), 2, 'Expected authentication type None.')
self.assertIn(
1, [ord(data[i + 1]) for i in range(ord(data[0]))],
'Expected authentication type None.')
# Send to the server that we only support authentication type None
self._websocket.send_frame(six.int2byte(1))
# The server should send 4 bytes of 0's if security handshake succeeded
data = self._websocket.receive_frame()
self.assertEqual(
len(data), 4, 'Server did not think security was successful.')
self.assertEqual(
[ord(i) for i in data], [0, 0, 0, 0],
'Server did not think security was successful.')
# Say to leave the desktop as shared as part of client initialization
self._websocket.send_frame(six.int2byte(1))
# Get the server initialization packet back and make sure it is the
# right structure where bytes 20-24 is the name length and
# 24-N is the name
data = self._websocket.receive_frame()
data_length = len(data) if data is not None else 0
self.assertFalse(data_length <= 24 or
data_length != (struct.unpack(">L",
data[20:24])[0] + 24),
'Server initialization was not the right format.')
# Since the rest of the data on the screen is arbitrary, we will
# close the socket and end our validation of the data at this point
# Assert that the latest check was false, meaning that the server
# initialization was the right format
self.assertFalse(data_length <= 24 or
data_length != (struct.unpack(">L",
data[20:24])[0] + 24))
def _validate_websocket_upgrade(self):
self.assertTrue(
self._websocket.response.startswith('HTTP/1.1 101 Switching '
'Protocols\r\n'),
'Did not get the expected 101 on the websockify call: '
+ str(len(self._websocket.response)))
self.assertTrue(
self._websocket.response.find('Server: WebSockify') > 0,
'Did not get the expected WebSocket HTTP Response.')
def _create_websocket(self, url):
url = urlparse.urlparse(url)
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
client_socket.connect((url.hostname, url.port))
# Turn the Socket into a WebSocket to do the communication
return _WebSocket(client_socket, url)
@test.idempotent_id('c640fdff-8ab4-45a4-a5d8-7e6146cbd0dc')
def test_novnc(self):
body = self.client.get_vnc_console(self.server['id'],
type='novnc')['console']
self.assertEqual('novnc', body['type'])
# Do the initial HTTP Request to novncproxy to get the NoVNC JavaScript
self._validate_novnc_html(body['url'])
# Do the WebSockify HTTP Request to novncproxy to do the RFB connection
self._websocket = self._create_websocket(body['url'])
# Validate that we succesfully connected and upgraded to Web Sockets
self._validate_websocket_upgrade()
# Validate the RFB Negotiation to determine if a valid VNC session
self._validate_rfb_negotiation()
@test.idempotent_id('f9c79937-addc-4aaa-9e0e-841eef02aeb7')
def test_novnc_bad_token(self):
body = self.client.get_vnc_console(self.server['id'],
type='novnc')['console']
self.assertEqual('novnc', body['type'])
# Do the WebSockify HTTP Request to novncproxy with a bad token
url = body['url'].replace('token=', 'token=bad')
self._websocket = self._create_websocket(url)
# Make sure the novncproxy rejected the connection and closed it
data = self._websocket.receive_frame()
self.assertTrue(data is None or len(data) == 0,
"The novnc proxy actually sent us some data, but we "
"expected it to close the connection.")
class _WebSocket(object):
def __init__(self, client_socket, url):
"""Contructor for the WebSocket wrapper to the socket."""
self._socket = client_socket
# Upgrade the HTTP connection to a WebSocket
self._upgrade(url)
def receive_frame(self):
"""Wrapper for receiving data to parse the WebSocket frame format"""
# We need to loop until we either get some bytes back in the frame
# or no data was received (meaning the socket was closed). This is
# done to handle the case where we get back some empty frames
while True:
header = self._socket.recv(2)
# If we didn't receive any data, just return None
if len(header) == 0:
return None
# We will make the assumption that we are only dealing with
# frames less than 125 bytes here (for the negotiation) and
# that only the 2nd byte contains the length, and since the
# server doesn't do masking, we can just read the data length
if ord(header[1]) & 127 > 0:
return self._socket.recv(ord(header[1]) & 127)
def send_frame(self, data):
"""Wrapper for sending data to add in the WebSocket frame format."""
frame_bytes = list()
# For the first byte, want to say we are sending binary data (130)
frame_bytes.append(130)
# Only sending negotiation data so don't need to worry about > 125
# We do need to add the bit that says we are masking the data
frame_bytes.append(len(data) | 128)
# We don't really care about providing a random mask for security
# So we will just hard-code a value since a test program
mask = [7, 2, 1, 9]
for i in range(len(mask)):
frame_bytes.append(mask[i])
# Mask each of the actual data bytes that we are going to send
for i in range(len(data)):
frame_bytes.append(ord(data[i]) ^ mask[i % 4])
# Convert our integer list to a binary array of bytes
frame_bytes = struct.pack('!%iB' % len(frame_bytes), * frame_bytes)
self._socket.sendall(frame_bytes)
def close(self):
"""Helper method to close the connection."""
# Close down the real socket connection and exit the test program
if self._socket is not None:
self._socket.shutdown(1)
self._socket.close()
self._socket = None
def _upgrade(self, url):
"""Upgrade the HTTP connection to a WebSocket and verify."""
# The real request goes to the /websockify URI always
reqdata = 'GET /websockify HTTP/1.1\r\n'
reqdata += 'Host: %s:%s\r\n' % (url.hostname, url.port)
# Tell the HTTP Server to Upgrade the connection to a WebSocket
reqdata += 'Upgrade: websocket\r\nConnection: Upgrade\r\n'
# The token=xxx is sent as a Cookie not in the URI
reqdata += 'Cookie: %s\r\n' % url.query
# Use a hard-coded WebSocket key since a test program
reqdata += 'Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n'
reqdata += 'Sec-WebSocket-Version: 13\r\n'
# We are choosing to use binary even though browser may do Base64
reqdata += 'Sec-WebSocket-Protocol: binary\r\n\r\n'
# Send the HTTP GET request and get the response back
self._socket.sendall(reqdata)
self.response = data = self._socket.recv(4096)
# Loop through & concatenate all of the data in the response body
while len(data) > 0 and self.response.find('\r\n\r\n') < 0:
data = self._socket.recv(4096)
self.response += data