Merge "Switch to decorators.idempotent_id on identity"
diff --git a/releasenotes/notes/jsonschema-validator-2377ba131e12d3c7.yaml b/releasenotes/notes/jsonschema-validator-2377ba131e12d3c7.yaml
new file mode 100644
index 0000000..8817ed4
--- /dev/null
+++ b/releasenotes/notes/jsonschema-validator-2377ba131e12d3c7.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - Added customized JSON schema format checker for 'date-time' format.
+ Compute response schema will be validated against customized format
+ checker.
diff --git a/tempest/api/compute/servers/test_novnc.py b/tempest/api/compute/servers/test_novnc.py
new file mode 100644
index 0000000..d10f370
--- /dev/null
+++ b/tempest/api/compute/servers/test_novnc.py
@@ -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
diff --git a/tempest/api/compute/servers/test_servers_negative.py b/tempest/api/compute/servers/test_servers_negative.py
index 853d2ff..b22a434 100644
--- a/tempest/api/compute/servers/test_servers_negative.py
+++ b/tempest/api/compute/servers/test_servers_negative.py
@@ -178,6 +178,7 @@
self.client.rebuild_server,
server['id'], self.image_ref_alt)
+ @test.related_bug('1660878', status_code=409)
@test.attr(type=['negative'])
@decorators.idempotent_id('581a397d-5eab-486f-9cf9-1014bbd4c984')
def test_reboot_deleted_server(self):
diff --git a/tempest/api/volume/admin/test_volume_types_negative.py b/tempest/api/volume/admin/test_volume_types_negative.py
index 857e7d2..5332f1e 100644
--- a/tempest/api/volume/admin/test_volume_types_negative.py
+++ b/tempest/api/volume/admin/test_volume_types_negative.py
@@ -51,6 +51,14 @@
self.admin_volume_types_client.delete_volume_type,
data_utils.rand_uuid())
+ @test.idempotent_id('8c09f849-f225-4d78-ba87-bffd9a5e0c6f')
+ def test_create_volume_with_private_volume_type(self):
+ # Should not be able to create volume with private volume type.
+ params = {'os-volume-type-access:is_public': False}
+ volume_type = self.create_volume_type(**params)
+ self.assertRaises(lib_exc.NotFound,
+ self.create_volume, volume_type=volume_type['id'])
+
class VolumeTypesNegativeV1Test(VolumeTypesNegativeV2Test):
_api_version = 1
diff --git a/tempest/api/volume/v2/test_volumes_snapshots_list.py b/tempest/api/volume/v2/test_volumes_snapshots_list.py
new file mode 100644
index 0000000..f389b59
--- /dev/null
+++ b/tempest/api/volume/v2/test_volumes_snapshots_list.py
@@ -0,0 +1,94 @@
+# Copyright 2016 Red Hat, Inc.
+# 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.
+
+from tempest.api.volume import base
+from tempest import config
+from tempest import test
+
+CONF = config.CONF
+
+
+class VolumesV2SnapshotListTestJSON(base.BaseVolumeTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(VolumesV2SnapshotListTestJSON, cls).skip_checks()
+ if not CONF.volume_feature_enabled.snapshot:
+ raise cls.skipException("Cinder volume snapshots are disabled")
+
+ @classmethod
+ def resource_setup(cls):
+ super(VolumesV2SnapshotListTestJSON, cls).resource_setup()
+ cls.snapshot_id_list = []
+ # Create a volume
+ cls.volume_origin = cls.create_volume()
+ cls.name_field = cls.special_fields['name_field']
+ # Create 3 snapshots
+ for _ in range(3):
+ snapshot = cls.create_snapshot(cls.volume_origin['id'])
+ cls.snapshot_id_list.append(snapshot['id'])
+
+ def _list_snapshots_param_sort(self, sort_key, sort_dir):
+ """list snapshots by sort param"""
+ snap_list = self.snapshots_client.list_snapshots(
+ sort_key=sort_key, sort_dir=sort_dir)['snapshots']
+ self.assertNotEmpty(snap_list)
+ if sort_key is 'display_name':
+ sort_key = 'name'
+ # Note: On Cinder V2 API, 'display_name' works as a sort key
+ # on a request, a volume name appears as 'name' on the response.
+ # So Tempest needs to change the key name here for this inconsistent
+ # API behavior.
+ sorted_list = [snapshot[sort_key] for snapshot in snap_list]
+ msg = 'The list of snapshots was not sorted correctly.'
+ self.assertEqual(sorted(sorted_list, reverse=(sort_dir == 'desc')),
+ sorted_list, msg)
+
+ @test.idempotent_id('c5513ada-64c1-4d28-83b9-af3307ec1388')
+ def test_snapshot_list_param_sort_id_asc(self):
+ self._list_snapshots_param_sort(sort_key='id', sort_dir='asc')
+
+ @test.idempotent_id('8a7fe058-0b41-402a-8afd-2dbc5a4a718b')
+ def test_snapshot_list_param_sort_id_desc(self):
+ self._list_snapshots_param_sort(sort_key='id', sort_dir='desc')
+
+ @test.idempotent_id('4052c3a0-2415-440a-a8cc-305a875331b0')
+ def test_snapshot_list_param_sort_created_at_asc(self):
+ self._list_snapshots_param_sort(sort_key='created_at', sort_dir='asc')
+
+ @test.idempotent_id('dcbbe24a-f3c0-4ec8-9274-55d48db8d1cf')
+ def test_snapshot_list_param_sort_created_at_desc(self):
+ self._list_snapshots_param_sort(sort_key='created_at', sort_dir='desc')
+
+ @test.idempotent_id('d58b5fed-0c37-42d3-8c5d-39014ac13c00')
+ def test_snapshot_list_param_sort_name_asc(self):
+ self._list_snapshots_param_sort(sort_key='display_name',
+ sort_dir='asc')
+
+ @test.idempotent_id('96ba6f4d-1f18-47e1-b4bc-76edc6c21250')
+ def test_snapshot_list_param_sort_name_desc(self):
+ self._list_snapshots_param_sort(sort_key='display_name',
+ sort_dir='desc')
+
+ @test.idempotent_id('05489dde-44bc-4961-a1f5-3ce7ee7824f7')
+ def test_snapshot_list_param_marker(self):
+ # The list of snapshots should end before the provided marker
+ params = {'marker': self.snapshot_id_list[1]}
+ snap_list = self.snapshots_client.list_snapshots(**params)['snapshots']
+ fetched_list_id = [snap['id'] for snap in snap_list]
+ # Verify the list of snapshots ends before the provided
+ # marker(second snapshot), therefore only the first snapshot
+ # should displayed.
+ self.assertEqual(self.snapshot_id_list[:1], fetched_list_id)
diff --git a/tempest/clients.py b/tempest/clients.py
index 18116f3..cdd6925 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -31,14 +31,6 @@
default_params = config.service_client_config()
- # TODO(jordanP): remove this once no Tempest plugin use that class
- # variable.
- default_params_with_timeout_values = {
- 'build_interval': CONF.compute.build_interval,
- 'build_timeout': CONF.compute.build_timeout
- }
- default_params_with_timeout_values.update(default_params)
-
def __init__(self, credentials, scope='project'):
"""Initialization of Manager class.
diff --git a/tempest/common/compute.py b/tempest/common/compute.py
index 4f2fe67..01de704 100644
--- a/tempest/common/compute.py
+++ b/tempest/common/compute.py
@@ -30,8 +30,7 @@
def create_test_server(clients, validatable=False, validation_resources=None,
tenant_network=None, wait_until=None,
volume_backed=False, name=None, flavor=None,
- image_id=None, delete_vol_on_termination=True,
- **kwargs):
+ image_id=None, **kwargs):
"""Common wrapper utility returning a test server.
This method is a common wrapper returning a test server that can be
@@ -44,16 +43,30 @@
:param tenant_network: Tenant network to be used for creating a server.
:param wait_until: Server status to wait for the server to reach after
its creation.
- :param volume_backed: Whether the instance is volume backed or not.
+ :param volume_backed: Whether the server is volume backed or not.
+ If this is true, a volume will be created and
+ create server will be requested with
+ 'block_device_mapping_v2' populated with below
+ values:
+ --------------------------------------------
+ bd_map_v2 = [{
+ 'uuid': volume['volume']['id'],
+ 'source_type': 'volume',
+ 'destination_type': 'volume',
+ 'boot_index': 0,
+ 'delete_on_termination': True}]
+ kwargs['block_device_mapping_v2'] = bd_map_v2
+ ---------------------------------------------
+ If server needs to be booted from volume with other
+ combination of bdm inputs than mentioned above, then
+ pass the bdm inputs explicitly as kwargs and image_id
+ as empty string ('').
:param name: Name of the server to be provisioned. If not defined a random
string ending with '-instance' will be generated.
:param flavor: Flavor of the server to be provisioned. If not defined,
CONF.compute.flavor_ref will be used instead.
:param image_id: ID of the image to be used to provision the server. If not
defined, CONF.compute.image_ref will be used instead.
- :param delete_vol_on_termination: Controls whether the backing volume
- should be deleted when the server is deleted. Only applies to volume
- backed servers.
:returns: a tuple
"""
@@ -103,12 +116,14 @@
if volume_backed:
volume_name = data_utils.rand_name(__name__ + '-volume')
volumes_client = clients.volumes_v2_client
- if CONF.volume_feature_enabled.api_v1:
+ name_field = 'name'
+ if not CONF.volume_feature_enabled.api_v2:
volumes_client = clients.volumes_client
- volume = volumes_client.create_volume(
- display_name=volume_name,
- imageRef=image_id,
- size=CONF.volume.volume_size)
+ name_field = 'display_name'
+ params = {name_field: volume_name,
+ 'imageRef': image_id,
+ 'size': CONF.volume.volume_size}
+ volume = volumes_client.create_volume(**params)
waiters.wait_for_volume_status(volumes_client,
volume['volume']['id'], 'available')
@@ -117,7 +132,7 @@
'source_type': 'volume',
'destination_type': 'volume',
'boot_index': 0,
- 'delete_on_termination': delete_vol_on_termination}]
+ 'delete_on_termination': True}]
kwargs['block_device_mapping_v2'] = bd_map_v2
# Since this is boot from volume an image does not need
diff --git a/tempest/lib/api_schema/response/compute/v2_1/aggregates.py b/tempest/lib/api_schema/response/compute/v2_1/aggregates.py
index 1a9fe41..3289a34 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/aggregates.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/aggregates.py
@@ -14,17 +14,19 @@
import copy
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
# create-aggregate api doesn't have 'hosts' and 'metadata' attributes.
aggregate_for_create = {
'type': 'object',
'properties': {
'availability_zone': {'type': ['string', 'null']},
- 'created_at': {'type': 'string'},
+ 'created_at': parameter_types.date_time,
'deleted': {'type': 'boolean'},
- 'deleted_at': {'type': ['string', 'null']},
+ 'deleted_at': parameter_types.date_time_or_null,
'id': {'type': 'integer'},
'name': {'type': 'string'},
- 'updated_at': {'type': ['string', 'null']}
+ 'updated_at': parameter_types.date_time_or_null
},
'additionalProperties': False,
'required': ['availability_zone', 'created_at', 'deleted',
@@ -69,9 +71,7 @@
# The 'updated_at' attribute of 'update_aggregate' can't be null.
update_aggregate = copy.deepcopy(get_aggregate)
update_aggregate['response_body']['properties']['aggregate']['properties'][
- 'updated_at'] = {
- 'type': 'string'
- }
+ 'updated_at'] = parameter_types.date_time
delete_aggregate = {
'status_code': [200]
diff --git a/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py b/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py
index d9aebce..f7b77a1 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/availability_zone.py
@@ -14,6 +14,8 @@
import copy
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
base = {
'status_code': [200],
@@ -61,7 +63,7 @@
'properties': {
'available': {'type': 'boolean'},
'active': {'type': 'boolean'},
- 'updated_at': {'type': ['string', 'null']}
+ 'updated_at': parameter_types.date_time_or_null
},
'additionalProperties': False,
'required': ['available', 'active', 'updated_at']
diff --git a/tempest/lib/api_schema/response/compute/v2_1/extensions.py b/tempest/lib/api_schema/response/compute/v2_1/extensions.py
index a6a455c..b5962d7 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/extensions.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/extensions.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
list_extensions = {
'status_code': [200],
'response_body': {
@@ -22,10 +24,7 @@
'items': {
'type': 'object',
'properties': {
- 'updated': {
- 'type': 'string',
- 'format': 'data-time'
- },
+ 'updated': parameter_types.date_time,
'name': {'type': 'string'},
'links': {'type': 'array'},
'namespace': {
diff --git a/tempest/lib/api_schema/response/compute/v2_1/images.py b/tempest/lib/api_schema/response/compute/v2_1/images.py
index f65b9d8..156ff4a 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/images.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/images.py
@@ -26,10 +26,10 @@
'properties': {
'id': {'type': 'string'},
'status': {'enum': image_status_enums},
- 'updated': {'type': 'string'},
+ 'updated': parameter_types.date_time,
'links': image_links,
'name': {'type': ['string', 'null']},
- 'created': {'type': 'string'},
+ 'created': parameter_types.date_time,
'minDisk': {'type': 'integer'},
'minRam': {'type': 'integer'},
'progress': {'type': 'integer'},
diff --git a/tempest/lib/api_schema/response/compute/v2_1/keypairs.py b/tempest/lib/api_schema/response/compute/v2_1/keypairs.py
index 9c04c79..2828097 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/keypairs.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/keypairs.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
get_keypair = {
'status_code': [200],
'response_body': {
@@ -25,9 +27,9 @@
'fingerprint': {'type': 'string'},
'user_id': {'type': 'string'},
'deleted': {'type': 'boolean'},
- 'created_at': {'type': 'string'},
- 'updated_at': {'type': ['string', 'null']},
- 'deleted_at': {'type': ['string', 'null']},
+ 'created_at': parameter_types.date_time,
+ 'updated_at': parameter_types.date_time_or_null,
+ 'deleted_at': parameter_types.date_time_or_null,
'id': {'type': 'integer'}
},
diff --git a/tempest/lib/api_schema/response/compute/v2_1/migrations.py b/tempest/lib/api_schema/response/compute/v2_1/migrations.py
index b7d66ea..c50286d 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/migrations.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/migrations.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
list_migrations = {
'status_code': [200],
'response_body': {
@@ -32,8 +34,8 @@
'dest_host': {'type': ['string', 'null']},
'old_instance_type_id': {'type': ['integer', 'null']},
'new_instance_type_id': {'type': ['integer', 'null']},
- 'created_at': {'type': 'string'},
- 'updated_at': {'type': ['string', 'null']}
+ 'created_at': parameter_types.date_time,
+ 'updated_at': parameter_types.date_time_or_null
},
'additionalProperties': False,
'required': [
diff --git a/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py b/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py
index 3cc5ca4..a3c9099 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/parameter_types.py
@@ -81,6 +81,16 @@
}
}
+date_time = {
+ 'type': 'string',
+ 'format': 'iso8601-date-time'
+}
+
+date_time_or_null = {
+ 'type': ['string', 'null'],
+ 'format': 'iso8601-date-time'
+}
+
response_header = {
'connection': {'type': 'string'},
'content-length': {'type': 'string'},
@@ -89,9 +99,14 @@
'x-compute-request-id': {'type': 'string'},
'vary': {'type': 'string'},
'x-openstack-nova-api-version': {'type': 'string'},
+ # NOTE(gmann): Validating this as string only as this
+ # date in header is returned in different format than
+ # ISO 8601 date time format which is not consistent with
+ # other date-time format in nova.
+ # This API is already deprecated so not worth to fix
+ # on nova side.
'date': {
- 'type': 'string',
- 'format': 'data-time'
+ 'type': 'string'
}
}
diff --git a/tempest/lib/api_schema/response/compute/v2_1/servers.py b/tempest/lib/api_schema/response/compute/v2_1/servers.py
index 1264416..4ccca6f 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/servers.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/servers.py
@@ -118,8 +118,8 @@
},
'user_id': {'type': 'string'},
'tenant_id': {'type': 'string'},
- 'created': {'type': 'string'},
- 'updated': {'type': 'string'},
+ 'created': parameter_types.date_time,
+ 'updated': parameter_types.date_time,
'progress': {'type': 'integer'},
'metadata': {'type': 'object'},
'links': parameter_types.links,
@@ -402,7 +402,7 @@
'request_id': {'type': 'string'},
'user_id': {'type': 'string'},
'project_id': {'type': 'string'},
- 'start_time': {'type': 'string'},
+ 'start_time': parameter_types.date_time,
'message': {'type': ['string', 'null']},
'instance_uuid': {'type': 'string'}
},
@@ -417,8 +417,8 @@
'type': 'object',
'properties': {
'event': {'type': 'string'},
- 'start_time': {'type': 'string'},
- 'finish_time': {'type': 'string'},
+ 'start_time': parameter_types.date_time,
+ 'finish_time': parameter_types.date_time,
'result': {'type': 'string'},
'traceback': {'type': ['string', 'null']}
},
diff --git a/tempest/lib/api_schema/response/compute/v2_1/services.py b/tempest/lib/api_schema/response/compute/v2_1/services.py
index ddef7b2..6949f86 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/services.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/services.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
list_services = {
'status_code': [200],
'response_body': {
@@ -29,7 +31,7 @@
'state': {'type': 'string'},
'binary': {'type': 'string'},
'status': {'type': 'string'},
- 'updated_at': {'type': ['string', 'null']},
+ 'updated_at': parameter_types.date_time_or_null,
'disabled_reason': {'type': ['string', 'null']}
},
'additionalProperties': False,
diff --git a/tempest/lib/api_schema/response/compute/v2_1/snapshots.py b/tempest/lib/api_schema/response/compute/v2_1/snapshots.py
index 01a524b..826f854 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/snapshots.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/snapshots.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
common_snapshot_info = {
'type': 'object',
'properties': {
@@ -20,7 +22,7 @@
'volumeId': {'type': 'string'},
'status': {'type': 'string'},
'size': {'type': 'integer'},
- 'createdAt': {'type': 'string'},
+ 'createdAt': parameter_types.date_time,
'displayName': {'type': ['string', 'null']},
'displayDescription': {'type': ['string', 'null']}
},
diff --git a/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py b/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py
index d51ef12..b531d2e 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/tenant_usages.py
@@ -14,24 +14,21 @@
import copy
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
_server_usages = {
'type': 'array',
'items': {
'type': 'object',
'properties': {
- 'ended_at': {
- 'oneOf': [
- {'type': 'string'},
- {'type': 'null'}
- ]
- },
+ 'ended_at': parameter_types.date_time_or_null,
'flavor': {'type': 'string'},
'hours': {'type': 'number'},
'instance_id': {'type': 'string'},
'local_gb': {'type': 'integer'},
'memory_mb': {'type': 'integer'},
'name': {'type': 'string'},
- 'started_at': {'type': 'string'},
+ 'started_at': parameter_types.date_time,
'state': {'type': 'string'},
'tenant_id': {'type': 'string'},
'uptime': {'type': 'integer'},
@@ -47,8 +44,8 @@
'type': 'object',
'properties': {
'server_usages': _server_usages,
- 'start': {'type': 'string'},
- 'stop': {'type': 'string'},
+ 'start': parameter_types.date_time,
+ 'stop': parameter_types.date_time,
'tenant_id': {'type': 'string'},
'total_hours': {'type': 'number'},
'total_local_gb_usage': {'type': 'number'},
diff --git a/tempest/lib/api_schema/response/compute/v2_1/versions.py b/tempest/lib/api_schema/response/compute/v2_1/versions.py
index 08a9fab..7f56239 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/versions.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/versions.py
@@ -14,6 +14,8 @@
import copy
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
_version = {
'type': 'object',
@@ -33,7 +35,7 @@
}
},
'status': {'type': 'string'},
- 'updated': {'type': 'string', 'format': 'date-time'},
+ 'updated': parameter_types.date_time,
'version': {'type': 'string'},
'min_version': {'type': 'string'},
'media-types': {
diff --git a/tempest/lib/api_schema/response/compute/v2_1/volumes.py b/tempest/lib/api_schema/response/compute/v2_1/volumes.py
index bb34acb..c35dae9 100644
--- a/tempest/lib/api_schema/response/compute/v2_1/volumes.py
+++ b/tempest/lib/api_schema/response/compute/v2_1/volumes.py
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+
create_get_volume = {
'status_code': [200],
'response_body': {
@@ -24,7 +26,7 @@
'status': {'type': 'string'},
'displayName': {'type': ['string', 'null']},
'availabilityZone': {'type': 'string'},
- 'createdAt': {'type': 'string'},
+ 'createdAt': parameter_types.date_time,
'displayDescription': {'type': ['string', 'null']},
'volumeType': {'type': ['string', 'null']},
'snapshotId': {'type': ['string', 'null']},
@@ -75,7 +77,7 @@
'status': {'type': 'string'},
'displayName': {'type': ['string', 'null']},
'availabilityZone': {'type': 'string'},
- 'createdAt': {'type': 'string'},
+ 'createdAt': parameter_types.date_time,
'displayDescription': {'type': ['string', 'null']},
'volumeType': {'type': ['string', 'null']},
'snapshotId': {'type': ['string', 'null']},
diff --git a/tempest/lib/common/jsonschema_validator.py b/tempest/lib/common/jsonschema_validator.py
new file mode 100644
index 0000000..bbdf382
--- /dev/null
+++ b/tempest/lib/common/jsonschema_validator.py
@@ -0,0 +1,39 @@
+# Copyright 2016 NEC Corporation.
+# 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 jsonschema
+from oslo_utils import timeutils
+
+# JSON Schema validator and format checker used for JSON Schema validation
+JSONSCHEMA_VALIDATOR = jsonschema.Draft4Validator
+FORMAT_CHECKER = jsonschema.draft4_format_checker
+
+
+# NOTE(gmann): Add customized format checker for 'date-time' format because:
+# 1. jsonschema needs strict_rfc3339 or isodate module to be installed
+# for proper date-time checking as per rfc3339.
+# 2. Nova or other OpenStack components handle the date time format as
+# ISO 8601 which is defined in oslo_utils.timeutils
+# so this checker will validate the date-time as defined in
+# oslo_utils.timeutils
+@FORMAT_CHECKER.checks('iso8601-date-time')
+def _validate_datetime_format(instance):
+ try:
+ if isinstance(instance, jsonschema.compat.str_types):
+ timeutils.parse_isotime(instance)
+ except ValueError:
+ return False
+ else:
+ return True
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index 2c36f55..d0e21ff 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -25,6 +25,7 @@
import six
from tempest.lib.common import http
+from tempest.lib.common import jsonschema_validator
from tempest.lib.common.utils import test_utils
from tempest.lib import exceptions
@@ -38,8 +39,8 @@
HTTP_REDIRECTION = (300, 301, 302, 303, 304, 305, 306, 307)
# JSON Schema validator and format checker used for JSON Schema validation
-JSONSCHEMA_VALIDATOR = jsonschema.Draft4Validator
-FORMAT_CHECKER = jsonschema.draft4_format_checker
+JSONSCHEMA_VALIDATOR = jsonschema_validator.JSONSCHEMA_VALIDATOR
+FORMAT_CHECKER = jsonschema_validator.FORMAT_CHECKER
class RestClient(object):
diff --git a/tempest/lib/common/ssh.py b/tempest/lib/common/ssh.py
index 4226cd6..5e65bee 100644
--- a/tempest/lib/common/ssh.py
+++ b/tempest/lib/common/ssh.py
@@ -37,7 +37,30 @@
def __init__(self, host, username, password=None, timeout=300, pkey=None,
channel_timeout=10, look_for_keys=False, key_filename=None,
- port=22):
+ port=22, proxy_client=None):
+ """SSH client.
+
+ Many of parameters are just passed to the underlying implementation
+ as it is. See the paramiko documentation for more details.
+ http://docs.paramiko.org/en/2.1/api/client.html#paramiko.client.SSHClient.connect
+
+ :param host: Host to login.
+ :param username: SSH username.
+ :param password: SSH password, or a password to unlock private key.
+ :param timeout: Timeout in seconds, including retries.
+ Default is 300 seconds.
+ :param pkey: Private key.
+ :param channel_timeout: Channel timeout in seconds, passed to the
+ paramiko. Default is 10 seconds.
+ :param look_for_keys: Whether or not to search for private keys
+ in ``~/.ssh``. Default is False.
+ :param key_filename: Filename for private key to use.
+ :param port: SSH port number.
+ :param proxy_client: Another SSH client to provide a transport
+ for ssh-over-ssh. The default is None, which means
+ not to use ssh-over-ssh.
+ :type proxy_client: ``tempest.lib.common.ssh.Client`` object
+ """
self.host = host
self.username = username
self.port = port
@@ -51,6 +74,8 @@
self.timeout = int(timeout)
self.channel_timeout = float(channel_timeout)
self.buf_size = 1024
+ self.proxy_client = proxy_client
+ self._proxy_conn = None
def _get_ssh_connection(self, sleep=1.5, backoff=1):
"""Returns an ssh connection to the specified host."""
@@ -59,6 +84,10 @@
ssh.set_missing_host_key_policy(
paramiko.AutoAddPolicy())
_start_time = time.time()
+ if self.proxy_client is not None:
+ proxy_chan = self._get_proxy_channel()
+ else:
+ proxy_chan = None
if self.pkey is not None:
LOG.info("Creating ssh connection to '%s:%d' as '%s'"
" with public key authentication",
@@ -74,7 +103,8 @@
password=self.password,
look_for_keys=self.look_for_keys,
key_filename=self.key_filename,
- timeout=self.channel_timeout, pkey=self.pkey)
+ timeout=self.channel_timeout, pkey=self.pkey,
+ sock=proxy_chan)
LOG.info("ssh connection to %s@%s successfully created",
self.username, self.host)
return ssh
@@ -175,3 +205,14 @@
"""Raises an exception when we can not connect to server via ssh."""
connection = self._get_ssh_connection()
connection.close()
+
+ def _get_proxy_channel(self):
+ conn = self.proxy_client._get_ssh_connection()
+ # Keep a reference to avoid g/c
+ # https://github.com/paramiko/paramiko/issues/440
+ self._proxy_conn = conn
+ transport = conn.get_transport()
+ chan = transport.open_session()
+ cmd = 'nc %s %s' % (self.host, self.port)
+ chan.exec_command(cmd)
+ return chan
diff --git a/tempest/tests/lib/common/test_jsonschema_validator.py b/tempest/tests/lib/common/test_jsonschema_validator.py
new file mode 100644
index 0000000..8694f3d
--- /dev/null
+++ b/tempest/tests/lib/common/test_jsonschema_validator.py
@@ -0,0 +1,83 @@
+# Copyright 2016 NEC Corporation. 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.
+
+from tempest.lib.api_schema.response.compute.v2_1 import parameter_types
+from tempest.lib.common import rest_client
+from tempest.lib import exceptions
+from tempest.tests import base
+from tempest.tests.lib import fake_http
+
+
+class TestJSONSchemaDateTimeFormat(base.TestCase):
+ date_time_schema = [
+ {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'date-time': parameter_types.date_time
+ }
+ }
+ },
+ {
+ 'status_code': [200],
+ 'response_body': {
+ 'type': 'object',
+ 'properties': {
+ 'date-time': parameter_types.date_time_or_null
+ }
+ }
+ }
+ ]
+
+ def test_valid_date_time_format(self):
+ valid_instances = ['2016-10-02T10:00:00-05:00',
+ '2016-10-02T10:00:00+09:00',
+ '2016-10-02T15:00:00Z',
+ '2016-10-02T15:00:00.05Z']
+ resp = fake_http.fake_http_response('', status=200)
+ for instance in valid_instances:
+ body = {'date-time': instance}
+ for schema in self.date_time_schema:
+ rest_client.RestClient.validate_response(schema, resp, body)
+
+ def test_invalid_date_time_format(self):
+ invalid_instances = ['2016-10-02 T10:00:00-05:00',
+ '2016-10-02T 15:00:00',
+ '2016-10-02T15:00:00.05 Z',
+ '2016-10-02:15:00:00.05Z',
+ 'T15:00:00.05Z',
+ '2016:10:02T15:00:00',
+ '2016-10-02T15-00-00',
+ '2016-10-02T15.05Z',
+ '09MAR2015 11:15',
+ '13 Oct 2015 05:55:36 GMT',
+ '']
+ resp = fake_http.fake_http_response('', status=200)
+ for instance in invalid_instances:
+ body = {'date-time': instance}
+ for schema in self.date_time_schema:
+ self.assertRaises(exceptions.InvalidHTTPResponseBody,
+ rest_client.RestClient.validate_response,
+ schema, resp, body)
+
+ def test_date_time_or_null_format(self):
+ instance = None
+ resp = fake_http.fake_http_response('', status=200)
+ body = {'date-time': instance}
+ rest_client.RestClient.validate_response(self.date_time_schema[1],
+ resp, body)
+ self.assertRaises(exceptions.InvalidHTTPResponseBody,
+ rest_client.RestClient.validate_response,
+ self.date_time_schema[0], resp, body)
diff --git a/tempest/tests/lib/services/compute/test_servers_client.py b/tempest/tests/lib/services/compute/test_servers_client.py
index 93550fd..adfaaf2 100644
--- a/tempest/tests/lib/services/compute/test_servers_client.py
+++ b/tempest/tests/lib/services/compute/test_servers_client.py
@@ -154,7 +154,7 @@
"request_id": "16fb98f-46ca-475e-917e-2563e5a8cd19",
"user_id": "16fb98f-46ca-475e-917e-2563e5a8cd12",
"project_id": "16fb98f-46ca-475e-917e-2563e5a8cd34",
- "start_time": "09MAR2015 11:15",
+ "start_time": "2016-10-02T10:00:00-05:00",
"message": "fake-msg",
"instance_uuid": "16fb98f-46ca-475e-917e-2563e5a8cd12"
}
@@ -166,8 +166,8 @@
FAKE_INSTANCE_ACTION_EVENTS = {
"event": "fake-event",
- "start_time": "09MAR2015 11:15",
- "finish_time": "09MAR2015 11:15",
+ "start_time": "2016-10-02T10:00:00-05:00",
+ "finish_time": "2016-10-02T10:00:00-05:00",
"result": "fake-result",
"traceback": "fake-trace-back"
}
diff --git a/tempest/tests/lib/services/identity/v3/test_roles_client.py b/tempest/tests/lib/services/identity/v3/test_roles_client.py
index bad1ef9..4f70b47 100644
--- a/tempest/tests/lib/services/identity/v3/test_roles_client.py
+++ b/tempest/tests/lib/services/identity/v3/test_roles_client.py
@@ -18,32 +18,40 @@
class TestRolesClient(base.BaseServiceTest):
+
+ FAKE_ROLE_ID = "1"
+ FAKE_ROLE_NAME = "test"
+ FAKE_DOMAIN_ID = "1"
+
+ FAKE_ROLE_ID_2 = "2"
+ FAKE_ROLE_NAME_2 = "test2"
+
FAKE_ROLE_INFO = {
"role": {
- "domain_id": "1",
- "id": "1",
- "name": "test",
- "links": "example.com"
+ "domain_id": FAKE_DOMAIN_ID,
+ "id": FAKE_ROLE_ID,
+ "name": FAKE_ROLE_NAME,
+ "links": {
+ "self": "http://example.com/identity/v3/roles/%s" % (
+ FAKE_ROLE_ID)
+ }
}
}
- FAKE_LIST_ROLES = {
- "roles": [
- {
- "domain_id": "1",
- "id": "1",
- "name": "test",
- "links": "example.com"
- },
- {
- "domain_id": "2",
- "id": "2",
- "name": "test2",
- "links": "example.com"
+ FAKE_ROLE_INFO_2 = {
+ "role": {
+ "domain_id": FAKE_DOMAIN_ID,
+ "id": FAKE_ROLE_ID_2,
+ "name": FAKE_ROLE_NAME_2,
+ "links": {
+ "self": "http://example.com/identity/v3/roles/%s" % (
+ FAKE_ROLE_ID_2)
}
- ]
+ }
}
+ FAKE_LIST_ROLES = {"roles": [FAKE_ROLE_INFO, FAKE_ROLE_INFO_2]}
+
def setUp(self):
super(TestRolesClient, self).setUp()
fake_auth = fake_auth_provider.FakeAuthProvider()
@@ -56,8 +64,8 @@
'tempest.lib.common.rest_client.RestClient.post',
self.FAKE_ROLE_INFO,
bytes_body,
- domain_id="1",
- name="test",
+ domain_id=self.FAKE_DOMAIN_ID,
+ name=self.FAKE_ROLE_NAME,
status=201)
def _test_show_role(self, bytes_body=False):
@@ -66,7 +74,7 @@
'tempest.lib.common.rest_client.RestClient.get',
self.FAKE_ROLE_INFO,
bytes_body,
- role_id="1")
+ role_id=self.FAKE_ROLE_ID)
def _test_list_roles(self, bytes_body=False):
self.check_service_client_function(
@@ -81,8 +89,8 @@
'tempest.lib.common.rest_client.RestClient.patch',
self.FAKE_ROLE_INFO,
bytes_body,
- role_id="1",
- name="test")
+ role_id=self.FAKE_ROLE_ID,
+ name=self.FAKE_ROLE_NAME)
def _test_create_user_role_on_project(self, bytes_body=False):
self.check_service_client_function(
@@ -193,7 +201,7 @@
self.client.delete_role,
'tempest.lib.common.rest_client.RestClient.delete',
{},
- role_id="1",
+ role_id=self.FAKE_ROLE_ID,
status=204)
def test_create_user_role_on_project_with_str_body(self):
diff --git a/tempest/tests/lib/test_ssh.py b/tempest/tests/lib/test_ssh.py
index 8a0a84c..a16da1c 100644
--- a/tempest/tests/lib/test_ssh.py
+++ b/tempest/tests/lib/test_ssh.py
@@ -75,7 +75,54 @@
key_filename=None,
look_for_keys=False,
timeout=10.0,
- password=None
+ password=None,
+ sock=None
+ )]
+ self.assertEqual(expected_connect, client_mock.connect.mock_calls)
+ self.assertEqual(0, s_mock.call_count)
+
+ def test_get_ssh_connection_over_ssh(self):
+ c_mock, aa_mock, client_mock = self._set_ssh_connection_mocks()
+ proxy_client_mock = mock.MagicMock()
+ proxy_client_mock.connect.return_value = True
+ s_mock = self.patch('time.sleep')
+
+ c_mock.side_effect = [client_mock, proxy_client_mock]
+ aa_mock.return_value = mock.sentinel.aa
+
+ proxy_client = ssh.Client('proxy-host', 'proxy-user', timeout=2)
+ client = ssh.Client('localhost', 'root', timeout=2,
+ proxy_client=proxy_client)
+ client._get_ssh_connection(sleep=1)
+
+ aa_mock.assert_has_calls([mock.call(), mock.call()])
+ proxy_client_mock.set_missing_host_key_policy.assert_called_once_with(
+ mock.sentinel.aa)
+ proxy_expected_connect = [mock.call(
+ 'proxy-host',
+ port=22,
+ username='proxy-user',
+ pkey=None,
+ key_filename=None,
+ look_for_keys=False,
+ timeout=10.0,
+ password=None,
+ sock=None
+ )]
+ self.assertEqual(proxy_expected_connect,
+ proxy_client_mock.connect.mock_calls)
+ client_mock.set_missing_host_key_policy.assert_called_once_with(
+ mock.sentinel.aa)
+ expected_connect = [mock.call(
+ 'localhost',
+ port=22,
+ username='root',
+ pkey=None,
+ key_filename=None,
+ look_for_keys=False,
+ timeout=10.0,
+ password=None,
+ sock=proxy_client_mock.get_transport().open_session()
)]
self.assertEqual(expected_connect, client_mock.connect.mock_calls)
self.assertEqual(0, s_mock.call_count)