Files
deb-python-dcos/dcos/rpcclient.py
2016-12-02 12:50:46 -08:00

169 lines
5.6 KiB
Python

import json
import jsonschema
import pkg_resources
from six.moves import urllib
from dcos import http, util
from dcos.errors import DCOSException, DCOSHTTPException
logger = util.get_logger(__name__)
def create_client(url, timeout):
return RpcClient(url, timeout)
def load_error_json_schema():
"""Reads and parses Marathon error response JSON schema from file
:returns: the parsed JSON schema
:rtype: dict
"""
schema_path = 'data/marathon/error.schema.json'
schema_bytes = pkg_resources.resource_string('dcos', schema_path)
return json.loads(schema_bytes.decode('utf-8'))
class RpcClient(object):
"""Convenience class for making requests against a common RPC API.
For example, it ensures the same base URL is used for all requests. This
class is also useful as a target for mocks in unit tests, because it
presents a minimal, application-focused interface.
:param base_url: the URL prefix to use for all requests
:type base_url: str
:param timeout: number of seconds to wait for a response
:type timeout: float
"""
def __init__(self, base_url, timeout=http.DEFAULT_TIMEOUT):
if not base_url.endswith('/'):
base_url += '/'
self._base_url = base_url
self._timeout = timeout
ERROR_JSON_VALIDATOR = jsonschema.Draft4Validator(load_error_json_schema())
RESOURCE_TYPES = ['app', 'group', 'pod']
@classmethod
def response_error_message(cls, status_code, reason, request_method,
request_url, json_body):
"""Renders a human-readable error message from the given response data.
:param status_code: the integer status code from an HTTP response
:type status_code: int
:param reason: human-readable text representation of the status code
:type reason: str
:param request_method: the HTTP method used for the request
:type request_method: str
:param request_url: the URL the request was sent to
:type request_url: str
:param json_body: the response body, parsed as JSON, or None if
parsing failed
:type json_body: dict | list | str | int | bool | None
:return: the rendered error message
:rtype: str
"""
if status_code == 400:
template = 'Error on request [{} {}]: HTTP 400: {}{}'
suffix = ''
if json_body is not None:
json_str = json.dumps(json_body, indent=2, sort_keys=True)
suffix = ':\n' + json_str
return template.format(request_method, request_url, reason, suffix)
if status_code == 409:
path = urllib.parse.urlparse(request_url).path
path_name = (name for name in cls.RESOURCE_TYPES if name in path)
resource_name = next(path_name, 'resource')
template = ('Changes blocked: '
'deployment already in progress for {}.')
return template.format(resource_name)
if json_body is None:
template = 'Error decoding response from [{}]: HTTP {}: {}'
return template.format(request_url, status_code, reason)
if not cls.ERROR_JSON_VALIDATOR.is_valid(json_body):
log_str = 'Server did not return a message: %s'
logger.error(log_str, json_body)
return _default_dcos_error()
message = json_body.get('message')
if message is None:
message = '\n'.join(err['error'] for err in json_body['errors'])
return _default_dcos_error(message)
return 'Error: {}'.format(message)
def http_req(self, method_fn, path, *args, **kwargs):
"""Make an HTTP request, and raise a DCOS-specific exception for
HTTP error codes.
:param method_fn: function to call that invokes a specific HTTP method
:type method_fn: function
:param path: the endpoint path to append to this object's base URL
:type path: str
:param args: additional args to pass to `method_fn`
:type args: [object]
:param kwargs: kwargs to pass to `method_fn`
:type kwargs: dict
:returns: `method_fn` return value
:rtype: requests.Response
"""
url = self._base_url + path.lstrip('/')
if 'timeout' not in kwargs:
kwargs['timeout'] = self._timeout
try:
return method_fn(url, *args, **kwargs)
except DCOSHTTPException as e:
text = _get_response_text(e.response)
logger.error('DCOS Error: %s\n%s',
e.response.reason, text)
try:
json_body = e.response.json()
except:
logger.exception(
'Unable to decode response body as a JSON value: %r',
e.response)
json_body = None
message = RpcClient.response_error_message(
status_code=e.response.status_code,
reason=e.response.reason,
request_method=e.response.request.method,
request_url=e.response.request.url,
json_body=json_body)
raise DCOSException(message)
def _get_response_text(response):
try:
return response.text
except:
return ''
def _default_dcos_error(message=""):
"""
:param message: additional message
:type message: str
:returns: dcos specific error message
:rtype: str
"""
return ("Service likely misconfigured. Please check your proxy or "
"Service URL settings. See dcos config --help. {}").format(
message)