diff options
author | Matt Clay <matt@mystile.com> | 2025-01-14 17:59:42 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-14 17:59:42 +0100 |
commit | 7677bf1c9b20ea2a4d575211179e76a51ed10668 (patch) | |
tree | 6164333e70ec959b2eb9edd5d33613e4ed729419 /test | |
parent | fix incongruent ansible-vault cli options (#84494) (diff) | |
download | ansible-7677bf1c9b20ea2a4d575211179e76a51ed10668.tar.xz ansible-7677bf1c9b20ea2a4d575211179e76a51ed10668.zip |
ansible-test - Use urllib intead of curl (#84551)
Also added automatic retries on HTTP request exceptions, since all currently implemented methods (GET/PUT/DELETE) are idempotent.
Diffstat (limited to 'test')
-rw-r--r-- | test/lib/ansible_test/_internal/http.py | 98 |
1 files changed, 41 insertions, 57 deletions
diff --git a/test/lib/ansible_test/_internal/http.py b/test/lib/ansible_test/_internal/http.py index 66afc60d8e..7317aae10d 100644 --- a/test/lib/ansible_test/_internal/http.py +++ b/test/lib/ansible_test/_internal/http.py @@ -1,36 +1,29 @@ -""" -Primitive replacement for requests to avoid extra dependency. -Avoids use of urllib2 due to lack of SNI support. -""" +"""A simple HTTP client.""" from __future__ import annotations +import http.client import json import time import typing as t +import urllib.error +import urllib.request from .util import ( ApplicationError, - SubprocessError, display, ) from .util_common import ( CommonConfig, - run_command, ) class HttpClient: - """Make HTTP requests via curl.""" + """Make HTTP requests.""" - def __init__(self, args: CommonConfig, always: bool = False, insecure: bool = False, proxy: t.Optional[str] = None) -> None: + def __init__(self, args: CommonConfig, always: bool = False) -> None: self.args = args self.always = always - self.insecure = insecure - self.proxy = proxy - - self.username = None - self.password = None def get(self, url: str) -> HttpResponse: """Perform an HTTP GET and return the response.""" @@ -46,74 +39,65 @@ class HttpClient: def request(self, method: str, url: str, data: t.Optional[str] = None, headers: t.Optional[dict[str, str]] = None) -> HttpResponse: """Perform an HTTP request and return the response.""" - cmd = ['curl', '-s', '-S', '-i', '-X', method] - - if self.insecure: - cmd += ['--insecure'] - if headers is None: headers = {} - headers['Expect'] = '' # don't send expect continue header - - if self.username: - if self.password: - display.sensitive.add(self.password) - cmd += ['-u', '%s:%s' % (self.username, self.password)] - else: - cmd += ['-u', self.username] - - for header in headers.keys(): - cmd += ['-H', '%s: %s' % (header, headers[header])] - - if data is not None: - cmd += ['-d', data] + data_bytes = data.encode() if data else None - if self.proxy: - cmd += ['-x', self.proxy] + request = urllib.request.Request(method=method, url=url, data=data_bytes, headers=headers) + response: http.client.HTTPResponse - cmd += [url] + display.info(f'HTTP {method} {url}', verbosity=2) attempts = 0 max_attempts = 3 sleep_seconds = 3 - # curl error codes which are safe to retry (request never sent to server) - retry_on_status = ( - 6, # CURLE_COULDNT_RESOLVE_HOST - ) - - stdout = '' + status_code = 200 + reason = 'OK' + body_bytes = b'' while True: attempts += 1 - try: - stdout = run_command(self.args, cmd, capture=True, always=self.always, cmd_verbosity=2)[0] + start = time.monotonic() + + if self.args.explain and not self.always: break - except SubprocessError as ex: - if ex.status in retry_on_status and attempts < max_attempts: - display.warning('%s' % ex) - time.sleep(sleep_seconds) - continue - raise + try: + try: + with urllib.request.urlopen(request) as response: + status_code = response.status + reason = response.reason + body_bytes = response.read() + except urllib.error.HTTPError as ex: + status_code = ex.status + reason = ex.reason + body_bytes = ex.read() + except Exception as ex: # pylint: disable=broad-exception-caught + if attempts >= max_attempts: + raise + + # all currently implemented methods are idempotent, so retries are unconditionally supported + duration = time.monotonic() - start + display.warning(f'{type(ex).__module__}.{type(ex).__name__}: {ex} [{duration:.2f} seconds]') + time.sleep(sleep_seconds) + + continue - if self.args.explain and not self.always: - return HttpResponse(method, url, 200, '') + break - header, body = stdout.split('\r\n\r\n', 1) + duration = time.monotonic() - start + display.info(f'HTTP {method} {url} -> HTTP {status_code} ({reason}) [{len(body_bytes)} bytes, {duration:.2f} seconds]', verbosity=3) - response_headers = header.split('\r\n') - first_line = response_headers[0] - http_response = first_line.split(' ') - status_code = int(http_response[1]) + body = body_bytes.decode() return HttpResponse(method, url, status_code, body) class HttpResponse: - """HTTP response from curl.""" + """HTTP response.""" def __init__(self, method: str, url: str, status_code: int, response: str) -> None: self.method = method |