vmware-nsx/vmware_nsx/nsxlib/v3/client.py
Boden R a03cec98da NSX v3 API client error propagation
Currently the NSX v3 REST API client masks the
backend NSX API error upon and invalid response.
This results in a very generic error message to consumers
(including the CLI) when a backend error happens.
This patch exposes the backend error message if
possible which provides more details of the issue
to consumers.

Change-Id: I215352d649b6579d6075cb104a7d311a7f1ffa66
2015-10-22 09:53:32 -06:00

243 lines
8.5 KiB
Python

# Copyright 2015 VMware, Inc.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import requests
from neutron.i18n import _LW, _
from oslo_config import cfg
from oslo_log import log
from oslo_serialization import jsonutils
from vmware_nsx.common import exceptions as nsx_exc
LOG = log.getLogger(__name__)
ERRORS = {requests.codes.NOT_FOUND: nsx_exc.ResourceNotFound,
requests.codes.PRECONDITION_FAILED: nsx_exc.StaleRevision}
class RESTClient(object):
_VERB_RESP_CODES = {
'get': [requests.codes.ok],
'post': [requests.codes.created, requests.codes.ok],
'put': [requests.codes.ok],
'delete': [requests.codes.ok]
}
def __init__(self, host_ip=None, user_name=None,
password=None, insecure=None,
url_prefix=None, default_headers=None,
cert_file=None):
self._host_ip = host_ip
self._user_name = user_name
self._password = password
self._insecure = insecure if insecure is not None else False
self._url_prefix = url_prefix or ""
self._default_headers = default_headers or {}
self._cert_file = cert_file
self._session = requests.Session()
self._session.auth = (self._user_name, self._password)
if not insecure and self._cert_file:
self._session.cert = self._cert_file
def new_client_for(self, *uri_segments):
uri = "%s/%s" % (self._url_prefix, '/'.join(uri_segments))
uri = uri.replace('//', '/')
return self.__class__(
host_ip=self._host_ip, user_name=self._user_name,
password=self._password, insecure=self._insecure,
url_prefix=uri,
default_headers=self._default_headers,
cert_file=self._cert_file)
@property
def validate_certificate(self):
return not self._insecure
def list(self, headers=None):
return self.url_list('')
def get(self, uuid, headers=None):
return self.url_get(uuid, headers=headers)
def delete(self, uuid, headers=None):
return self.url_delete(uuid, headers=headers)
def update(self, uuid, body=None, headers=None):
return self.url_put(uuid, body, headers=headers)
def create(self, body=None, headers=None):
return self.url_post('', body, headers=headers)
def url_list(self, url, headers=None):
return self.url_get(url, headers=headers)
def url_get(self, url, headers=None):
return self._rest_call(url, method='GET', headers=headers)
def url_delete(self, url, headers=None):
return self._rest_call(url, method='DELETE', headers=headers)
def url_put(self, url, body, headers=None):
return self._rest_call(url, method='PUT', body=body, headers=headers)
def url_post(self, url, body, headers=None):
return self._rest_call(url, method='POST', body=body, headers=headers)
def _validate_result(self, result, expected, operation):
if result.status_code not in expected:
result_msg = result.json() if result.content else ''
LOG.warning(_LW("The HTTP request returned error code "
"%(result)d, whereas %(expected)s response "
"codes were expected. Response body %(body)s"),
{'result': result.status_code,
'expected': '/'.join([str(code)
for code in expected]),
'body': result_msg})
manager_error = ERRORS.get(
result.status_code, nsx_exc.ManagerError)
if type(result_msg) is dict:
result_msg = result_msg.get('error_message', result_msg)
raise manager_error(
manager=self._host_ip,
operation=operation,
details=result_msg)
@classmethod
def merge_headers(cls, *headers):
merged = {}
for header in headers:
if header:
merged.update(header)
return merged
def _build_url(self, uri):
uri = ("/%s/%s" % (self._url_prefix, uri)).replace('//', '/')
return ("https://%s%s" % (self._host_ip, uri)).strip('/')
def _rest_call(self, url, method='GET', body=None, headers=None):
request_headers = headers.copy() if headers else {}
request_headers.update(self._default_headers)
request_url = self._build_url(url)
do_request = getattr(self._session, method.lower())
LOG.debug("REST call: %s %s\nHeaders: %s\nBody: %s",
method, request_url, request_headers, body)
result = do_request(
request_url,
verify=self.validate_certificate,
data=body,
headers=request_headers,
cert=self._cert_file)
self._validate_result(
result, RESTClient._VERB_RESP_CODES[method.lower()],
_("%(verb)s %(url)s") % {'verb': method, 'url': request_url})
return result
class JSONRESTClient(RESTClient):
_DEFAULT_HEADERS = {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
def __init__(self, host_ip=None, user_name=None,
password=None, insecure=None,
url_prefix=None, default_headers=None,
cert_file=None):
super(JSONRESTClient, self).__init__(
host_ip=host_ip, user_name=user_name,
password=password, insecure=insecure,
url_prefix=url_prefix,
default_headers=RESTClient.merge_headers(
JSONRESTClient._DEFAULT_HEADERS, default_headers),
cert_file=cert_file)
def _rest_call(self, *args, **kwargs):
if kwargs.get('body') is not None:
kwargs['body'] = jsonutils.dumps(kwargs['body'], sort_keys=True)
result = super(JSONRESTClient, self)._rest_call(*args, **kwargs)
return result.json() if result.content else result
class NSX3Client(JSONRESTClient):
_NSX_V1_API_PREFIX = '/api/v1/'
def __init__(self, host_ip=None, user_name=None,
password=None, insecure=None,
url_prefix=None, default_headers=None,
cert_file=None):
url_prefix = url_prefix or NSX3Client._NSX_V1_API_PREFIX
if (url_prefix and not url_prefix.startswith(
NSX3Client._NSX_V1_API_PREFIX)):
url_prefix = "%s/%s" % (NSX3Client._NSX_V1_API_PREFIX,
url_prefix or '')
host_ip = host_ip or cfg.CONF.nsx_v3.nsx_manager
user_name = user_name or cfg.CONF.nsx_v3.nsx_user
password = password or cfg.CONF.nsx_v3.nsx_password
cert_file = cert_file or cfg.CONF.nsx_v3.ca_file
insecure = (insecure if insecure is not None
else cfg.CONF.nsx_v3.insecure)
super(NSX3Client, self).__init__(
host_ip=host_ip, user_name=user_name,
password=password, insecure=insecure,
url_prefix=url_prefix,
default_headers=default_headers,
cert_file=cert_file)
# NOTE(boden): tmp until all refs use client class
def _get_client(client, *args, **kwargs):
return client or NSX3Client(*args, **kwargs)
# NOTE(shihli): tmp until all refs use client class
def _get_manager_ip(client=None):
# NOTE: In future this may return the IP address from a pool
return (client._host_ip if client is not None
else cfg.CONF.nsx_v3.nsx_manager)
# NOTE(boden): tmp until all refs use client class
def get_resource(resource, client=None):
return _get_client(client).get(resource)
# NOTE(boden): tmp until all refs use client class
def create_resource(resource, data, client=None):
return _get_client(client).url_post(resource, body=data)
# NOTE(boden): tmp until all refs use client class
def update_resource(resource, data, client=None):
return _get_client(client).update(resource, body=data)
# NOTE(boden): tmp until all refs use client class
def delete_resource(resource, client=None):
return _get_client(client).delete(resource)