
This patch adds multi-manager support to the NSX v3 plugin thereby enabling the ability to specify multiple NSX managers for the v3 plugin. This implementation supports the same basic features as the MH multi-manager support does including; timeouts, retries, keep-alive, etc.. The approach in a nutshell is to introduce a "proxy" class which looks like a requests or requests.Session object and can be used in place of requests in the NSX REST API client. Under the covers this class handles management of endpoint selection and connectivity. Also note that with this patch your devstack local rc / conf no longer needs to specify NSX_CONTROLLERS when using the v3 plugin. Instead a comma list of managers is supported on the NSX_MANAGERS devstack var. Closes-Bug: #1524046 Change-Id: I433a4b9ea73de0680d64d86e2f826c092adfba87
228 lines
7.4 KiB
Python
228 lines
7.4 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
|
|
import urlparse
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log
|
|
from oslo_serialization import jsonutils
|
|
from vmware_nsx._i18n import _, _LW
|
|
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, connection, url_prefix=None,
|
|
default_headers=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)
|
|
|
|
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=_get_nsx_managers_from_conf(),
|
|
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):
|
|
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 _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._conn, method.lower())
|
|
|
|
LOG.debug("REST call: %s %s\nHeaders: %s\nBody: %s",
|
|
method, request_url, request_headers, body)
|
|
|
|
result = do_request(
|
|
request_url,
|
|
data=body,
|
|
headers=request_headers)
|
|
|
|
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, connection, url_prefix=None,
|
|
default_headers=None):
|
|
|
|
super(JSONRESTClient, self).__init__(
|
|
connection,
|
|
url_prefix=url_prefix,
|
|
default_headers=RESTClient.merge_headers(
|
|
JSONRESTClient._DEFAULT_HEADERS, default_headers))
|
|
|
|
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, connection, url_prefix=None,
|
|
default_headers=None):
|
|
|
|
url_prefix = url_prefix or NSX3Client._NSX_V1_API_PREFIX
|
|
if url_prefix and NSX3Client._NSX_V1_API_PREFIX not in url_prefix:
|
|
if url_prefix.startswith('http'):
|
|
url_prefix += '/' + NSX3Client._NSX_V1_API_PREFIX
|
|
else:
|
|
url_prefix = "%s/%s" % (NSX3Client._NSX_V1_API_PREFIX,
|
|
url_prefix or '')
|
|
|
|
super(NSX3Client, self).__init__(
|
|
connection, url_prefix=url_prefix,
|
|
default_headers=default_headers)
|
|
|
|
|
|
# TODO(boden): remove mod level fns and vars below
|
|
_DEFAULT_API_CLUSTER = None
|
|
|
|
|
|
def _get_default_api_cluster():
|
|
global _DEFAULT_API_CLUSTER
|
|
if _DEFAULT_API_CLUSTER is None:
|
|
# removes circular ref between client / cluster
|
|
import vmware_nsx.nsxlib.v3.cluster as nsx_cluster
|
|
_DEFAULT_API_CLUSTER = nsx_cluster.NSXClusteredAPI()
|
|
return _DEFAULT_API_CLUSTER
|
|
|
|
|
|
def _set_default_api_cluster(cluster):
|
|
global _DEFAULT_API_CLUSTER
|
|
old = _DEFAULT_API_CLUSTER
|
|
_DEFAULT_API_CLUSTER = cluster
|
|
return old
|
|
|
|
|
|
def _get_client(client):
|
|
return client or NSX3Client(_get_default_api_cluster())
|
|
|
|
|
|
# NOTE(shihli): tmp until all refs use client class
|
|
def _get_nsx_managers_from_conf():
|
|
return cfg.CONF.nsx_v3.nsx_managers
|
|
|
|
|
|
def get_resource(resource, client=None):
|
|
return _get_client(client).get(resource)
|
|
|
|
|
|
def create_resource(resource, data, client=None):
|
|
return _get_client(client).url_post(resource, body=data)
|
|
|
|
|
|
def update_resource(resource, data, client=None):
|
|
return _get_client(client).update(resource, body=data)
|
|
|
|
|
|
def delete_resource(resource, client=None):
|
|
return _get_client(client).delete(resource)
|