blob: cd85ede07a4ef9990d574584f8f936400a5e3dc9 [file] [log] [blame]
Joseph Lanouxb3e1f872015-01-30 11:13:07 +00001# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain 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,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
Clark Boylan844180e2017-03-15 15:24:58 -070016import base64
Markus Zoellerae36ce82017-03-20 16:27:26 +010017import socket
xxj8eb90982017-04-10 21:18:39 +080018import ssl
Markus Zoellerae36ce82017-03-20 16:27:26 +010019import struct
Clark Boylan844180e2017-03-15 15:24:58 -070020import textwrap
21
Markus Zoellerae36ce82017-03-20 16:27:26 +010022import six
23from six.moves.urllib import parse as urlparse
24
Joseph Lanouxb3e1f872015-01-30 11:13:07 +000025from oslo_log import log as logging
26from oslo_utils import excutils
Joseph Lanouxb3e1f872015-01-30 11:13:07 +000027
Ken'ichi Ohmichi0eb153c2015-07-13 02:18:25 +000028from tempest.common import waiters
Joseph Lanouxb3e1f872015-01-30 11:13:07 +000029from tempest import config
Matthew Treinishb19c55d2017-07-17 12:38:35 -040030from tempest.lib.common import fixed_network
Ken'ichi Ohmichi54030522016-03-02 11:01:34 -080031from tempest.lib.common import rest_client
Ken'ichi Ohmichi757833a2017-03-10 10:30:30 -080032from tempest.lib.common.utils import data_utils
Joseph Lanouxb3e1f872015-01-30 11:13:07 +000033
Markus Zoellerae36ce82017-03-20 16:27:26 +010034if six.PY2:
35 ord_func = ord
36else:
37 ord_func = int
38
Joseph Lanouxb3e1f872015-01-30 11:13:07 +000039CONF = config.CONF
40
41LOG = logging.getLogger(__name__)
42
43
Andrea Frittoli88eb6772017-08-07 21:06:27 +010044def is_scheduler_filter_enabled(filter_name):
45 """Check the list of enabled compute scheduler filters from config.
46
Artom Lifshitz595ae162018-05-23 10:19:18 -040047 This function checks whether the given compute scheduler filter is enabled
48 in the nova config file. If the scheduler_enabled_filters option is set to
49 'all' in tempest.conf then, this function returns True with assumption that
50 requested filter 'filter_name' is one of the enabled filters in nova
51 ("nova.scheduler.filters.all_filters").
Andrea Frittoli88eb6772017-08-07 21:06:27 +010052 """
53
Artom Lifshitz595ae162018-05-23 10:19:18 -040054 filters = CONF.compute_feature_enabled.scheduler_enabled_filters
Andrea Frittoli88eb6772017-08-07 21:06:27 +010055 if not filters:
56 return False
57 if 'all' in filters:
58 return True
59 if filter_name in filters:
60 return True
61 return False
62
63
Andrea Frittoli (andreaf)476e9192015-08-14 23:59:58 +010064def create_test_server(clients, validatable=False, validation_resources=None,
Joe Gordon8843f0f2015-03-17 15:07:34 -070065 tenant_network=None, wait_until=None,
Anusha Ramineni9aaef8b2016-01-19 10:56:40 +053066 volume_backed=False, name=None, flavor=None,
ghanshyam61db96e2016-12-16 12:49:25 +090067 image_id=None, **kwargs):
Joseph Lanouxb3e1f872015-01-30 11:13:07 +000068 """Common wrapper utility returning a test server.
69
70 This method is a common wrapper returning a test server that can be
71 pingable or sshable.
72
Takashi NATSUME6d5a2b42015-09-08 11:27:49 +090073 :param clients: Client manager which provides OpenStack Tempest clients.
Joseph Lanouxb3e1f872015-01-30 11:13:07 +000074 :param validatable: Whether the server will be pingable or sshable.
75 :param validation_resources: Resources created for the connection to the
Andrea Frittoli (andreaf)9df3a522016-07-06 14:09:48 +010076 server. Include a keypair, a security group and an IP.
Ken'ichi Ohmichid5bc31a2015-09-02 01:45:28 +000077 :param tenant_network: Tenant network to be used for creating a server.
Ken'ichi Ohmichifc25e692015-09-02 01:48:06 +000078 :param wait_until: Server status to wait for the server to reach after
Andrea Frittoli (andreaf)9df3a522016-07-06 14:09:48 +010079 its creation.
ghanshyam61db96e2016-12-16 12:49:25 +090080 :param volume_backed: Whether the server is volume backed or not.
Sergey Vilgelmeac094a2018-11-21 18:27:51 -060081 If this is true, a volume will be created and create server will be
82 requested with 'block_device_mapping_v2' populated with below values:
83
84 .. code-block:: python
85
86 bd_map_v2 = [{
87 'uuid': volume['volume']['id'],
88 'source_type': 'volume',
89 'destination_type': 'volume',
90 'boot_index': 0,
91 'delete_on_termination': True}]
92 kwargs['block_device_mapping_v2'] = bd_map_v2
93
94 If server needs to be booted from volume with other combination of bdm
95 inputs than mentioned above, then pass the bdm inputs explicitly as
96 kwargs and image_id as empty string ('').
Andrea Frittoli (andreaf)9df3a522016-07-06 14:09:48 +010097 :param name: Name of the server to be provisioned. If not defined a random
98 string ending with '-instance' will be generated.
99 :param flavor: Flavor of the server to be provisioned. If not defined,
100 CONF.compute.flavor_ref will be used instead.
101 :param image_id: ID of the image to be used to provision the server. If not
102 defined, CONF.compute.image_ref will be used instead.
lei zhangdd552b22015-11-25 20:41:48 +0800103 :returns: a tuple
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000104 """
105
106 # TODO(jlanoux) add support of wait_until PINGABLE/SSHABLE
107
Anusha Ramineni9aaef8b2016-01-19 10:56:40 +0530108 if name is None:
109 name = data_utils.rand_name(__name__ + "-instance")
110 if flavor is None:
111 flavor = CONF.compute.flavor_ref
112 if image_id is None:
113 image_id = CONF.compute.image_ref
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000114
115 kwargs = fixed_network.set_networks_kwarg(
116 tenant_network, kwargs) or {}
117
Ghanshyam4de44ae2015-12-25 10:34:00 +0900118 multiple_create_request = (max(kwargs.get('min_count', 0),
119 kwargs.get('max_count', 0)) > 1)
120
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000121 if CONF.validation.run_validation and validatable:
122 # As a first implementation, multiple pingable or sshable servers will
123 # not be supported
Ghanshyam4de44ae2015-12-25 10:34:00 +0900124 if multiple_create_request:
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000125 msg = ("Multiple pingable or sshable servers not supported at "
126 "this stage.")
127 raise ValueError(msg)
128
Andrea Frittoli9f416dd2017-08-10 15:38:00 +0100129 LOG.debug("Provisioning test server with validation resources %s",
130 validation_resources)
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000131 if 'security_groups' in kwargs:
132 kwargs['security_groups'].append(
133 {'name': validation_resources['security_group']['name']})
134 else:
135 try:
136 kwargs['security_groups'] = [
137 {'name': validation_resources['security_group']['name']}]
138 except KeyError:
139 LOG.debug("No security group provided.")
140
141 if 'key_name' not in kwargs:
142 try:
143 kwargs['key_name'] = validation_resources['keypair']['name']
144 except KeyError:
145 LOG.debug("No key provided.")
146
147 if CONF.validation.connect_method == 'floating':
Ken'ichi Ohmichifc25e692015-09-02 01:48:06 +0000148 if wait_until is None:
149 wait_until = 'ACTIVE'
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000150
Clark Boylan844180e2017-03-15 15:24:58 -0700151 if 'user_data' not in kwargs:
152 # If nothing overrides the default user data script then run
153 # a simple script on the host to print networking info. This is
154 # to aid in debugging ssh failures.
155 script = '''
156 #!/bin/sh
157 echo "Printing {user} user authorized keys"
158 cat ~{user}/.ssh/authorized_keys || true
159 '''.format(user=CONF.validation.image_ssh_user)
160 script_clean = textwrap.dedent(script).lstrip().encode('utf8')
161 script_b64 = base64.b64encode(script_clean)
162 kwargs['user_data'] = script_b64
163
Joe Gordon8843f0f2015-03-17 15:07:34 -0700164 if volume_backed:
zhuflc6ce5392016-08-17 14:34:37 +0800165 volume_name = data_utils.rand_name(__name__ + '-volume')
ghanshyam6c682ff2018-08-06 09:54:45 +0000166 volumes_client = clients.volumes_client_latest
ghanshyam3bd0d2b2017-03-23 01:57:28 +0000167 params = {'name': volume_name,
ghanshyam61db96e2016-12-16 12:49:25 +0900168 'imageRef': image_id,
169 'size': CONF.volume.volume_size}
Martin Kopec00e6d6c2019-06-05 14:30:06 +0000170 if CONF.compute.compute_volume_common_az:
171 params.setdefault('availability_zone',
172 CONF.compute.compute_volume_common_az)
ghanshyam61db96e2016-12-16 12:49:25 +0900173 volume = volumes_client.create_volume(**params)
mccasland, trevor (tm2086)0fedb412019-01-21 13:37:58 -0600174 try:
175 waiters.wait_for_volume_resource_status(volumes_client,
176 volume['volume']['id'],
177 'available')
178 except Exception:
179 with excutils.save_and_reraise_exception():
180 try:
181 volumes_client.delete_volume(volume['volume']['id'])
182 volumes_client.wait_for_resource_deletion(
183 volume['volume']['id'])
184 except Exception as exc:
185 LOG.exception("Deleting volume %s failed, exception %s",
186 volume['volume']['id'], exc)
Joe Gordon8843f0f2015-03-17 15:07:34 -0700187 bd_map_v2 = [{
188 'uuid': volume['volume']['id'],
189 'source_type': 'volume',
190 'destination_type': 'volume',
191 'boot_index': 0,
ghanshyam61db96e2016-12-16 12:49:25 +0900192 'delete_on_termination': True}]
Joe Gordon8843f0f2015-03-17 15:07:34 -0700193 kwargs['block_device_mapping_v2'] = bd_map_v2
194
195 # Since this is boot from volume an image does not need
196 # to be specified.
197 image_id = ''
198
Martin Kopec00e6d6c2019-06-05 14:30:06 +0000199 if CONF.compute.compute_volume_common_az:
200 kwargs.setdefault('availability_zone',
201 CONF.compute.compute_volume_common_az)
Ken'ichi Ohmichif2d436e2015-09-03 01:13:16 +0000202 body = clients.servers_client.create_server(name=name, imageRef=image_id,
203 flavorRef=flavor,
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000204 **kwargs)
205
206 # handle the case of multiple servers
Ghanshyam4de44ae2015-12-25 10:34:00 +0900207 if multiple_create_request:
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000208 # Get servers created which name match with name param.
209 body_servers = clients.servers_client.list_servers()
210 servers = \
211 [s for s in body_servers['servers'] if s['name'].startswith(name)]
ghanshyam0f825252015-08-25 16:02:50 +0900212 else:
Ken'ichi Ohmichi54030522016-03-02 11:01:34 -0800213 body = rest_client.ResponseBody(body.response, body['server'])
ghanshyam0f825252015-08-25 16:02:50 +0900214 servers = [body]
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000215
Artom Lifshitz70d7a112017-05-10 17:25:54 +0000216 def _setup_validation_fip():
217 if CONF.service_available.neutron:
218 ifaces = clients.interfaces_client.list_interfaces(server['id'])
219 validation_port = None
220 for iface in ifaces['interfaceAttachments']:
221 if iface['net_id'] == tenant_network['id']:
222 validation_port = iface['port_id']
223 break
224 if not validation_port:
225 # NOTE(artom) This will get caught by the catch-all clause in
226 # the wait_until loop below
227 raise ValueError('Unable to setup floating IP for validation: '
228 'port not found on tenant network')
229 clients.floating_ips_client.update_floatingip(
230 validation_resources['floating_ip']['id'],
231 port_id=validation_port)
232 else:
233 fip_client = clients.compute_floating_ips_client
234 fip_client.associate_floating_ip_to_server(
235 floating_ip=validation_resources['floating_ip']['ip'],
236 server_id=servers[0]['id'])
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000237
Ken'ichi Ohmichifc25e692015-09-02 01:48:06 +0000238 if wait_until:
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000239 for server in servers:
240 try:
Ken'ichi Ohmichi0eb153c2015-07-13 02:18:25 +0000241 waiters.wait_for_server_status(
Ken'ichi Ohmichifc25e692015-09-02 01:48:06 +0000242 clients.servers_client, server['id'], wait_until)
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000243
244 # Multiple validatable servers are not supported for now. Their
Masayuki Igawa544b3c82017-12-08 15:39:36 +0900245 # creation will fail with the condition above.
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000246 if CONF.validation.run_validation and validatable:
247 if CONF.validation.connect_method == 'floating':
Artom Lifshitz70d7a112017-05-10 17:25:54 +0000248 _setup_validation_fip()
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000249
250 except Exception:
251 with excutils.save_and_reraise_exception():
Jordan Pittier87ba2872016-03-08 11:43:11 +0100252 for server in servers:
253 try:
254 clients.servers_client.delete_server(
255 server['id'])
256 except Exception:
Jordan Pittier525ec712016-12-07 17:51:26 +0100257 LOG.exception('Deleting server %s failed',
258 server['id'])
Artom Lifshitz9b3f42b2017-06-19 05:46:32 +0000259 for server in servers:
260 # NOTE(artom) If the servers were booted with volumes
261 # and with delete_on_termination=False we need to wait
262 # for the servers to go away before proceeding with
263 # cleanup, otherwise we'll attempt to delete the
264 # volumes while they're still attached to servers that
265 # are in the process of being deleted.
266 try:
267 waiters.wait_for_server_termination(
268 clients.servers_client, server['id'])
269 except Exception:
270 LOG.exception('Server %s failed to delete in time',
271 server['id'])
Joseph Lanouxb3e1f872015-01-30 11:13:07 +0000272
273 return body, servers
ghanshyam017b5fe2016-04-15 18:49:26 +0900274
275
ghanshyam4c1391c2016-12-01 13:13:06 +0900276def shelve_server(servers_client, server_id, force_shelve_offload=False):
ghanshyam017b5fe2016-04-15 18:49:26 +0900277 """Common wrapper utility to shelve server.
278
279 This method is a common wrapper to make server in 'SHELVED'
280 or 'SHELVED_OFFLOADED' state.
281
ghanshyam4c1391c2016-12-01 13:13:06 +0900282 :param servers_clients: Compute servers client instance.
ghanshyam017b5fe2016-04-15 18:49:26 +0900283 :param server_id: Server to make in shelve state
284 :param force_shelve_offload: Forcefully offload shelve server if it
285 is configured not to offload server
286 automatically after offload time.
287 """
ghanshyam4c1391c2016-12-01 13:13:06 +0900288 servers_client.shelve_server(server_id)
ghanshyam017b5fe2016-04-15 18:49:26 +0900289
290 offload_time = CONF.compute.shelved_offload_time
291 if offload_time >= 0:
ghanshyam4c1391c2016-12-01 13:13:06 +0900292 waiters.wait_for_server_status(servers_client, server_id,
ghanshyam017b5fe2016-04-15 18:49:26 +0900293 'SHELVED_OFFLOADED',
294 extra_timeout=offload_time)
295 else:
ghanshyam4c1391c2016-12-01 13:13:06 +0900296 waiters.wait_for_server_status(servers_client, server_id, 'SHELVED')
ghanshyam017b5fe2016-04-15 18:49:26 +0900297 if force_shelve_offload:
ghanshyam4c1391c2016-12-01 13:13:06 +0900298 servers_client.shelve_offload_server(server_id)
299 waiters.wait_for_server_status(servers_client, server_id,
ghanshyam017b5fe2016-04-15 18:49:26 +0900300 'SHELVED_OFFLOADED')
Markus Zoellerae36ce82017-03-20 16:27:26 +0100301
302
303def create_websocket(url):
304 url = urlparse.urlparse(url)
Mohammed Naseraa5dd9a2017-12-29 18:52:01 -0500305
306 # NOTE(mnaser): It is possible that there is no port specified, so fall
307 # back to the default port based on the scheme.
308 port = url.port or (443 if url.scheme == 'https' else 80)
309
310 for res in socket.getaddrinfo(url.hostname, port,
Jens Harbott6bc422d2017-09-27 10:29:34 +0000311 socket.AF_UNSPEC, socket.SOCK_STREAM):
312 af, socktype, proto, _, sa = res
313 client_socket = socket.socket(af, socktype, proto)
314 if url.scheme == 'https':
315 client_socket = ssl.wrap_socket(client_socket)
316 client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
317 try:
318 client_socket.connect(sa)
319 except socket.error:
320 client_socket.close()
321 continue
322 break
xxj8eb90982017-04-10 21:18:39 +0800323 else:
Jens Harbott6bc422d2017-09-27 10:29:34 +0000324 raise socket.error('WebSocket creation failed')
Markus Zoellerae36ce82017-03-20 16:27:26 +0100325 # Turn the Socket into a WebSocket to do the communication
326 return _WebSocket(client_socket, url)
327
328
329class _WebSocket(object):
330 def __init__(self, client_socket, url):
331 """Contructor for the WebSocket wrapper to the socket."""
332 self._socket = client_socket
jianghua wangd22514a2017-05-08 08:05:04 +0100333 # cached stream for early frames.
334 self.cached_stream = b''
Markus Zoellerae36ce82017-03-20 16:27:26 +0100335 # Upgrade the HTTP connection to a WebSocket
336 self._upgrade(url)
337
jianghua wangd22514a2017-05-08 08:05:04 +0100338 def _recv(self, recv_size):
339 """Wrapper to receive data from the cached stream or socket."""
340 if recv_size <= 0:
341 return None
342
343 data_from_cached = b''
344 data_from_socket = b''
345 if len(self.cached_stream) > 0:
346 read_from_cached = min(len(self.cached_stream), recv_size)
347 data_from_cached += self.cached_stream[:read_from_cached]
348 self.cached_stream = self.cached_stream[read_from_cached:]
349 recv_size -= read_from_cached
350 if recv_size > 0:
351 data_from_socket = self._socket.recv(recv_size)
352 return data_from_cached + data_from_socket
353
Markus Zoellerae36ce82017-03-20 16:27:26 +0100354 def receive_frame(self):
355 """Wrapper for receiving data to parse the WebSocket frame format"""
356 # We need to loop until we either get some bytes back in the frame
357 # or no data was received (meaning the socket was closed). This is
358 # done to handle the case where we get back some empty frames
359 while True:
jianghua wangd22514a2017-05-08 08:05:04 +0100360 header = self._recv(2)
Markus Zoellerae36ce82017-03-20 16:27:26 +0100361 # If we didn't receive any data, just return None
Masayuki Igawa0c0f0142017-04-10 17:22:02 +0900362 if not header:
Markus Zoellerae36ce82017-03-20 16:27:26 +0100363 return None
364 # We will make the assumption that we are only dealing with
365 # frames less than 125 bytes here (for the negotiation) and
366 # that only the 2nd byte contains the length, and since the
367 # server doesn't do masking, we can just read the data length
368 if ord_func(header[1]) & 127 > 0:
jianghua wangd22514a2017-05-08 08:05:04 +0100369 return self._recv(ord_func(header[1]) & 127)
Markus Zoellerae36ce82017-03-20 16:27:26 +0100370
371 def send_frame(self, data):
372 """Wrapper for sending data to add in the WebSocket frame format."""
373 frame_bytes = list()
374 # For the first byte, want to say we are sending binary data (130)
375 frame_bytes.append(130)
376 # Only sending negotiation data so don't need to worry about > 125
377 # We do need to add the bit that says we are masking the data
378 frame_bytes.append(len(data) | 128)
379 # We don't really care about providing a random mask for security
380 # So we will just hard-code a value since a test program
381 mask = [7, 2, 1, 9]
382 for i in range(len(mask)):
383 frame_bytes.append(mask[i])
384 # Mask each of the actual data bytes that we are going to send
385 for i in range(len(data)):
386 frame_bytes.append(ord_func(data[i]) ^ mask[i % 4])
387 # Convert our integer list to a binary array of bytes
388 frame_bytes = struct.pack('!%iB' % len(frame_bytes), * frame_bytes)
389 self._socket.sendall(frame_bytes)
390
391 def close(self):
392 """Helper method to close the connection."""
393 # Close down the real socket connection and exit the test program
394 if self._socket is not None:
395 self._socket.shutdown(1)
396 self._socket.close()
397 self._socket = None
398
399 def _upgrade(self, url):
400 """Upgrade the HTTP connection to a WebSocket and verify."""
melanie witt27ba9332019-04-26 02:33:20 +0000401 # It is possible to pass the path as a query parameter in the request,
402 # so use it if present
403 qparams = urlparse.parse_qs(url.query)
404 path = qparams['path'][0] if 'path' in qparams else '/websockify'
405 reqdata = 'GET %s HTTP/1.1\r\n' % path
Mohammed Naseraa5dd9a2017-12-29 18:52:01 -0500406 reqdata += 'Host: %s' % url.hostname
407 # Add port only if we have one specified
408 if url.port:
409 reqdata += ':%s' % url.port
410 # Line-ending for Host header
411 reqdata += '\r\n'
Markus Zoellerae36ce82017-03-20 16:27:26 +0100412 # Tell the HTTP Server to Upgrade the connection to a WebSocket
413 reqdata += 'Upgrade: websocket\r\nConnection: Upgrade\r\n'
melanie witt27ba9332019-04-26 02:33:20 +0000414 # The token=xxx is sent as a Cookie not in the URI for noVNC < v1.1.0
Markus Zoellerae36ce82017-03-20 16:27:26 +0100415 reqdata += 'Cookie: %s\r\n' % url.query
416 # Use a hard-coded WebSocket key since a test program
417 reqdata += 'Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n'
418 reqdata += 'Sec-WebSocket-Version: 13\r\n'
419 # We are choosing to use binary even though browser may do Base64
420 reqdata += 'Sec-WebSocket-Protocol: binary\r\n\r\n'
421 # Send the HTTP GET request and get the response back
422 self._socket.sendall(reqdata.encode('utf8'))
423 self.response = data = self._socket.recv(4096)
424 # Loop through & concatenate all of the data in the response body
jianghua wangd22514a2017-05-08 08:05:04 +0100425 end_loc = self.response.find(b'\r\n\r\n')
426 while data and end_loc < 0:
Markus Zoellerae36ce82017-03-20 16:27:26 +0100427 data = self._socket.recv(4096)
428 self.response += data
jianghua wangd22514a2017-05-08 08:05:04 +0100429 end_loc = self.response.find(b'\r\n\r\n')
430
431 if len(self.response) > end_loc + 4:
432 # In case some frames (e.g. the first RFP negotiation) have
433 # arrived, cache it for next reading.
434 self.cached_stream = self.response[end_loc + 4:]
435 # ensure response ends with '\r\n\r\n'.
436 self.response = self.response[:end_loc + 4]