blob: 34e8148faac7ab45721f9c366afd2ae67989811a [file] [log] [blame]
Jude Cross986e3f52017-07-24 14:57:20 -07001# Copyright 2017 GoDaddy
2# Copyright 2017 Catalyst IT Ltd
3# Copyright 2018 Rackspace US Inc. All rights reserved.
Michael Johnson89bdbcd2020-03-19 15:59:19 -07004# Copyright 2020 Red Hat, Inc. All rights reserved.
Jude Cross986e3f52017-07-24 14:57:20 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
Michael Johnson89bdbcd2020-03-19 15:59:19 -070017import errno
18import ipaddress
Jude Cross986e3f52017-07-24 14:57:20 -070019import requests
Michael Johnson89bdbcd2020-03-19 15:59:19 -070020import socket
Jude Cross986e3f52017-07-24 14:57:20 -070021import time
Michael Johnson89bdbcd2020-03-19 15:59:19 -070022from urllib.parse import urlparse
Jude Cross986e3f52017-07-24 14:57:20 -070023
24from oslo_log import log as logging
25from tempest import config
26from tempest.lib import exceptions
Michael Johnson89bdbcd2020-03-19 15:59:19 -070027from tempest import test
28
29from octavia_tempest_plugin.common import constants as const
30from octavia_tempest_plugin.common import requests_adapters
Jude Cross986e3f52017-07-24 14:57:20 -070031
32CONF = config.CONF
33LOG = logging.getLogger(__name__)
34
35
Michael Johnson89bdbcd2020-03-19 15:59:19 -070036class ValidatorsMixin(test.BaseTestCase):
Jude Cross986e3f52017-07-24 14:57:20 -070037
Michael Johnson89bdbcd2020-03-19 15:59:19 -070038 @staticmethod
39 def validate_URL_response(
40 URL, expected_status_code=200, requests_session=None,
41 expected_body=None, HTTPS_verify=True, client_cert_path=None,
42 CA_certs_path=None, source_port=None,
43 request_interval=CONF.load_balancer.build_interval,
44 request_timeout=CONF.load_balancer.build_timeout):
45 """Check a URL response (HTTP or HTTPS).
46
47 :param URL: The URL to query.
48 :param expected_status_code: The expected HTTP status code.
49 :param requests_session: A requests session to use for the request.
50 If None, a new session will be created.
51 :param expected_body: The expected response text, None will not
52 compare.
53 :param HTTPS_verify: Should we verify the HTTPS server.
54 :param client_cert_path: Filesystem path to a file with the client
55 private key and certificate.
56 :param CA_certs_path: Filesystem path to a file containing CA
57 certificates to use for HTTPS validation.
58 :param source_port: If set, the request will come from this source port
59 number. If None, a random port will be used.
60 :param request_interval: Time, in seconds, to timeout a request.
61 :param request_timeout: The maximum time, in seconds, to attempt
62 requests. Failed validation of expected
63 results does not result in a retry.
64 :raises InvalidHttpSuccessCode: The expected_status_code did not match.
65 :raises InvalidHTTPResponseBody: The response body did not match the
66 expected content.
67 :raises TimeoutException: The request timed out.
68 :returns: The response data.
69 """
70 session = requests_session
71 if requests_session is None:
72 session = requests.Session()
73 if source_port:
74 session.mount('http://',
75 requests_adapters.SourcePortAdapter(source_port))
76 session.mount('https://',
77 requests_adapters.SourcePortAdapter(source_port))
78
Jude Cross986e3f52017-07-24 14:57:20 -070079 session_kwargs = {}
80 if not HTTPS_verify:
81 session_kwargs['verify'] = False
82 if CA_certs_path:
83 session_kwargs['verify'] = CA_certs_path
84 if client_cert_path:
85 session_kwargs['cert'] = client_cert_path
86 session_kwargs['timeout'] = request_interval
87 start = time.time()
88 while time.time() - start < request_timeout:
89 try:
90 response = session.get(URL, **session_kwargs)
Michael Johnson89bdbcd2020-03-19 15:59:19 -070091 response_status_code = response.status_code
92 response_text = response.text
93 response.close()
94 if response_status_code != expected_status_code:
Jude Cross986e3f52017-07-24 14:57:20 -070095 raise exceptions.InvalidHttpSuccessCode(
96 '{0} is not the expected code {1}'.format(
Michael Johnson89bdbcd2020-03-19 15:59:19 -070097 response_status_code, expected_status_code))
98 if expected_body and response_text != expected_body:
Jude Cross986e3f52017-07-24 14:57:20 -070099 details = '{} does not match expected {}'.format(
Michael Johnson89bdbcd2020-03-19 15:59:19 -0700100 response_text, expected_body)
Jude Cross986e3f52017-07-24 14:57:20 -0700101 raise exceptions.InvalidHTTPResponseBody(
102 resp_body=details)
Michael Johnson89bdbcd2020-03-19 15:59:19 -0700103 if requests_session is None:
104 session.close()
105 return response_text
Jude Cross986e3f52017-07-24 14:57:20 -0700106 except requests.exceptions.Timeout:
107 # Don't sleep as we have already waited the interval.
Jonathan Rosser7654d2e2019-06-24 14:55:17 +0100108 LOG.info('Request for {} timed out. Retrying.'.format(URL))
Jude Cross986e3f52017-07-24 14:57:20 -0700109 except (exceptions.InvalidHttpSuccessCode,
110 exceptions.InvalidHTTPResponseBody,
111 requests.exceptions.SSLError):
Michael Johnson89bdbcd2020-03-19 15:59:19 -0700112 if requests_session is None:
113 session.close()
Jude Cross986e3f52017-07-24 14:57:20 -0700114 raise
115 except Exception as e:
116 LOG.info('Validate URL got exception: {0}. '
117 'Retrying.'.format(e))
118 time.sleep(request_interval)
Michael Johnson89bdbcd2020-03-19 15:59:19 -0700119 if requests_session is None:
120 session.close()
Jude Cross986e3f52017-07-24 14:57:20 -0700121 raise exceptions.TimeoutException()
Michael Johnson89bdbcd2020-03-19 15:59:19 -0700122
123 @classmethod
124 def make_udp_request(cls, vip_address, port=80, timeout=None,
125 source_port=None):
126 if ipaddress.ip_address(vip_address).version == 6:
127 family = socket.AF_INET6
128 else:
129 family = socket.AF_INET
130
131 sock = socket.socket(family, socket.SOCK_DGRAM)
132
133 # Force the use of an incremental port number for source to avoid
134 # re-use of a previous source port that will affect the round-robin
135 # dispatch
136 while True:
137 port_number = cls.src_port_number
138 cls.src_port_number += 1
139 if cls.src_port_number >= cls.SRC_PORT_NUMBER_MAX:
140 cls.src_port_number = cls.SRC_PORT_NUMBER_MIN
141
142 # catch and skip already used ports on the host
143 try:
144 if source_port:
145 sock.bind(('', source_port))
146 else:
147 sock.bind(('', port_number))
148 except OSError as e:
149 # if error is 'Address already in use', try next port number
150 # If source_port is defined and already in use, a test
151 # developer has made a mistake by using a duplicate source
152 # port.
153 if e.errno != errno.EADDRINUSE or source_port:
154 raise e
155 else:
156 # successfully bind the socket
157 break
158
159 server_address = (vip_address, port)
160 data = b"data\n"
161
162 if timeout is not None:
163 sock.settimeout(timeout)
164
165 try:
166 sock.sendto(data, server_address)
167 data, addr = sock.recvfrom(4096)
168 except socket.timeout:
169 # Normalize the timeout exception so that UDP and other protocol
170 # tests all return a common timeout exception.
171 raise exceptions.TimeoutException()
172 finally:
173 sock.close()
174
175 return data.decode('utf-8')
176
177 def make_request(
178 self, vip_address, protocol=const.HTTP, HTTPS_verify=True,
179 protocol_port=80, requests_session=None, client_cert_path=None,
180 CA_certs_path=None, request_timeout=2, source_port=None):
181 """Make a request to a VIP.
182
183 :param vip_address: The VIP address to test.
184 :param protocol: The protocol to use for the test.
185 :param HTTPS_verify: How to verify the TLS certificate. True: verify
186 using the system CA certificates. False: Do not
187 verify the VIP certificate. <path>: Filesytem path
188 to a CA certificate bundle file or directory. For
189 directories, the directory must be processed using
190 the c_rehash utility from openssl.
191 :param protocol_port: The port number to use for the test.
192 :param requests_session: A requests session to use for the request.
193 If None, a new session will be created.
194 :param request_timeout: The maximum time, in seconds, to attempt
195 requests.
196 :param client_cert_path: Filesystem path to a file with the client
197 private key and certificate.
198 :param CA_certs_path: Filesystem path to a file containing CA
199 certificates to use for HTTPS validation.
200 :param source_port: If set, the request will come from this source port
201 number. If None, a random port will be used.
202 :raises InvalidHttpSuccessCode: The expected_status_code did not match.
203 :raises InvalidHTTPResponseBody: The response body did not match the
204 expected content.
205 :raises TimeoutException: The request timed out.
206 :raises Exception: If a protocol is requested that is not implemented.
207 :returns: The response data.
208 """
209 # Note: We are using HTTP as the TCP protocol check to simplify
210 # the test setup. HTTP is a TCP based protocol.
211 if protocol == const.HTTP or protocol == const.TCP:
212 url = "http://{0}{1}{2}".format(
213 vip_address, ':' if protocol_port else '',
214 protocol_port or '')
215 data = self.validate_URL_response(
216 url, HTTPS_verify=False, requests_session=requests_session,
217 request_timeout=request_timeout,
218 source_port=source_port)
219 elif (protocol == const.HTTPS or
220 protocol == const.TERMINATED_HTTPS):
221 url = "https://{0}{1}{2}".format(
222 vip_address, ':' if protocol_port else '',
223 protocol_port or '')
224 data = self.validate_URL_response(
225 url, HTTPS_verify=HTTPS_verify,
226 requests_session=requests_session,
227 client_cert_path=client_cert_path,
228 CA_certs_path=CA_certs_path, source_port=source_port,
229 request_timeout=request_timeout)
230 elif protocol == const.UDP:
231 data = self.make_udp_request(
232 vip_address, port=protocol_port, timeout=request_timeout,
233 source_port=source_port)
234 else:
235 message = ("Unknown protocol %s. Unable to check if the "
236 "load balancer is balanced.", protocol)
237 LOG.error(message)
238 raise Exception(message)
239 return data
240
241 def check_members_balanced(
242 self, vip_address, traffic_member_count=2, protocol=const.HTTP,
243 HTTPS_verify=True, protocol_port=80, persistent=True, repeat=20,
244 client_cert_path=None, CA_certs_path=None, request_interval=2,
245 request_timeout=10, source_port=None, delay=None):
246 """Checks that members are evenly balanced behind a VIP.
247
248 :param vip_address: The VIP address to test.
249 :param traffic_member_count: The expected number of members.
250 :param protocol: The protocol to use for the test.
251 :param HTTPS_verify: How to verify the TLS certificate. True: verify
252 using the system CA certificates. False: Do not
253 verify the VIP certificate. <path>: Filesytem path
254 to a CA certificate bundle file or directory. For
255 directories, the directory must be processed using
256 the c_rehash utility from openssl.
257 :param protocol_port: The port number to use for the test.
258 :param persistent: True when the test should persist cookies and use
259 the protocol keepalive mechanism with the target.
260 This may include maintaining a connection to the
261 member server across requests.
262 :param repeat: The number of requests to make against the VIP.
263 :param request_timeout: The maximum time, in seconds, to attempt
264 requests.
265 :param client_cert_path: Filesystem path to a file with the client
266 private key and certificate.
267 :param CA_certs_path: Filesystem path to a file containing CA
268 certificates to use for HTTPS validation.
269 :param source_port: If set, the request will come from this source port
270 number. If None, a random port will be used.
271 :param delay: The time to pause between requests in seconds, can be
272 fractional.
273 """
274 if (ipaddress.ip_address(vip_address).version == 6 and
275 protocol != const.UDP):
276 vip_address = '[{}]'.format(vip_address)
277
278 requests_session = None
279 if persistent:
280 requests_session = requests.Session()
281
282 self._wait_for_lb_functional(
283 vip_address, traffic_member_count, protocol_port, protocol,
284 HTTPS_verify, requests_session=requests_session,
285 source_port=source_port)
286
Brian Haley52531e22021-01-21 16:52:09 -0500287 if source_port:
288 LOG.debug('Using source port %s for request(s)', source_port)
289
Michael Johnson89bdbcd2020-03-19 15:59:19 -0700290 response_counts = {}
291 # Send a number requests to lb vip
292 for i in range(repeat):
293 try:
294 data = self.make_request(
295 vip_address, protocol=protocol, HTTPS_verify=HTTPS_verify,
296 protocol_port=protocol_port,
297 requests_session=requests_session,
298 client_cert_path=client_cert_path,
299 CA_certs_path=CA_certs_path, source_port=source_port,
300 request_timeout=request_timeout)
301
302 if data in response_counts:
303 response_counts[data] += 1
304 else:
305 response_counts[data] = 1
306 if delay is not None:
307 time.sleep(delay)
308 except Exception:
309 LOG.exception('Failed to send request to loadbalancer vip')
310 if persistent:
311 requests_session.close()
312 raise Exception('Failed to connect to lb')
313 if persistent:
314 requests_session.close()
315 LOG.debug('Loadbalancer response totals: %s', response_counts)
316
317 # Ensure the correct number of members responded
318 self.assertEqual(traffic_member_count, len(response_counts))
319
320 # Ensure both members got the same number of responses
321 self.assertEqual(1, len(set(response_counts.values())))
322
323 def assertConsistentResponse(self, response, url, method='GET', repeat=10,
324 redirect=False, timeout=2,
325 expect_connection_error=False, **kwargs):
326 """Assert that a request to URL gets the expected response.
327
328 :param response: Expected response in format (status_code, content).
329 :param url: The URL to request.
330 :param method: The HTTP method to use (GET, POST, PUT, etc)
331 :param repeat: How many times to test the response.
332 :param data: Optional data to send in the request.
333 :param headers: Optional headers to send in the request.
334 :param cookies: Optional cookies to send in the request.
335 :param redirect: Is the request a redirect? If true, assume the passed
336 content should be the next URL in the chain.
337 :param timeout: Optional seconds to wait for the server to send data.
338 :param expect_connection_error: Should we expect a connection error
339 :param expect_timeout: Should we expect a connection timeout
340
341 :return: boolean success status
342
343 :raises: testtools.matchers.MismatchError
344 """
345 session = requests.Session()
346 response_code, response_content = response
347
348 for i in range(repeat):
349 if url.startswith(const.HTTP.lower()):
350 if expect_connection_error:
351 self.assertRaises(
352 requests.exceptions.ConnectionError, session.request,
353 method, url, allow_redirects=not redirect,
354 timeout=timeout, **kwargs)
355 continue
356
357 req = session.request(method, url,
358 allow_redirects=not redirect,
359 timeout=timeout, **kwargs)
360 if response_code:
361 self.assertEqual(response_code, req.status_code)
362 if redirect:
363 self.assertTrue(req.is_redirect)
364 self.assertEqual(response_content,
365 session.get_redirect_target(req))
366 elif response_content:
367 self.assertEqual(str(response_content), req.text)
368 elif url.startswith(const.UDP.lower()):
369 parsed_url = urlparse(url)
370 if expect_connection_error:
371 self.assertRaises(exceptions.TimeoutException,
372 self.make_udp_request,
373 parsed_url.hostname,
374 port=parsed_url.port, timeout=timeout)
375 continue
376
377 data = self.make_udp_request(parsed_url.hostname,
378 port=parsed_url.port,
379 timeout=timeout)
380 self.assertEqual(response_content, data)
381
382 def _wait_for_lb_functional(
383 self, vip_address, traffic_member_count, protocol_port, protocol,
384 HTTPS_verify, client_cert_path=None, CA_certs_path=None,
385 request_interval=2, request_timeout=10, requests_session=None,
386 source_port=None):
387 start = time.time()
388 response_counts = {}
389
390 # Send requests to the load balancer until at least
391 # "traffic_member_count" members have replied (ensure network
392 # connectivity is functional between the load balancer and the members)
393 while time.time() - start < CONF.load_balancer.build_timeout:
394 try:
395 data = self.make_request(
396 vip_address, protocol=protocol, HTTPS_verify=HTTPS_verify,
397 protocol_port=protocol_port,
398 client_cert_path=client_cert_path,
399 CA_certs_path=CA_certs_path, source_port=source_port,
400 request_timeout=request_timeout,
401 requests_session=requests_session)
402
403 if data in response_counts:
404 response_counts[data] += 1
405 else:
406 response_counts[data] = 1
407
408 if traffic_member_count == len(response_counts):
409 LOG.debug('Loadbalancer response totals: %s',
410 response_counts)
411 time.sleep(1)
412 return
413 except Exception:
414 LOG.warning('Server is not passing initial traffic. Waiting.')
Gregory Thiemonged698a182023-04-06 09:50:38 +0200415 time.sleep(request_interval)
Michael Johnson89bdbcd2020-03-19 15:59:19 -0700416
417 LOG.debug('Loadbalancer wait for load balancer response totals: %s',
418 response_counts)
419 message = ('Server %s on port %s did not begin passing traffic within '
420 'the timeout period. Failing test.' % (vip_address,
421 protocol_port))
422 LOG.error(message)
423 raise Exception(message)
Arkady Shtemplera186f062020-09-30 18:20:03 +0300424
425 def make_udp_requests_with_retries(
426 self, vip_address, number_of_retries, dst_port,
427 src_port=None, socket_timeout=20):
428 """Send UDP packets using retries mechanism
429
430 The delivery of data to the destination cannot be guaranteed in UDP.
431 In case when UDP package is getting lost and we might want to check
432 what could be the reason for that (Network issues or Server Side),
433 well need to send more packets to get into the conclusion.
434
435 :param vip_address: LB VIP address
436 :param number_of_retries: integer number of retries
437 :param dst_port: UDP server destination port
438 :param src_port: UDP source port to bind for UDP connection
439 :param socket_timeout: UDP socket timeout
440 :return: None if all UPD retries failed, else first successful
441 response data from UDP server.
442 """
443 retry_number = 0
444 received_data = None
445 while retry_number < number_of_retries:
446 LOG.info('make_udp_requests_with_retries attempt '
447 'number:{}'.format(retry_number))
448 retry_number += 1
449 try:
450 received_data = self.make_udp_request(
451 vip_address, dst_port, timeout=socket_timeout,
452 source_port=src_port)
453 break
454 except Exception as e:
455 LOG.warning('make_udp_request has failed with: '
456 '{}'.format(e))
457 return received_data