Merge "Add api docs to the rest client"
This commit is contained in:
@@ -36,7 +36,27 @@ HTTP_SUCCESS = (200, 201, 202, 203, 204, 205, 206, 207)
|
|||||||
|
|
||||||
|
|
||||||
class RestClient(object):
|
class RestClient(object):
|
||||||
|
"""Unified OpenStack RestClient class
|
||||||
|
|
||||||
|
This class is used for building openstack api clients on top of. It is
|
||||||
|
intended to provide a base layer for wrapping outgoing http requests in
|
||||||
|
keystone auth as well as providing response code checking and error
|
||||||
|
handling.
|
||||||
|
|
||||||
|
:param auth_provider: an auth provider object used to wrap requests in auth
|
||||||
|
:param str service: The service name to use for the catalog lookup
|
||||||
|
:param str region: The region to use for the catalog lookup
|
||||||
|
:param str endpoint_type: The endpoint type to use for the catalog lookup
|
||||||
|
:param int build_interval: Time in seconds between to status checks in
|
||||||
|
wait loops
|
||||||
|
:param int build_timeout: Timeout in seconds to wait for a wait operation.
|
||||||
|
:param bool disable_ssl_certificate_validation: Set to true to disable ssl
|
||||||
|
certificate validation
|
||||||
|
:param str ca_certs: File containing the CA Bundle to use in verifying a
|
||||||
|
TLS server cert
|
||||||
|
:param str trace_request: Regex to use for specifying logging the entirety
|
||||||
|
of the request and response payload
|
||||||
|
"""
|
||||||
TYPE = "json"
|
TYPE = "json"
|
||||||
|
|
||||||
# The version of the API this client implements
|
# The version of the API this client implements
|
||||||
@@ -74,6 +94,18 @@ class RestClient(object):
|
|||||||
return self.TYPE
|
return self.TYPE
|
||||||
|
|
||||||
def get_headers(self, accept_type=None, send_type=None):
|
def get_headers(self, accept_type=None, send_type=None):
|
||||||
|
"""Return the default headers which will be used with outgoing requests
|
||||||
|
|
||||||
|
:param str accept_type: The media type to use for the Accept header, if
|
||||||
|
one isn't provided the object var TYPE will be
|
||||||
|
used
|
||||||
|
:param str send_type: The media-type to use for the Content-Type
|
||||||
|
header, if one isn't provided the object var
|
||||||
|
TYPE will be used
|
||||||
|
:rtype: dict
|
||||||
|
:return: The dictionary of headers which can be used in the headers
|
||||||
|
dict for outgoing request
|
||||||
|
"""
|
||||||
if accept_type is None:
|
if accept_type is None:
|
||||||
accept_type = self._get_type()
|
accept_type = self._get_type()
|
||||||
if send_type is None:
|
if send_type is None:
|
||||||
@@ -94,22 +126,48 @@ class RestClient(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def user(self):
|
def user(self):
|
||||||
|
"""The username used for requests
|
||||||
|
|
||||||
|
:rtype: string
|
||||||
|
:return: The username being used for requests
|
||||||
|
"""
|
||||||
|
|
||||||
return self.auth_provider.credentials.username
|
return self.auth_provider.credentials.username
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_id(self):
|
def user_id(self):
|
||||||
|
"""The user_id used for requests
|
||||||
|
|
||||||
|
:rtype: string
|
||||||
|
:return: The user id being used for requests
|
||||||
|
"""
|
||||||
return self.auth_provider.credentials.user_id
|
return self.auth_provider.credentials.user_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tenant_name(self):
|
def tenant_name(self):
|
||||||
|
"""The tenant/project being used for requests
|
||||||
|
|
||||||
|
:rtype: string
|
||||||
|
:return: The tenant/project name being used for requests
|
||||||
|
"""
|
||||||
return self.auth_provider.credentials.tenant_name
|
return self.auth_provider.credentials.tenant_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tenant_id(self):
|
def tenant_id(self):
|
||||||
|
"""The tenant/project id being used for requests
|
||||||
|
|
||||||
|
:rtype: string
|
||||||
|
:return: The tenant/project id being used for requests
|
||||||
|
"""
|
||||||
return self.auth_provider.credentials.tenant_id
|
return self.auth_provider.credentials.tenant_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def password(self):
|
def password(self):
|
||||||
|
"""The password being used for requests
|
||||||
|
|
||||||
|
:rtype: string
|
||||||
|
:return: The password being used for requests
|
||||||
|
"""
|
||||||
return self.auth_provider.credentials.password
|
return self.auth_provider.credentials.password
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -143,6 +201,18 @@ class RestClient(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def expected_success(cls, expected_code, read_code):
|
def expected_success(cls, expected_code, read_code):
|
||||||
|
"""Check expected success response code against the http response
|
||||||
|
|
||||||
|
:param int expected_code: The response code that is expected.
|
||||||
|
Optionally a list of integers can be used
|
||||||
|
to specify multiple valid success codes
|
||||||
|
:param int read_code: The response code which was returned in the
|
||||||
|
response
|
||||||
|
:raises AssertionError: if the expected_code isn't a valid http success
|
||||||
|
response code
|
||||||
|
:raises exceptions.InvalidHttpSuccessCode: if the read code isn't an
|
||||||
|
expected http success code
|
||||||
|
"""
|
||||||
assert_msg = ("This function only allowed to use for HTTP status"
|
assert_msg = ("This function only allowed to use for HTTP status"
|
||||||
"codes which explicitly defined in the RFC 7231 & 4918."
|
"codes which explicitly defined in the RFC 7231 & 4918."
|
||||||
"{0} is not a defined Success Code!"
|
"{0} is not a defined Success Code!"
|
||||||
@@ -166,27 +236,125 @@ class RestClient(object):
|
|||||||
raise exceptions.InvalidHttpSuccessCode(details)
|
raise exceptions.InvalidHttpSuccessCode(details)
|
||||||
|
|
||||||
def post(self, url, body, headers=None, extra_headers=False):
|
def post(self, url, body, headers=None, extra_headers=False):
|
||||||
|
"""Send a HTTP POST request using keystone auth
|
||||||
|
|
||||||
|
:param str url: the relative url to send the post request to
|
||||||
|
:param dict body: the request body
|
||||||
|
:param dict headers: The headers to use for the request
|
||||||
|
:param dict extra_headers: If the headers returned by the get_headers()
|
||||||
|
method are to be used but additional headers
|
||||||
|
are needed in the request pass them in as a
|
||||||
|
dict
|
||||||
|
:return: a tuple with the first entry containing the response headers
|
||||||
|
and the second the response body
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
return self.request('POST', url, extra_headers, headers, body)
|
return self.request('POST', url, extra_headers, headers, body)
|
||||||
|
|
||||||
def get(self, url, headers=None, extra_headers=False):
|
def get(self, url, headers=None, extra_headers=False):
|
||||||
|
"""Send a HTTP GET request using keystone service catalog and auth
|
||||||
|
|
||||||
|
:param str url: the relative url to send the post request to
|
||||||
|
:param dict headers: The headers to use for the request
|
||||||
|
:param dict extra_headers: If the headers returned by the get_headers()
|
||||||
|
method are to be used but additional headers
|
||||||
|
are needed in the request pass them in as a
|
||||||
|
dict
|
||||||
|
:return: a tuple with the first entry containing the response headers
|
||||||
|
and the second the response body
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
return self.request('GET', url, extra_headers, headers)
|
return self.request('GET', url, extra_headers, headers)
|
||||||
|
|
||||||
def delete(self, url, headers=None, body=None, extra_headers=False):
|
def delete(self, url, headers=None, body=None, extra_headers=False):
|
||||||
|
"""Send a HTTP DELETE request using keystone service catalog and auth
|
||||||
|
|
||||||
|
:param str url: the relative url to send the post request to
|
||||||
|
:param dict body: the request body
|
||||||
|
:param dict headers: The headers to use for the request
|
||||||
|
:param dict extra_headers: If the headers returned by the get_headers()
|
||||||
|
method are to be used but additional headers
|
||||||
|
are needed in the request pass them in as a
|
||||||
|
dict
|
||||||
|
:return: a tuple with the first entry containing the response headers
|
||||||
|
and the second the response body
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
return self.request('DELETE', url, extra_headers, headers, body)
|
return self.request('DELETE', url, extra_headers, headers, body)
|
||||||
|
|
||||||
def patch(self, url, body, headers=None, extra_headers=False):
|
def patch(self, url, body, headers=None, extra_headers=False):
|
||||||
|
"""Send a HTTP PATCH request using keystone service catalog and auth
|
||||||
|
|
||||||
|
:param str url: the relative url to send the post request to
|
||||||
|
:param dict body: the request body
|
||||||
|
:param dict headers: The headers to use for the request
|
||||||
|
:param dict extra_headers: If the headers returned by the get_headers()
|
||||||
|
method are to be used but additional headers
|
||||||
|
are needed in the request pass them in as a
|
||||||
|
dict
|
||||||
|
:return: a tuple with the first entry containing the response headers
|
||||||
|
and the second the response body
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
return self.request('PATCH', url, extra_headers, headers, body)
|
return self.request('PATCH', url, extra_headers, headers, body)
|
||||||
|
|
||||||
def put(self, url, body, headers=None, extra_headers=False):
|
def put(self, url, body, headers=None, extra_headers=False):
|
||||||
|
"""Send a HTTP PUT request using keystone service catalog and auth
|
||||||
|
|
||||||
|
:param str url: the relative url to send the post request to
|
||||||
|
:param dict body: the request body
|
||||||
|
:param dict headers: The headers to use for the request
|
||||||
|
:param dict extra_headers: If the headers returned by the get_headers()
|
||||||
|
method are to be used but additional headers
|
||||||
|
are needed in the request pass them in as a
|
||||||
|
dict
|
||||||
|
:return: a tuple with the first entry containing the response headers
|
||||||
|
and the second the response body
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
return self.request('PUT', url, extra_headers, headers, body)
|
return self.request('PUT', url, extra_headers, headers, body)
|
||||||
|
|
||||||
def head(self, url, headers=None, extra_headers=False):
|
def head(self, url, headers=None, extra_headers=False):
|
||||||
|
"""Send a HTTP HEAD request using keystone service catalog and auth
|
||||||
|
|
||||||
|
:param str url: the relative url to send the post request to
|
||||||
|
:param dict headers: The headers to use for the request
|
||||||
|
:param dict extra_headers: If the headers returned by the get_headers()
|
||||||
|
method are to be used but additional headers
|
||||||
|
are needed in the request pass them in as a
|
||||||
|
dict
|
||||||
|
:return: a tuple with the first entry containing the response headers
|
||||||
|
and the second the response body
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
return self.request('HEAD', url, extra_headers, headers)
|
return self.request('HEAD', url, extra_headers, headers)
|
||||||
|
|
||||||
def copy(self, url, headers=None, extra_headers=False):
|
def copy(self, url, headers=None, extra_headers=False):
|
||||||
|
"""Send a HTTP COPY request using keystone service catalog and auth
|
||||||
|
|
||||||
|
:param str url: the relative url to send the post request to
|
||||||
|
:param dict headers: The headers to use for the request
|
||||||
|
:param dict extra_headers: If the headers returned by the get_headers()
|
||||||
|
method are to be used but additional headers
|
||||||
|
are needed in the request pass them in as a
|
||||||
|
dict
|
||||||
|
:return: a tuple with the first entry containing the response headers
|
||||||
|
and the second the response body
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
return self.request('COPY', url, extra_headers, headers)
|
return self.request('COPY', url, extra_headers, headers)
|
||||||
|
|
||||||
def get_versions(self):
|
def get_versions(self):
|
||||||
|
"""Get the versions on a endpoint from the keystone catalog
|
||||||
|
|
||||||
|
This method will make a GET request on the baseurl from the keystone
|
||||||
|
catalog to return a list of API versions. It is expected that a GET
|
||||||
|
on the endpoint in the catalog will return a list of supported API
|
||||||
|
versions.
|
||||||
|
|
||||||
|
:return tuple with response headers and list of version numbers
|
||||||
|
:rtype: tuple
|
||||||
|
"""
|
||||||
resp, body = self.get('')
|
resp, body = self.get('')
|
||||||
body = self._parse_resp(body)
|
body = self._parse_resp(body)
|
||||||
versions = map(lambda x: x['id'], body)
|
versions = map(lambda x: x['id'], body)
|
||||||
@@ -294,6 +462,20 @@ class RestClient(object):
|
|||||||
return body
|
return body
|
||||||
|
|
||||||
def response_checker(self, method, resp, resp_body):
|
def response_checker(self, method, resp, resp_body):
|
||||||
|
"""A sanity check on the response from a HTTP request
|
||||||
|
|
||||||
|
This method does a sanity check on whether the response from an HTTP
|
||||||
|
request conforms the HTTP RFC.
|
||||||
|
|
||||||
|
:param str method: The HTTP verb of the request associated with the
|
||||||
|
response being passed in.
|
||||||
|
:param resp: The response headers
|
||||||
|
:param resp_body: The body of the response
|
||||||
|
:raises ResponseWithNonEmptyBody: If the response with the status code
|
||||||
|
is not supposed to have a body
|
||||||
|
:raises ResponseWithEntity: If the response code is 205 but has an
|
||||||
|
entity
|
||||||
|
"""
|
||||||
if (resp.status in set((204, 205, 304)) or resp.status < 200 or
|
if (resp.status in set((204, 205, 304)) or resp.status < 200 or
|
||||||
method.upper() == 'HEAD') and resp_body:
|
method.upper() == 'HEAD') and resp_body:
|
||||||
raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
|
raise exceptions.ResponseWithNonEmptyBody(status=resp.status)
|
||||||
@@ -343,6 +525,22 @@ class RestClient(object):
|
|||||||
return resp, resp_body
|
return resp, resp_body
|
||||||
|
|
||||||
def raw_request(self, url, method, headers=None, body=None):
|
def raw_request(self, url, method, headers=None, body=None):
|
||||||
|
"""Send a raw HTTP request without the keystone catalog or auth
|
||||||
|
|
||||||
|
This method sends a HTTP request in the same manner as the request()
|
||||||
|
method, however it does so without using keystone auth or the catalog
|
||||||
|
to determine the base url. Additionally no response handling is done
|
||||||
|
the results from the request are just returned.
|
||||||
|
|
||||||
|
:param str url: Full url to send the request
|
||||||
|
:param str method: The HTTP verb to use for the request
|
||||||
|
:param str headers: Headers to use for the request if none are specifed
|
||||||
|
the headers
|
||||||
|
:param str body: Body to to send with the request
|
||||||
|
:rtype: tuple
|
||||||
|
:return: a tuple with the first entry containing the response headers
|
||||||
|
and the second the response body
|
||||||
|
"""
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers = self.get_headers()
|
headers = self.get_headers()
|
||||||
return self.http_obj.request(url, method,
|
return self.http_obj.request(url, method,
|
||||||
@@ -350,6 +548,55 @@ class RestClient(object):
|
|||||||
|
|
||||||
def request(self, method, url, extra_headers=False, headers=None,
|
def request(self, method, url, extra_headers=False, headers=None,
|
||||||
body=None):
|
body=None):
|
||||||
|
"""Send a HTTP request with keystone auth and using the catalog
|
||||||
|
|
||||||
|
This method will send an HTTP request using keystone auth in the
|
||||||
|
headers and the catalog to determine the endpoint to use for the
|
||||||
|
baseurl to send the request to. Additionally
|
||||||
|
|
||||||
|
When a response is recieved it will check it to see if an error
|
||||||
|
response was recieved. If it was an exception will be raised to enable
|
||||||
|
it to be handled quickly.
|
||||||
|
|
||||||
|
This method will also handle rate-limiting, if a 413 response code is
|
||||||
|
recieved it will retry the request after waiting the 'retry-after'
|
||||||
|
duration from the header.
|
||||||
|
|
||||||
|
:param str url: Relative url to send the request to
|
||||||
|
:param str method: The HTTP verb to use for the request
|
||||||
|
:param dict extra_headers: If specified without the headers kwarg the
|
||||||
|
headers sent with the request will be the
|
||||||
|
combination from the get_headers() method
|
||||||
|
and this kwarg
|
||||||
|
:param dict headers: Headers to use for the request if none are
|
||||||
|
specifed the headers returned from the
|
||||||
|
get_headers() method are used. If the request
|
||||||
|
explicitly requires no headers use an empty dict.
|
||||||
|
:param str body: Body to to send with the request
|
||||||
|
:rtype: tuple
|
||||||
|
:return: a tuple with the first entry containing the response headers
|
||||||
|
and the second the response body
|
||||||
|
:raises InvalidContentType: If the content-type of the response isn't
|
||||||
|
an expect type or a 415 response code is
|
||||||
|
recieved
|
||||||
|
:raises Unauthorized: If a 401 response code is recieved
|
||||||
|
:raises Forbidden: If a 403 response code is recieved
|
||||||
|
:raises NotFound: If a 404 response code is recieved
|
||||||
|
:raises BadRequest: If a 400 response code is recieved
|
||||||
|
:raises Conflict: If a 409 response code is recieved
|
||||||
|
:raies Overlimit: If a 413 response code is recieved and over_limit is
|
||||||
|
not in the response body
|
||||||
|
:raises RateLimitExceeded: If a 413 response code is recieved and
|
||||||
|
over_limit is in the response body
|
||||||
|
:raises UnprocessableEntity: If a 422 response code is recieved
|
||||||
|
:raises InvalidHTTPResponseBody: The response body wasn't valid JSON
|
||||||
|
and couldn't be parsed
|
||||||
|
:raises NotImplemented: If a 501 response code is recieved
|
||||||
|
:raises ServerFault: If a 500 response code is recieved
|
||||||
|
:raises UnexpectedResponseCode: If a response code above 400 is
|
||||||
|
recieved and it doesn't fall into any
|
||||||
|
of the handled checks
|
||||||
|
"""
|
||||||
# if extra_headers is True
|
# if extra_headers is True
|
||||||
# default headers would be added to headers
|
# default headers would be added to headers
|
||||||
retry = 0
|
retry = 0
|
||||||
@@ -499,7 +746,16 @@ class RestClient(object):
|
|||||||
return 'exceed' in over_limit.get('message', 'blabla')
|
return 'exceed' in over_limit.get('message', 'blabla')
|
||||||
|
|
||||||
def wait_for_resource_deletion(self, id):
|
def wait_for_resource_deletion(self, id):
|
||||||
"""Waits for a resource to be deleted."""
|
"""Waits for a resource to be deleted
|
||||||
|
|
||||||
|
This method will loop over is_resource_deleted until either
|
||||||
|
is_resource_deleted returns True or the build timeout is reached. This
|
||||||
|
depends on is_resource_deleted being implemented
|
||||||
|
|
||||||
|
:param str id: The id of the resource to check
|
||||||
|
:raises TimeoutException: If the build_timeout has elapsed and the
|
||||||
|
resource still hasn't been deleted
|
||||||
|
"""
|
||||||
start_time = int(time.time())
|
start_time = int(time.time())
|
||||||
while True:
|
while True:
|
||||||
if self.is_resource_deleted(id):
|
if self.is_resource_deleted(id):
|
||||||
|
Reference in New Issue
Block a user