blob: 57ba2e2bd1fe2bed0331f727ba3343955807dc1f [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.
Michael Johnson77b8bae2024-11-08 01:39:29 +0000108 LOG.info('Request for %s timed out. Retrying.', 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:
Fernando Royo13cb2ec2025-05-21 17:54:20 +0200116 if "[Errno 98] Address already in use" in str(e):
117 raise e
Michael Johnson77b8bae2024-11-08 01:39:29 +0000118 LOG.info('Validate URL got exception: %s. '
119 'Retrying.', e)
Jude Cross986e3f52017-07-24 14:57:20 -0700120 time.sleep(request_interval)
Michael Johnson89bdbcd2020-03-19 15:59:19 -0700121 if requests_session is None:
122 session.close()
Jude Cross986e3f52017-07-24 14:57:20 -0700123 raise exceptions.TimeoutException()
Michael Johnson89bdbcd2020-03-19 15:59:19 -0700124
125 @classmethod
126 def make_udp_request(cls, vip_address, port=80, timeout=None,
127 source_port=None):
128 if ipaddress.ip_address(vip_address).version == 6:
129 family = socket.AF_INET6
130 else:
131 family = socket.AF_INET
132
133 sock = socket.socket(family, socket.SOCK_DGRAM)
134
135 # Force the use of an incremental port number for source to avoid
136 # re-use of a previous source port that will affect the round-robin
137 # dispatch
138 while True:
139 port_number = cls.src_port_number
140 cls.src_port_number += 1
141 if cls.src_port_number >= cls.SRC_PORT_NUMBER_MAX:
142 cls.src_port_number = cls.SRC_PORT_NUMBER_MIN
143
144 # catch and skip already used ports on the host
145 try:
146 if source_port:
147 sock.bind(('', source_port))
148 else:
149 sock.bind(('', port_number))
150 except OSError as e:
151 # if error is 'Address already in use', try next port number
152 # If source_port is defined and already in use, a test
153 # developer has made a mistake by using a duplicate source
154 # port.
155 if e.errno != errno.EADDRINUSE or source_port:
156 raise e
157 else:
158 # successfully bind the socket
159 break
160
161 server_address = (vip_address, port)
162 data = b"data\n"
163
164 if timeout is not None:
165 sock.settimeout(timeout)
166
167 try:
168 sock.sendto(data, server_address)
169 data, addr = sock.recvfrom(4096)
170 except socket.timeout:
171 # Normalize the timeout exception so that UDP and other protocol
172 # tests all return a common timeout exception.
173 raise exceptions.TimeoutException()
174 finally:
175 sock.close()
176
177 return data.decode('utf-8')
178
179 def make_request(
180 self, vip_address, protocol=const.HTTP, HTTPS_verify=True,
181 protocol_port=80, requests_session=None, client_cert_path=None,
182 CA_certs_path=None, request_timeout=2, source_port=None):
183 """Make a request to a VIP.
184
185 :param vip_address: The VIP address to test.
186 :param protocol: The protocol to use for the test.
187 :param HTTPS_verify: How to verify the TLS certificate. True: verify
188 using the system CA certificates. False: Do not
189 verify the VIP certificate. <path>: Filesytem path
190 to a CA certificate bundle file or directory. For
191 directories, the directory must be processed using
192 the c_rehash utility from openssl.
193 :param protocol_port: The port number to use for the test.
194 :param requests_session: A requests session to use for the request.
195 If None, a new session will be created.
196 :param request_timeout: The maximum time, in seconds, to attempt
197 requests.
198 :param client_cert_path: Filesystem path to a file with the client
199 private key and certificate.
200 :param CA_certs_path: Filesystem path to a file containing CA
201 certificates to use for HTTPS validation.
202 :param source_port: If set, the request will come from this source port
203 number. If None, a random port will be used.
204 :raises InvalidHttpSuccessCode: The expected_status_code did not match.
205 :raises InvalidHTTPResponseBody: The response body did not match the
206 expected content.
207 :raises TimeoutException: The request timed out.
208 :raises Exception: If a protocol is requested that is not implemented.
209 :returns: The response data.
210 """
211 # Note: We are using HTTP as the TCP protocol check to simplify
212 # the test setup. HTTP is a TCP based protocol.
213 if protocol == const.HTTP or protocol == const.TCP:
214 url = "http://{0}{1}{2}".format(
215 vip_address, ':' if protocol_port else '',
216 protocol_port or '')
217 data = self.validate_URL_response(
218 url, HTTPS_verify=False, requests_session=requests_session,
219 request_timeout=request_timeout,
220 source_port=source_port)
221 elif (protocol == const.HTTPS or
222 protocol == const.TERMINATED_HTTPS):
223 url = "https://{0}{1}{2}".format(
224 vip_address, ':' if protocol_port else '',
225 protocol_port or '')
226 data = self.validate_URL_response(
227 url, HTTPS_verify=HTTPS_verify,
228 requests_session=requests_session,
229 client_cert_path=client_cert_path,
230 CA_certs_path=CA_certs_path, source_port=source_port,
231 request_timeout=request_timeout)
232 elif protocol == const.UDP:
233 data = self.make_udp_request(
234 vip_address, port=protocol_port, timeout=request_timeout,
235 source_port=source_port)
236 else:
237 message = ("Unknown protocol %s. Unable to check if the "
238 "load balancer is balanced.", protocol)
239 LOG.error(message)
240 raise Exception(message)
241 return data
242
243 def check_members_balanced(
244 self, vip_address, traffic_member_count=2, protocol=const.HTTP,
245 HTTPS_verify=True, protocol_port=80, persistent=True, repeat=20,
246 client_cert_path=None, CA_certs_path=None, request_interval=2,
247 request_timeout=10, source_port=None, delay=None):
248 """Checks that members are evenly balanced behind a VIP.
249
250 :param vip_address: The VIP address to test.
251 :param traffic_member_count: The expected number of members.
252 :param protocol: The protocol to use for the test.
253 :param HTTPS_verify: How to verify the TLS certificate. True: verify
254 using the system CA certificates. False: Do not
255 verify the VIP certificate. <path>: Filesytem path
256 to a CA certificate bundle file or directory. For
257 directories, the directory must be processed using
258 the c_rehash utility from openssl.
259 :param protocol_port: The port number to use for the test.
260 :param persistent: True when the test should persist cookies and use
261 the protocol keepalive mechanism with the target.
262 This may include maintaining a connection to the
263 member server across requests.
264 :param repeat: The number of requests to make against the VIP.
265 :param request_timeout: The maximum time, in seconds, to attempt
266 requests.
267 :param client_cert_path: Filesystem path to a file with the client
268 private key and certificate.
269 :param CA_certs_path: Filesystem path to a file containing CA
270 certificates to use for HTTPS validation.
271 :param source_port: If set, the request will come from this source port
272 number. If None, a random port will be used.
273 :param delay: The time to pause between requests in seconds, can be
274 fractional.
275 """
276 if (ipaddress.ip_address(vip_address).version == 6 and
277 protocol != const.UDP):
278 vip_address = '[{}]'.format(vip_address)
279
280 requests_session = None
281 if persistent:
282 requests_session = requests.Session()
283
284 self._wait_for_lb_functional(
285 vip_address, traffic_member_count, protocol_port, protocol,
286 HTTPS_verify, requests_session=requests_session,
287 source_port=source_port)
288
Brian Haley52531e22021-01-21 16:52:09 -0500289 if source_port:
290 LOG.debug('Using source port %s for request(s)', source_port)
291
Michael Johnson89bdbcd2020-03-19 15:59:19 -0700292 response_counts = {}
293 # Send a number requests to lb vip
294 for i in range(repeat):
295 try:
296 data = self.make_request(
297 vip_address, protocol=protocol, HTTPS_verify=HTTPS_verify,
298 protocol_port=protocol_port,
299 requests_session=requests_session,
300 client_cert_path=client_cert_path,
301 CA_certs_path=CA_certs_path, source_port=source_port,
302 request_timeout=request_timeout)
303
304 if data in response_counts:
305 response_counts[data] += 1
306 else:
307 response_counts[data] = 1
308 if delay is not None:
309 time.sleep(delay)
310 except Exception:
311 LOG.exception('Failed to send request to loadbalancer vip')
312 if persistent:
313 requests_session.close()
314 raise Exception('Failed to connect to lb')
315 if persistent:
316 requests_session.close()
317 LOG.debug('Loadbalancer response totals: %s', response_counts)
318
319 # Ensure the correct number of members responded
320 self.assertEqual(traffic_member_count, len(response_counts))
321
322 # Ensure both members got the same number of responses
323 self.assertEqual(1, len(set(response_counts.values())))
324
325 def assertConsistentResponse(self, response, url, method='GET', repeat=10,
326 redirect=False, timeout=2,
327 expect_connection_error=False, **kwargs):
328 """Assert that a request to URL gets the expected response.
329
330 :param response: Expected response in format (status_code, content).
331 :param url: The URL to request.
332 :param method: The HTTP method to use (GET, POST, PUT, etc)
333 :param repeat: How many times to test the response.
334 :param data: Optional data to send in the request.
335 :param headers: Optional headers to send in the request.
336 :param cookies: Optional cookies to send in the request.
337 :param redirect: Is the request a redirect? If true, assume the passed
338 content should be the next URL in the chain.
339 :param timeout: Optional seconds to wait for the server to send data.
340 :param expect_connection_error: Should we expect a connection error
341 :param expect_timeout: Should we expect a connection timeout
342
343 :return: boolean success status
344
345 :raises: testtools.matchers.MismatchError
346 """
347 session = requests.Session()
348 response_code, response_content = response
349
350 for i in range(repeat):
351 if url.startswith(const.HTTP.lower()):
352 if expect_connection_error:
353 self.assertRaises(
354 requests.exceptions.ConnectionError, session.request,
355 method, url, allow_redirects=not redirect,
356 timeout=timeout, **kwargs)
357 continue
358
359 req = session.request(method, url,
360 allow_redirects=not redirect,
361 timeout=timeout, **kwargs)
362 if response_code:
363 self.assertEqual(response_code, req.status_code)
364 if redirect:
365 self.assertTrue(req.is_redirect)
366 self.assertEqual(response_content,
367 session.get_redirect_target(req))
368 elif response_content:
369 self.assertEqual(str(response_content), req.text)
370 elif url.startswith(const.UDP.lower()):
371 parsed_url = urlparse(url)
372 if expect_connection_error:
373 self.assertRaises(exceptions.TimeoutException,
374 self.make_udp_request,
375 parsed_url.hostname,
376 port=parsed_url.port, timeout=timeout)
377 continue
378
379 data = self.make_udp_request(parsed_url.hostname,
380 port=parsed_url.port,
381 timeout=timeout)
382 self.assertEqual(response_content, data)
383
384 def _wait_for_lb_functional(
385 self, vip_address, traffic_member_count, protocol_port, protocol,
386 HTTPS_verify, client_cert_path=None, CA_certs_path=None,
387 request_interval=2, request_timeout=10, requests_session=None,
388 source_port=None):
389 start = time.time()
390 response_counts = {}
391
392 # Send requests to the load balancer until at least
393 # "traffic_member_count" members have replied (ensure network
394 # connectivity is functional between the load balancer and the members)
395 while time.time() - start < CONF.load_balancer.build_timeout:
396 try:
397 data = self.make_request(
398 vip_address, protocol=protocol, HTTPS_verify=HTTPS_verify,
399 protocol_port=protocol_port,
400 client_cert_path=client_cert_path,
401 CA_certs_path=CA_certs_path, source_port=source_port,
402 request_timeout=request_timeout,
403 requests_session=requests_session)
404
405 if data in response_counts:
406 response_counts[data] += 1
407 else:
408 response_counts[data] = 1
409
410 if traffic_member_count == len(response_counts):
411 LOG.debug('Loadbalancer response totals: %s',
412 response_counts)
413 time.sleep(1)
414 return
Fernando Royo13cb2ec2025-05-21 17:54:20 +0200415 except Exception as e:
Michael Johnson89bdbcd2020-03-19 15:59:19 -0700416 LOG.warning('Server is not passing initial traffic. Waiting.')
Fernando Royo13cb2ec2025-05-21 17:54:20 +0200417 if "[Errno 98] Address already in use" in str(e):
418 raise e
Gregory Thiemonged698a182023-04-06 09:50:38 +0200419 time.sleep(request_interval)
Michael Johnson89bdbcd2020-03-19 15:59:19 -0700420
421 LOG.debug('Loadbalancer wait for load balancer response totals: %s',
422 response_counts)
423 message = ('Server %s on port %s did not begin passing traffic within '
424 'the timeout period. Failing test.' % (vip_address,
425 protocol_port))
426 LOG.error(message)
427 raise Exception(message)
Arkady Shtemplera186f062020-09-30 18:20:03 +0300428
429 def make_udp_requests_with_retries(
430 self, vip_address, number_of_retries, dst_port,
431 src_port=None, socket_timeout=20):
432 """Send UDP packets using retries mechanism
433
434 The delivery of data to the destination cannot be guaranteed in UDP.
435 In case when UDP package is getting lost and we might want to check
436 what could be the reason for that (Network issues or Server Side),
437 well need to send more packets to get into the conclusion.
438
439 :param vip_address: LB VIP address
440 :param number_of_retries: integer number of retries
441 :param dst_port: UDP server destination port
442 :param src_port: UDP source port to bind for UDP connection
443 :param socket_timeout: UDP socket timeout
444 :return: None if all UPD retries failed, else first successful
445 response data from UDP server.
446 """
447 retry_number = 0
448 received_data = None
449 while retry_number < number_of_retries:
Michael Johnson77b8bae2024-11-08 01:39:29 +0000450 LOG.info('make_udp_requests_with_retries attempt number: %s',
451 retry_number)
Arkady Shtemplera186f062020-09-30 18:20:03 +0300452 retry_number += 1
453 try:
454 received_data = self.make_udp_request(
455 vip_address, dst_port, timeout=socket_timeout,
456 source_port=src_port)
457 break
458 except Exception as e:
Michael Johnson77b8bae2024-11-08 01:39:29 +0000459 LOG.warning('make_udp_request has failed with: %s', e)
Arkady Shtemplera186f062020-09-30 18:20:03 +0300460 return received_data