OpenStack Testing (Tempest) of an existing cloud
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

238 lines
10 KiB

  1. # Copyright 2016-2017 OpenStack Foundation
  2. # All Rights Reserved.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. import struct
  16. import six
  17. import six.moves.urllib.parse as urlparse
  18. import urllib3
  19. from tempest.api.compute import base
  20. from tempest.common import compute
  21. from tempest import config
  22. from tempest.lib import decorators
  23. CONF = config.CONF
  24. if six.PY2:
  25. ord_func = ord
  26. else:
  27. ord_func = int
  28. class NoVNCConsoleTestJSON(base.BaseV2ComputeTest):
  29. """Test novnc console"""
  30. create_default_network = True
  31. @classmethod
  32. def skip_checks(cls):
  33. super(NoVNCConsoleTestJSON, cls).skip_checks()
  34. if not CONF.compute_feature_enabled.vnc_console:
  35. raise cls.skipException('VNC Console feature is disabled.')
  36. def setUp(self):
  37. super(NoVNCConsoleTestJSON, self).setUp()
  38. self._websocket = None
  39. def tearDown(self):
  40. super(NoVNCConsoleTestJSON, self).tearDown()
  41. if self._websocket is not None:
  42. self._websocket.close()
  43. # NOTE(zhufl): Because server_check_teardown will raise Exception
  44. # which will prevent other cleanup steps from being executed, so
  45. # server_check_teardown should be called after super's tearDown.
  46. self.server_check_teardown()
  47. @classmethod
  48. def setup_clients(cls):
  49. super(NoVNCConsoleTestJSON, cls).setup_clients()
  50. cls.client = cls.servers_client
  51. @classmethod
  52. def resource_setup(cls):
  53. super(NoVNCConsoleTestJSON, cls).resource_setup()
  54. cls.server = cls.create_test_server(wait_until="ACTIVE")
  55. cls.use_get_remote_console = False
  56. if not cls.is_requested_microversion_compatible('2.5'):
  57. cls.use_get_remote_console = True
  58. def _validate_novnc_html(self, vnc_url):
  59. """Verify we can connect to novnc and get back the javascript."""
  60. resp = urllib3.PoolManager().request('GET', vnc_url)
  61. # Make sure that the GET request was accepted by the novncproxy
  62. self.assertEqual(resp.status, 200, 'Got a Bad HTTP Response on the '
  63. 'initial call: ' + six.text_type(resp.status))
  64. # Do some basic validation to make sure it is an expected HTML document
  65. resp_data = resp.data.decode()
  66. # This is needed in the case of example: <html lang="en">
  67. self.assertRegex(resp_data, '<html.*>',
  68. 'Not a valid html document in the response.')
  69. self.assertIn('</html>', resp_data,
  70. 'Not a valid html document in the response.')
  71. # Just try to make sure we got JavaScript back for noVNC, since we
  72. # won't actually use it since not inside of a browser
  73. self.assertIn('noVNC', resp_data,
  74. 'Not a valid noVNC javascript html document.')
  75. self.assertIn('<script', resp_data,
  76. 'Not a valid noVNC javascript html document.')
  77. def _validate_rfb_negotiation(self):
  78. """Verify we can connect to novnc and do the websocket connection."""
  79. # Turn the Socket into a WebSocket to do the communication
  80. data = self._websocket.receive_frame()
  81. self.assertFalse(data is None or not data,
  82. 'Token must be invalid because the connection '
  83. 'closed.')
  84. # Parse the RFB version from the data to make sure it is valid
  85. # and belong to the known supported RFB versions.
  86. version = float("%d.%d" % (int(data[4:7], base=10),
  87. int(data[8:11], base=10)))
  88. # Add the max RFB versions supported
  89. supported_versions = [3.3, 3.8]
  90. self.assertIn(version, supported_versions,
  91. 'Bad RFB Version: ' + str(version))
  92. # Send our RFB version to the server
  93. self._websocket.send_frame(data)
  94. # Get the sever authentication type and make sure None is supported
  95. data = self._websocket.receive_frame()
  96. self.assertIsNotNone(data, 'Expected authentication type None.')
  97. data_length = len(data)
  98. if version == 3.3:
  99. # For RFB 3.3: in the security handshake, rather than a two-way
  100. # negotiation, the server decides the security type and sends a
  101. # single word(4 bytes).
  102. self.assertEqual(
  103. data_length, 4, 'Expected authentication type None.')
  104. self.assertIn(1, [ord_func(data[i]) for i in (0, 3)],
  105. 'Expected authentication type None.')
  106. else:
  107. self.assertGreaterEqual(
  108. len(data), 2, 'Expected authentication type None.')
  109. self.assertIn(
  110. 1,
  111. [ord_func(data[i + 1]) for i in range(ord_func(data[0]))],
  112. 'Expected authentication type None.')
  113. # Send to the server that we only support authentication
  114. # type None
  115. self._websocket.send_frame(six.int2byte(1))
  116. # The server should send 4 bytes of 0's if security
  117. # handshake succeeded
  118. data = self._websocket.receive_frame()
  119. self.assertEqual(
  120. len(data), 4,
  121. 'Server did not think security was successful.')
  122. self.assertEqual(
  123. [ord_func(i) for i in data], [0, 0, 0, 0],
  124. 'Server did not think security was successful.')
  125. # Say to leave the desktop as shared as part of client initialization
  126. self._websocket.send_frame(six.int2byte(1))
  127. # Get the server initialization packet back and make sure it is the
  128. # right structure where bytes 20-24 is the name length and
  129. # 24-N is the name
  130. data = self._websocket.receive_frame()
  131. data_length = len(data) if data is not None else 0
  132. self.assertFalse(data_length <= 24 or
  133. data_length != (struct.unpack(">L",
  134. data[20:24])[0] + 24),
  135. 'Server initialization was not the right format.')
  136. # Since the rest of the data on the screen is arbitrary, we will
  137. # close the socket and end our validation of the data at this point
  138. # Assert that the latest check was false, meaning that the server
  139. # initialization was the right format
  140. self.assertFalse(data_length <= 24 or
  141. data_length != (struct.unpack(">L",
  142. data[20:24])[0] + 24))
  143. def _validate_websocket_upgrade(self):
  144. """Verify that the websocket upgrade was successful.
  145. Parses response and ensures that required response
  146. fields are present and accurate.
  147. (https://tools.ietf.org/html/rfc7231#section-6.2.2)
  148. """
  149. self.assertTrue(
  150. self._websocket.response.startswith(b'HTTP/1.1 101 Switching '
  151. b'Protocols'),
  152. 'Incorrect HTTP return status code: {}'.format(
  153. six.text_type(self._websocket.response)
  154. )
  155. )
  156. _required_header = 'upgrade: websocket'
  157. _response = six.text_type(self._websocket.response).lower()
  158. self.assertIn(
  159. _required_header,
  160. _response,
  161. 'Did not get the expected WebSocket HTTP Response.'
  162. )
  163. @decorators.idempotent_id('c640fdff-8ab4-45a4-a5d8-7e6146cbd0dc')
  164. def test_novnc(self):
  165. """Test accessing novnc console of server"""
  166. if self.use_get_remote_console:
  167. body = self.client.get_remote_console(
  168. self.server['id'], console_type='novnc',
  169. protocol='vnc')['remote_console']
  170. else:
  171. body = self.client.get_vnc_console(self.server['id'],
  172. type='novnc')['console']
  173. self.assertEqual('novnc', body['type'])
  174. # Do the initial HTTP Request to novncproxy to get the NoVNC JavaScript
  175. self._validate_novnc_html(body['url'])
  176. # Do the WebSockify HTTP Request to novncproxy to do the RFB connection
  177. self._websocket = compute.create_websocket(body['url'])
  178. # Validate that we successfully connected and upgraded to Web Sockets
  179. self._validate_websocket_upgrade()
  180. # Validate the RFB Negotiation to determine if a valid VNC session
  181. self._validate_rfb_negotiation()
  182. @decorators.idempotent_id('f9c79937-addc-4aaa-9e0e-841eef02aeb7')
  183. def test_novnc_bad_token(self):
  184. """Test accessing novnc console with bad token
  185. Do the WebSockify HTTP Request to novnc proxy with a bad token,
  186. the novnc proxy should reject the connection and closed it.
  187. """
  188. if self.use_get_remote_console:
  189. body = self.client.get_remote_console(
  190. self.server['id'], console_type='novnc',
  191. protocol='vnc')['remote_console']
  192. else:
  193. body = self.client.get_vnc_console(self.server['id'],
  194. type='novnc')['console']
  195. self.assertEqual('novnc', body['type'])
  196. # Do the WebSockify HTTP Request to novncproxy with a bad token
  197. parts = urlparse.urlparse(body['url'])
  198. qparams = urlparse.parse_qs(parts.query)
  199. if 'path' in qparams:
  200. qparams['path'] = urlparse.unquote(qparams['path'][0]).replace(
  201. 'token=', 'token=bad')
  202. elif 'token' in qparams:
  203. qparams['token'] = 'bad' + qparams['token'][0]
  204. new_query = urlparse.urlencode(qparams)
  205. new_parts = urlparse.ParseResult(parts.scheme, parts.netloc,
  206. parts.path, parts.params, new_query,
  207. parts.fragment)
  208. url = urlparse.urlunparse(new_parts)
  209. self._websocket = compute.create_websocket(url)
  210. # Make sure the novncproxy rejected the connection and closed it
  211. data = self._websocket.receive_frame()
  212. self.assertTrue(data is None or not data,
  213. "The novnc proxy actually sent us some data, but we "
  214. "expected it to close the connection.")