Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 1 | # Copyright 2017 GoDaddy |
| 2 | # Copyright 2017 Catalyst IT Ltd |
| 3 | # Copyright 2018 Rackspace US Inc. All rights reserved. |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 4 | # Copyright 2020 Red Hat, Inc. All rights reserved. |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 5 | # |
| 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 Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 17 | import errno |
| 18 | import ipaddress |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 19 | import requests |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 20 | import socket |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 21 | import time |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 22 | from urllib.parse import urlparse |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 23 | |
| 24 | from oslo_log import log as logging |
| 25 | from tempest import config |
| 26 | from tempest.lib import exceptions |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 27 | from tempest import test |
| 28 | |
| 29 | from octavia_tempest_plugin.common import constants as const |
| 30 | from octavia_tempest_plugin.common import requests_adapters |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 31 | |
| 32 | CONF = config.CONF |
| 33 | LOG = logging.getLogger(__name__) |
| 34 | |
| 35 | |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 36 | class ValidatorsMixin(test.BaseTestCase): |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 37 | |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 38 | @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 Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 79 | 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 Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 91 | response_status_code = response.status_code |
| 92 | response_text = response.text |
| 93 | response.close() |
| 94 | if response_status_code != expected_status_code: |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 95 | raise exceptions.InvalidHttpSuccessCode( |
| 96 | '{0} is not the expected code {1}'.format( |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 97 | response_status_code, expected_status_code)) |
| 98 | if expected_body and response_text != expected_body: |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 99 | details = '{} does not match expected {}'.format( |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 100 | response_text, expected_body) |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 101 | raise exceptions.InvalidHTTPResponseBody( |
| 102 | resp_body=details) |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 103 | if requests_session is None: |
| 104 | session.close() |
| 105 | return response_text |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 106 | except requests.exceptions.Timeout: |
| 107 | # Don't sleep as we have already waited the interval. |
Jonathan Rosser | 7654d2e | 2019-06-24 14:55:17 +0100 | [diff] [blame] | 108 | LOG.info('Request for {} timed out. Retrying.'.format(URL)) |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 109 | except (exceptions.InvalidHttpSuccessCode, |
| 110 | exceptions.InvalidHTTPResponseBody, |
| 111 | requests.exceptions.SSLError): |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 112 | if requests_session is None: |
| 113 | session.close() |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 114 | raise |
| 115 | except Exception as e: |
| 116 | LOG.info('Validate URL got exception: {0}. ' |
| 117 | 'Retrying.'.format(e)) |
| 118 | time.sleep(request_interval) |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 119 | if requests_session is None: |
| 120 | session.close() |
Jude Cross | 986e3f5 | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 121 | raise exceptions.TimeoutException() |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 122 | |
| 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 Haley | 52531e2 | 2021-01-21 16:52:09 -0500 | [diff] [blame] | 287 | if source_port: |
| 288 | LOG.debug('Using source port %s for request(s)', source_port) |
| 289 | |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 290 | 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 Thiemonge | d698a18 | 2023-04-06 09:50:38 +0200 | [diff] [blame] | 415 | time.sleep(request_interval) |
Michael Johnson | 89bdbcd | 2020-03-19 15:59:19 -0700 | [diff] [blame] | 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) |
Arkady Shtempler | a186f06 | 2020-09-30 18:20:03 +0300 | [diff] [blame] | 424 | |
| 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 |