You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
391 lines
16 KiB
391 lines
16 KiB
# 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 re |
|
import time |
|
from urllib import parse as urlparse |
|
|
|
from oslo_log import log |
|
from oslo_serialization import jsonutils |
|
import requests |
|
|
|
from vmware_nsxlib._i18n import _ |
|
from vmware_nsxlib.v3 import exceptions |
|
from vmware_nsxlib.v3 import utils |
|
|
|
LOG = log.getLogger(__name__) |
|
|
|
NULL_CURSOR_PREFIX = '0000' |
|
|
|
|
|
def get_http_error_details(response): |
|
msg = response.json() if response.content else '' |
|
error_code = None |
|
related_error_codes = [] |
|
related_status_codes = [] |
|
|
|
if isinstance(msg, dict) and 'error_message' in msg: |
|
error_code = msg.get('error_code') |
|
related_errors = [error['error_message'] for error in |
|
msg.get('related_errors', [])] |
|
related_error_codes = [str(error['error_code']) for error in |
|
msg.get('related_errors', []) if |
|
error.get('error_code')] |
|
related_status_codes = [getattr(requests.codes, error['httpStatus']) |
|
for error in msg.get('related_errors', []) if |
|
error.get('httpStatus')] |
|
|
|
msg = msg['error_message'] |
|
if related_errors: |
|
msg += " relatedErrors: %s" % ' '.join(related_errors) |
|
|
|
return {'status_code': response.status_code, |
|
'error_code': error_code, |
|
'related_error_codes': related_error_codes, |
|
'related_status_codes': related_status_codes, |
|
'details': msg} |
|
|
|
|
|
def init_http_exception_from_response(response): |
|
if response is None or response: |
|
# The response object has a __bool__ method that return True for |
|
# status code under 400. In that case there is no need for exception |
|
return None |
|
|
|
error_details = get_http_error_details(response) |
|
if not error_details['error_code']: |
|
return None |
|
|
|
error = http_error_to_exception(error_details['status_code'], |
|
error_details['error_code'], |
|
error_details['related_error_codes']) |
|
|
|
return error(manager='', **error_details) |
|
|
|
|
|
def http_error_to_exception(status_code, error_code, related_error_codes=None): |
|
errors = { |
|
requests.codes.NOT_FOUND: |
|
{'202': exceptions.BackendResourceNotFound, |
|
'500090': exceptions.StaleRevision, |
|
'default': exceptions.ResourceNotFound}, |
|
requests.codes.BAD_REQUEST: |
|
{'60508': exceptions.NsxIndexingInProgress, |
|
'60514': exceptions.NsxSearchTimeout, |
|
'60515': exceptions.NsxSearchOutOfSync, |
|
'8327': exceptions.NsxOverlapVlan, |
|
'500045': exceptions.NsxPendingDelete, |
|
'500030': exceptions.ResourceInUse, |
|
'500087': exceptions.StaleRevision, |
|
'500105': exceptions.NsxOverlapAddresses, |
|
'500232': exceptions.StaleRevision, # Missing dependent objects |
|
'503040': exceptions.NsxSegemntWithVM, |
|
'100148': exceptions.StaleRevision, |
|
'500012': exceptions.NsxInvalidPath}, |
|
requests.codes.CONFLICT: exceptions.StaleRevision, |
|
requests.codes.PRECONDITION_FAILED: exceptions.StaleRevision, |
|
requests.codes.INTERNAL_SERVER_ERROR: |
|
{'98': exceptions.CannotConnectToServer, |
|
'99': exceptions.ClientCertificateNotTrusted, |
|
'607': exceptions.APITransactionAborted, |
|
'610': exceptions.APITransactionAborted}, |
|
requests.codes.FORBIDDEN: |
|
{'98': exceptions.BadXSRFToken, |
|
'403': exceptions.InvalidCredentials, |
|
'505': exceptions.InvalidLicense}, |
|
requests.codes.TOO_MANY_REQUESTS: exceptions.TooManyRequests, |
|
requests.codes.SERVICE_UNAVAILABLE: exceptions.ServiceUnavailable} |
|
|
|
if status_code in errors: |
|
if isinstance(errors[status_code], dict): |
|
# choose based on error code |
|
if error_code and str(error_code) in errors[status_code]: |
|
return errors[status_code][str(error_code)] |
|
# try the related errors |
|
if related_error_codes: |
|
for err in related_error_codes: |
|
if err and str(err) in errors[status_code]: |
|
return errors[status_code][str(err)] |
|
|
|
if 'default' in errors[status_code]: |
|
return errors[status_code]['default'] |
|
else: |
|
return errors[status_code] |
|
|
|
# default exception |
|
return exceptions.ManagerError |
|
|
|
|
|
class RESTClient(object): |
|
|
|
_VERB_RESP_CODES = { |
|
'get': [requests.codes.ok], |
|
'post': [requests.codes.created, requests.codes.ok], |
|
'put': [requests.codes.created, requests.codes.ok], |
|
'patch': [requests.codes.created, requests.codes.ok], |
|
'delete': [requests.codes.ok] |
|
} |
|
|
|
def __init__(self, connection, url_prefix=None, |
|
default_headers=None, |
|
client_obj=None): |
|
self._conn = connection |
|
self._url_prefix = url_prefix or "" |
|
self._default_headers = default_headers or {} |
|
|
|
def new_client_for(self, *uri_segments): |
|
uri = self._build_url('/'.join(uri_segments)) |
|
|
|
return self.__class__( |
|
self._conn, |
|
url_prefix=uri, |
|
default_headers=self._default_headers, |
|
client_obj=self) |
|
|
|
def list(self, resource='', headers=None, silent=False): |
|
return self.url_list(resource, headers=headers, silent=silent) |
|
|
|
def get(self, uuid, headers=None, silent=False, with_retries=False): |
|
return self.url_get(uuid, headers=headers, silent=silent, |
|
with_retries=with_retries) |
|
|
|
def delete(self, uuid, headers=None, expected_results=None): |
|
return self.url_delete(uuid, headers=headers, |
|
expected_results=expected_results) |
|
|
|
def update(self, uuid, body=None, headers=None, expected_results=None): |
|
return self.url_put(uuid, body, headers=headers, |
|
expected_results=expected_results) |
|
|
|
def create(self, resource='', body=None, headers=None, |
|
expected_results=None): |
|
return self.url_post(resource, body, headers=headers, |
|
expected_results=expected_results) |
|
|
|
def patch(self, resource='', body=None, headers=None): |
|
return self.url_patch(resource, body, headers=headers) |
|
|
|
def url_list(self, url, headers=None, silent=False): |
|
concatenate_response = self.url_get(url, headers=headers, |
|
silent=silent) |
|
cursor = concatenate_response.get('cursor', NULL_CURSOR_PREFIX) |
|
op = '&' if urlparse.urlparse(url).query else '?' |
|
url += op + 'cursor=' |
|
|
|
while cursor and not cursor.startswith(NULL_CURSOR_PREFIX): |
|
page = self.url_get(url + cursor, headers=headers, silent=silent) |
|
concatenate_response['results'].extend(page.get('results', [])) |
|
cursor = page.get('cursor', NULL_CURSOR_PREFIX) |
|
return concatenate_response |
|
|
|
def url_get(self, url, headers=None, silent=False, with_retries=False): |
|
return self._rest_call(url, method='GET', headers=headers, |
|
silent=silent, with_retries=with_retries) |
|
|
|
def url_delete(self, url, headers=None, expected_results=None): |
|
return self._rest_call(url, method='DELETE', headers=headers, |
|
expected_results=expected_results) |
|
|
|
def url_put(self, url, body, headers=None, expected_results=None): |
|
return self._rest_call(url, method='PUT', body=body, headers=headers, |
|
expected_results=expected_results) |
|
|
|
def url_post(self, url, body, headers=None, expected_results=None): |
|
return self._rest_call(url, method='POST', body=body, headers=headers, |
|
expected_results=expected_results) |
|
|
|
def url_patch(self, url, body, headers=None): |
|
return self._rest_call(url, method='PATCH', body=body, headers=headers) |
|
|
|
def _raise_error(self, operation, status_code, details, |
|
error_code=None, related_error_codes=None, |
|
related_status_codes=None): |
|
error = http_error_to_exception(status_code, error_code, |
|
related_error_codes) |
|
raise error(manager='', operation=operation, details=details, |
|
error_code=error_code, |
|
related_error_codes=related_error_codes, |
|
status_code=status_code, |
|
related_status_codes=related_status_codes) |
|
|
|
def _validate_result(self, result, expected, operation, silent=False): |
|
if result.status_code not in expected: |
|
result_msg = result.json() if result.content else '' |
|
if not silent: |
|
LOG.warning("The HTTP request returned error code " |
|
"%(result)s, 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}) |
|
|
|
error_details = get_http_error_details(result) |
|
|
|
self._raise_error(operation, **error_details) |
|
|
|
@classmethod |
|
def merge_headers(cls, *headers): |
|
merged = {} |
|
for header in headers: |
|
if header: |
|
merged.update(header) |
|
return merged |
|
|
|
def _build_url(self, uri): |
|
prefix = urlparse.urlparse(self._url_prefix) |
|
uri = ("/%s/%s" % (prefix.path, uri)).replace('//', '/').strip('/') |
|
if prefix.netloc: |
|
uri = "%s/%s" % (prefix.netloc, uri) |
|
if prefix.scheme: |
|
uri = "%s://%s" % (prefix.scheme, uri) |
|
return uri |
|
|
|
def _mask_password(self, json): |
|
'''Mask password value in json format''' |
|
if not json: |
|
return json |
|
|
|
pattern = r'\"password\": [^,}]*' |
|
return re.sub(pattern, '"password": "********"', json) |
|
|
|
def _rest_call(self, url, method='GET', body=None, headers=None, |
|
silent=False, expected_results=None, **kwargs): |
|
request_headers = headers.copy() if headers else {} |
|
request_headers.update(self._default_headers) |
|
|
|
if utils.INJECT_HEADERS_CALLBACK: |
|
inject_headers = utils.INJECT_HEADERS_CALLBACK() |
|
request_headers.update(inject_headers) |
|
|
|
request_url = self._build_url(url) |
|
do_request = getattr(self._conn, method.lower()) |
|
if silent: |
|
self._conn.set_silent(True) |
|
ts = time.time() |
|
result = do_request( |
|
request_url, |
|
data=body, |
|
headers=request_headers) |
|
te = time.time() |
|
if silent: |
|
self._conn.set_silent(False) |
|
|
|
if not silent: |
|
LOG.debug("[%x] REST call: %s %s. Headers: %s. Body: %s. " |
|
"Response: %s. Took %2.4f", |
|
id(self._conn), method, request_url, |
|
utils.censor_headers(request_headers), |
|
self._mask_password(body), |
|
result.json() if result.content else '', |
|
te - ts) |
|
|
|
if not expected_results: |
|
expected_results = RESTClient._VERB_RESP_CODES[method.lower()] |
|
self._validate_result( |
|
result, expected_results, |
|
_("%(verb)s %(url)s") % {'verb': method, 'url': request_url}, |
|
silent=silent) |
|
return result |
|
|
|
|
|
class JSONRESTClient(RESTClient): |
|
|
|
_DEFAULT_HEADERS = { |
|
'Accept': 'application/json', |
|
'Content-Type': 'application/json' |
|
} |
|
|
|
def __init__(self, connection, url_prefix=None, |
|
default_headers=None, |
|
client_obj=None): |
|
|
|
super(JSONRESTClient, self).__init__( |
|
connection, |
|
url_prefix=url_prefix, |
|
default_headers=RESTClient.merge_headers( |
|
JSONRESTClient._DEFAULT_HEADERS, default_headers), |
|
client_obj=None) |
|
|
|
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/' |
|
NSX_POLICY_V1_API_PREFIX = 'policy/api/v1/' |
|
|
|
# NOTE: For user-facing client, NsxClusteredAPI instance |
|
# will be passed as connection parameter below, thus all |
|
# requests on this client will pass via cluster code to |
|
# determine endpoint |
|
# For validation client, TimeoutSession with specific |
|
# endpoint parameters will be passed as connection. |
|
def __init__(self, connection, url_prefix=None, |
|
default_headers=None, |
|
nsx_api_managers=None, |
|
max_attempts=utils.DEFAULT_MAX_ATTEMPTS, |
|
rate_limit_retry=True, |
|
client_obj=None, |
|
url_path_base=NSX_V1_API_PREFIX): |
|
|
|
# If the client obj is defined - copy configuration from it |
|
if client_obj: |
|
self.nsx_api_managers = client_obj.nsx_api_managers or [] |
|
self.max_attempts = client_obj.max_attempts |
|
self.rate_limit_retry = client_obj.rate_limit_retry |
|
else: |
|
self.nsx_api_managers = nsx_api_managers or [] |
|
self.max_attempts = max_attempts |
|
self.rate_limit_retry = rate_limit_retry |
|
|
|
url_prefix = url_prefix or url_path_base |
|
if url_prefix and url_path_base not in url_prefix: |
|
if url_prefix.startswith('http'): |
|
url_prefix += '/' + url_path_base |
|
else: |
|
url_prefix = "%s/%s" % (url_path_base, |
|
url_prefix or '') |
|
|
|
super(NSX3Client, self).__init__( |
|
connection, url_prefix=url_prefix, |
|
default_headers=default_headers, |
|
client_obj=client_obj) |
|
|
|
def _raise_error(self, operation, status_code, details, |
|
error_code=None, related_error_codes=None, |
|
related_status_codes=None): |
|
"""Override the Rest client errors to add the manager IPs""" |
|
error = http_error_to_exception(status_code, error_code, |
|
related_error_codes) |
|
raise error(manager=self.nsx_api_managers, |
|
operation=operation, |
|
details=details, |
|
error_code=error_code, |
|
related_error_codes=related_error_codes, |
|
related_status_codes=related_status_codes, |
|
status_code=status_code) |
|
|
|
def _rest_call(self, url, **kwargs): |
|
if 'with_retries' in kwargs and kwargs['with_retries']: |
|
LOG.warning("with_retries setting is deprecated and will be " |
|
"removed. Please use exceptions setting in nsxlib " |
|
"config instead") |
|
|
|
return super(NSX3Client, self)._rest_call(url, **kwargs)
|
|
|