blob: 5972d5401519bb0a20ef5e38ba55391f9a30be59 [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.')
415 time.sleep(1)
416
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)