blob: a93e2ebc369ccde3b6f0a42d54edb7bf47729ed3 [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
287 response_counts = {}
288 # Send a number requests to lb vip
289 for i in range(repeat):
290 try:
291 data = self.make_request(
292 vip_address, protocol=protocol, HTTPS_verify=HTTPS_verify,
293 protocol_port=protocol_port,
294 requests_session=requests_session,
295 client_cert_path=client_cert_path,
296 CA_certs_path=CA_certs_path, source_port=source_port,
297 request_timeout=request_timeout)
298
299 if data in response_counts:
300 response_counts[data] += 1
301 else:
302 response_counts[data] = 1
303 if delay is not None:
304 time.sleep(delay)
305 except Exception:
306 LOG.exception('Failed to send request to loadbalancer vip')
307 if persistent:
308 requests_session.close()
309 raise Exception('Failed to connect to lb')
310 if persistent:
311 requests_session.close()
312 LOG.debug('Loadbalancer response totals: %s', response_counts)
313
314 # Ensure the correct number of members responded
315 self.assertEqual(traffic_member_count, len(response_counts))
316
317 # Ensure both members got the same number of responses
318 self.assertEqual(1, len(set(response_counts.values())))
319
320 def assertConsistentResponse(self, response, url, method='GET', repeat=10,
321 redirect=False, timeout=2,
322 expect_connection_error=False, **kwargs):
323 """Assert that a request to URL gets the expected response.
324
325 :param response: Expected response in format (status_code, content).
326 :param url: The URL to request.
327 :param method: The HTTP method to use (GET, POST, PUT, etc)
328 :param repeat: How many times to test the response.
329 :param data: Optional data to send in the request.
330 :param headers: Optional headers to send in the request.
331 :param cookies: Optional cookies to send in the request.
332 :param redirect: Is the request a redirect? If true, assume the passed
333 content should be the next URL in the chain.
334 :param timeout: Optional seconds to wait for the server to send data.
335 :param expect_connection_error: Should we expect a connection error
336 :param expect_timeout: Should we expect a connection timeout
337
338 :return: boolean success status
339
340 :raises: testtools.matchers.MismatchError
341 """
342 session = requests.Session()
343 response_code, response_content = response
344
345 for i in range(repeat):
346 if url.startswith(const.HTTP.lower()):
347 if expect_connection_error:
348 self.assertRaises(
349 requests.exceptions.ConnectionError, session.request,
350 method, url, allow_redirects=not redirect,
351 timeout=timeout, **kwargs)
352 continue
353
354 req = session.request(method, url,
355 allow_redirects=not redirect,
356 timeout=timeout, **kwargs)
357 if response_code:
358 self.assertEqual(response_code, req.status_code)
359 if redirect:
360 self.assertTrue(req.is_redirect)
361 self.assertEqual(response_content,
362 session.get_redirect_target(req))
363 elif response_content:
364 self.assertEqual(str(response_content), req.text)
365 elif url.startswith(const.UDP.lower()):
366 parsed_url = urlparse(url)
367 if expect_connection_error:
368 self.assertRaises(exceptions.TimeoutException,
369 self.make_udp_request,
370 parsed_url.hostname,
371 port=parsed_url.port, timeout=timeout)
372 continue
373
374 data = self.make_udp_request(parsed_url.hostname,
375 port=parsed_url.port,
376 timeout=timeout)
377 self.assertEqual(response_content, data)
378
379 def _wait_for_lb_functional(
380 self, vip_address, traffic_member_count, protocol_port, protocol,
381 HTTPS_verify, client_cert_path=None, CA_certs_path=None,
382 request_interval=2, request_timeout=10, requests_session=None,
383 source_port=None):
384 start = time.time()
385 response_counts = {}
386
387 # Send requests to the load balancer until at least
388 # "traffic_member_count" members have replied (ensure network
389 # connectivity is functional between the load balancer and the members)
390 while time.time() - start < CONF.load_balancer.build_timeout:
391 try:
392 data = self.make_request(
393 vip_address, protocol=protocol, HTTPS_verify=HTTPS_verify,
394 protocol_port=protocol_port,
395 client_cert_path=client_cert_path,
396 CA_certs_path=CA_certs_path, source_port=source_port,
397 request_timeout=request_timeout,
398 requests_session=requests_session)
399
400 if data in response_counts:
401 response_counts[data] += 1
402 else:
403 response_counts[data] = 1
404
405 if traffic_member_count == len(response_counts):
406 LOG.debug('Loadbalancer response totals: %s',
407 response_counts)
408 time.sleep(1)
409 return
410 except Exception:
411 LOG.warning('Server is not passing initial traffic. Waiting.')
412 time.sleep(1)
413
414 LOG.debug('Loadbalancer wait for load balancer response totals: %s',
415 response_counts)
416 message = ('Server %s on port %s did not begin passing traffic within '
417 'the timeout period. Failing test.' % (vip_address,
418 protocol_port))
419 LOG.error(message)
420 raise Exception(message)