summaryrefslogtreecommitdiffstats
path: root/test/lib/ansible_test/_internal/http.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/lib/ansible_test/_internal/http.py')
-rw-r--r--test/lib/ansible_test/_internal/http.py181
1 files changed, 181 insertions, 0 deletions
diff --git a/test/lib/ansible_test/_internal/http.py b/test/lib/ansible_test/_internal/http.py
new file mode 100644
index 00000000..6607a10b
--- /dev/null
+++ b/test/lib/ansible_test/_internal/http.py
@@ -0,0 +1,181 @@
+"""
+Primitive replacement for requests to avoid extra dependency.
+Avoids use of urllib2 due to lack of SNI support.
+"""
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import time
+
+try:
+ from urllib import urlencode
+except ImportError:
+ # noinspection PyCompatibility, PyUnresolvedReferences
+ from urllib.parse import urlencode # pylint: disable=locally-disabled, import-error, no-name-in-module
+
+try:
+ # noinspection PyCompatibility
+ from urlparse import urlparse, urlunparse, parse_qs
+except ImportError:
+ # noinspection PyCompatibility, PyUnresolvedReferences
+ from urllib.parse import urlparse, urlunparse, parse_qs # pylint: disable=locally-disabled, ungrouped-imports
+
+from .util import (
+ ApplicationError,
+ SubprocessError,
+ display,
+)
+
+from .util_common import (
+ CommonConfig,
+ run_command,
+)
+
+
+class HttpClient:
+ """Make HTTP requests via curl."""
+ def __init__(self, args, always=False, insecure=False, proxy=None):
+ """
+ :type args: CommonConfig
+ :type always: bool
+ :type insecure: bool
+ """
+ self.args = args
+ self.always = always
+ self.insecure = insecure
+ self.proxy = proxy
+
+ self.username = None
+ self.password = None
+
+ def get(self, url):
+ """
+ :type url: str
+ :rtype: HttpResponse
+ """
+ return self.request('GET', url)
+
+ def delete(self, url):
+ """
+ :type url: str
+ :rtype: HttpResponse
+ """
+ return self.request('DELETE', url)
+
+ def put(self, url, data=None, headers=None):
+ """
+ :type url: str
+ :type data: str | None
+ :type headers: dict[str, str] | None
+ :rtype: HttpResponse
+ """
+ return self.request('PUT', url, data, headers)
+
+ def request(self, method, url, data=None, headers=None):
+ """
+ :type method: str
+ :type url: str
+ :type data: str | None
+ :type headers: dict[str, str] | None
+ :rtype: HttpResponse
+ """
+ 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]
+
+ if self.proxy:
+ cmd += ['-x', self.proxy]
+
+ cmd += [url]
+
+ 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 = ''
+
+ while True:
+ attempts += 1
+
+ try:
+ stdout = run_command(self.args, cmd, capture=True, always=self.always, cmd_verbosity=2)[0]
+ break
+ except SubprocessError as ex:
+ if ex.status in retry_on_status and attempts < max_attempts:
+ display.warning(u'%s' % ex)
+ time.sleep(sleep_seconds)
+ continue
+
+ raise
+
+ if self.args.explain and not self.always:
+ return HttpResponse(method, url, 200, '')
+
+ header, body = stdout.split('\r\n\r\n', 1)
+
+ response_headers = header.split('\r\n')
+ first_line = response_headers[0]
+ http_response = first_line.split(' ')
+ status_code = int(http_response[1])
+
+ return HttpResponse(method, url, status_code, body)
+
+
+class HttpResponse:
+ """HTTP response from curl."""
+ def __init__(self, method, url, status_code, response):
+ """
+ :type method: str
+ :type url: str
+ :type status_code: int
+ :type response: str
+ """
+ self.method = method
+ self.url = url
+ self.status_code = status_code
+ self.response = response
+
+ def json(self):
+ """
+ :rtype: any
+ """
+ try:
+ return json.loads(self.response)
+ except ValueError:
+ raise HttpError(self.status_code, 'Cannot parse response to %s %s as JSON:\n%s' % (self.method, self.url, self.response))
+
+
+class HttpError(ApplicationError):
+ """HTTP response as an error."""
+ def __init__(self, status, message):
+ """
+ :type status: int
+ :type message: str
+ """
+ super(HttpError, self).__init__('%s: %s' % (status, message))
+ self.status = status