diff --git a/vmware_nsx/nsxlib/v3/client.py b/vmware_nsx/nsxlib/v3/client.py index e960fcf09b..96834b2ff1 100644 --- a/vmware_nsx/nsxlib/v3/client.py +++ b/vmware_nsx/nsxlib/v3/client.py @@ -1,4 +1,4 @@ -# Copyright 2015 OpenStack Foundation +# Copyright 2015 VMware, Inc. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -12,14 +12,13 @@ # 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 -import requests -from requests import auth - -from neutron.i18n import _LW from vmware_nsx.common import exceptions as nsx_exc LOG = log.getLogger(__name__) @@ -28,88 +27,197 @@ ERRORS = {requests.codes.NOT_FOUND: nsx_exc.ResourceNotFound, requests.codes.PRECONDITION_FAILED: nsx_exc.StaleRevision} -def _get_manager_endpoint(): - manager = _get_manager_ip() - username = cfg.CONF.nsx_v3.nsx_user - password = cfg.CONF.nsx_v3.nsx_password - verify_cert = not cfg.CONF.nsx_v3.insecure - return "https://%s" % manager, username, password, verify_cert +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 _get_manager_ip(): - # NOTE: In future this may return the IP address from a pool - manager = cfg.CONF.nsx_v3.nsx_manager - return manager + 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 _validate_result(result, expected, operation): - if result.status_code not in expected: - if (result.status_code == requests.codes.bad): + 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: 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.json()}) - else: - LOG.warning(_LW("The HTTP request returned error code " - "%(result)d, whereas %(expected)s response " - "codes were expected."), - {'result': result.status_code, - 'expected': '/'.join([str(code) - for code in expected])}) - manager_ip = _get_manager_ip() + 'body': result.json() if result.content else ''}) - manager_error = ERRORS.get(result.status_code, nsx_exc.ManagerError) - raise manager_error(manager=manager_ip, operation=operation) + manager_error = ERRORS.get( + result.status_code, nsx_exc.ManagerError) + raise manager_error(manager=self._host_ip, operation=operation) + + @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 -def get_resource(resource, **params): - manager, user, password, verify = _get_manager_endpoint() - url = manager + "/api/v1/%s" % resource - headers = {'Accept': 'application/json'} - result = requests.get(url, auth=auth.HTTPBasicAuth(user, password), - verify=verify, headers=headers, - cert=cfg.CONF.nsx_v3.ca_file, - params=params) - _validate_result(result, [requests.codes.ok], - _("reading resource: %s") % resource) - return result.json() +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']) + 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_resource(resource): + return NSX3Client().get(resource) + + +# NOTE(boden): tmp until all refs use client class def create_resource(resource, data): - manager, user, password, verify = _get_manager_endpoint() - url = manager + "/api/v1/%s" % resource - headers = {'Content-Type': 'application/json', - 'Accept': 'application/json'} - result = requests.post(url, auth=auth.HTTPBasicAuth(user, password), - verify=verify, headers=headers, - data=jsonutils.dumps(data), - cert=cfg.CONF.nsx_v3.ca_file) - _validate_result(result, [requests.codes.created, requests.codes.ok], - _("creating resource at: %s") % resource) - return result.json() + return NSX3Client(url_prefix=resource).create(body=data) +# NOTE(boden): tmp until all refs use client class def update_resource(resource, data): - manager, user, password, verify = _get_manager_endpoint() - url = manager + "/api/v1/%s" % resource - headers = {'Content-Type': 'application/json', - 'Accept': 'application/json'} - result = requests.put(url, auth=auth.HTTPBasicAuth(user, password), - verify=verify, headers=headers, - data=jsonutils.dumps(data), - cert=cfg.CONF.nsx_v3.ca_file) - _validate_result(result, [requests.codes.ok], - _("updating resource: %s") % resource) - return result.json() + return NSX3Client().update(resource, body=data) +# NOTE(boden): tmp until all refs use client class def delete_resource(resource): - manager, user, password, verify = _get_manager_endpoint() - url = manager + "/api/v1/%s" % resource - result = requests.delete(url, auth=auth.HTTPBasicAuth(user, password), - verify=verify, cert=cfg.CONF.nsx_v3.ca_file) - _validate_result(result, [requests.codes.ok], - _("deleting resource: %s") % resource) + return NSX3Client().delete(resource) diff --git a/vmware_nsx/nsxlib/v3/resources.py b/vmware_nsx/nsxlib/v3/resources.py new file mode 100644 index 0000000000..50a751120e --- /dev/null +++ b/vmware_nsx/nsxlib/v3/resources.py @@ -0,0 +1,107 @@ +# 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 abc +import six + + +@six.add_metaclass(abc.ABCMeta) +class AbstractRESTResource(object): + + def __init__(self, rest_client, *args, **kwargs): + self._client = rest_client.new_client_for(self.uri_segment) + + @abc.abstractproperty + def uri_segment(self): + pass + + def list(self): + return self._client.list() + + def get(self, uuid): + return self._client.get(uuid) + + def delete(self, uuid): + return self._client.delete(uuid) + + @abc.abstractmethod + def create(self, *args, **kwargs): + pass + + @abc.abstractmethod + def update(self, uuid, *args, **kwargs): + pass + + def find_by_display_name(self, display_name): + found = [] + for resource in self.list()['results']: + if resource['display_name'] == display_name: + found.append(resource) + return found + + +class SwitchingProfileTypes(object): + IP_DISCOVERY = 'IpDiscoverySwitchingProfile' + PORT_MIRRORING = 'PortMirroringSwitchingProfile' + QOS = 'QosSwitchingProfile' + SPOOF_GUARD = 'SpoofGuardSwitchingProfile' + + +class WhiteListAddressTypes(object): + PORT = 'LPORT_BINDINGS' + SWITCH = 'LSWITCH_BINDINGS' + + +class SwitchingProfile(AbstractRESTResource): + + @property + def uri_segment(self): + return 'switching-profiles' + + def create(self, profile_type, display_name=None, + description=None, **api_args): + body = { + 'resource_type': profile_type, + 'display_name': display_name or '', + 'description': description or '' + } + body.update(api_args) + + return self._client.create(body=body) + + def update(self, uuid, profile_type, **api_args): + body = { + 'resource_type': profile_type + } + body.update(api_args) + + return self._client.update(uuid, body=body) + + def create_spoofguard_profile(self, display_name, + description, + whitelist_ports=False, + whitelist_switches=False, + tags=None): + whitelist_providers = [] + if whitelist_ports: + whitelist_providers.append(WhiteListAddressTypes.PORT) + if whitelist_switches: + whitelist_providers.append(WhiteListAddressTypes.SWITCH) + + return self.create(SwitchingProfileTypes.SPOOF_GUARD, + display_name=display_name, + description=description, + white_list_providers=whitelist_providers, + tags=tags or []) diff --git a/vmware_nsx/tests/unit/vmware/nsx_v3_mocks.py b/vmware_nsx/tests/unit/vmware/nsx_v3_mocks.py index b246caa5a5..4fa7db1a95 100644 --- a/vmware_nsx/tests/unit/vmware/nsx_v3_mocks.py +++ b/vmware_nsx/tests/unit/vmware/nsx_v3_mocks.py @@ -12,8 +12,7 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. - - +from oslo_serialization import jsonutils from oslo_utils import uuidutils from vmware_nsx.common import exceptions as nsx_exc @@ -419,3 +418,12 @@ class NsxV3Mock(object): def update_logical_router_advertisement(self, logical_router_id, **kwargs): # TODO(berlin): implement this latter. pass + + +class MockRequestsResponse(object): + def __init__(self, status_code, content=None): + self.status_code = status_code + self.content = content + + def json(self): + return jsonutils.loads(self.content) diff --git a/vmware_nsx/tests/unit/vmware/nsxlib/v3/test_client.py b/vmware_nsx/tests/unit/vmware/nsxlib/v3/test_client.py new file mode 100644 index 0000000000..155b50e7eb --- /dev/null +++ b/vmware_nsx/tests/unit/vmware/nsxlib/v3/test_client.py @@ -0,0 +1,369 @@ +# 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 mock + +import vmware_nsx.common.exceptions as exep +import vmware_nsx.tests.unit.vmware.nsx_v3_mocks as mocks + +from oslo_config import cfg +from oslo_log import log +from oslo_serialization import jsonutils +from vmware_nsx.nsxlib.v3 import client +from vmware_nsx.tests.unit.vmware.nsxlib.v3 import nsxlib_testcase + +LOG = log.getLogger(__name__) + +CLIENT_PKG = 'vmware_nsx.nsxlib.v3.client' + + +def assert_session_call(mock_call, url, verify, data, headers, cert): + mock_call.assert_called_once_with( + url, verify=verify, data=data, headers=headers, cert=cert) + + +class BaseClientTestCase(nsxlib_testcase.NsxLibTestCase): + nsx_manager = '1.2.3.4' + nsx_user = 'testuser' + nsx_password = 'pass123' + ca_file = '/path/to/ca.pem' + insecure = True + + def setUp(self): + cfg.CONF.set_override( + 'nsx_manager', BaseClientTestCase.nsx_manager, 'nsx_v3') + cfg.CONF.set_override( + 'nsx_user', BaseClientTestCase.nsx_user, 'nsx_v3') + cfg.CONF.set_override( + 'nsx_password', BaseClientTestCase.nsx_password, 'nsx_v3') + cfg.CONF.set_override( + 'ca_file', BaseClientTestCase.ca_file, 'nsx_v3') + cfg.CONF.set_override( + 'insecure', BaseClientTestCase.insecure, 'nsx_v3') + super(BaseClientTestCase, self).setUp() + + def new_client( + self, clazz, host_ip=nsx_manager, + user_name=nsx_user, password=nsx_password, + insecure=insecure, url_prefix=None, + default_headers=None, cert_file=ca_file): + + return clazz(host_ip=host_ip, user_name=user_name, + password=password, insecure=insecure, + url_prefix=url_prefix, default_headers=default_headers, + cert_file=cert_file) + + +class NsxV3RESTClientTestCase(BaseClientTestCase): + + def test_client_conf_init(self): + api = self.new_client(client.RESTClient) + self.assertEqual(( + BaseClientTestCase.nsx_user, BaseClientTestCase.nsx_password), + api._session.auth) + self.assertEqual(BaseClientTestCase.nsx_manager, api._host_ip) + self.assertEqual(BaseClientTestCase.ca_file, api._cert_file) + + def test_client_params_init(self): + api = self.new_client( + client.RESTClient, host_ip='11.12.13.14', password='mypass') + self.assertEqual(( + BaseClientTestCase.nsx_user, 'mypass'), + api._session.auth) + self.assertEqual('11.12.13.14', api._host_ip) + self.assertEqual(BaseClientTestCase.ca_file, api._cert_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.get')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_url_prefix(self, mock_validate, mock_get): + mock_get.return_value = {} + api = self.new_client(client.RESTClient, url_prefix='/cloud/api') + api.list() + + assert_session_call( + mock_get, + 'https://1.2.3.4/cloud/api', + False, None, {}, BaseClientTestCase.ca_file) + + mock_get.reset_mock() + + api.url_list('v1/ports') + assert_session_call( + mock_get, + 'https://1.2.3.4/cloud/api/v1/ports', False, None, {}, + BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.get')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_headers(self, mock_validate, mock_get): + default_headers = {'Content-Type': 'application/golang'} + + mock_get.return_value = {} + api = self.new_client( + client.RESTClient, default_headers=default_headers, + url_prefix='/v1/api') + api.list() + + assert_session_call( + mock_get, + 'https://1.2.3.4/v1/api', + False, None, default_headers, BaseClientTestCase.ca_file) + + mock_get.reset_mock() + + method_headers = {'X-API-Key': 'strong-crypt'} + api.url_list('ports/33', headers=method_headers) + method_headers.update(default_headers) + assert_session_call( + mock_get, + 'https://1.2.3.4/v1/api/ports/33', False, None, + method_headers, + BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.get')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_for(self, mock_validate, mock_get): + api = self.new_client(client.RESTClient, url_prefix='api/v1/') + sub_api = api.new_client_for('switch/ports') + sub_api.get('11a2b') + + assert_session_call( + mock_get, + 'https://1.2.3.4/api/v1/switch/ports/11a2b', + False, None, {}, BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.get')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_list(self, mock_validate, mock_get): + api = self.new_client(client.RESTClient, url_prefix='api/v1/ports') + api.list() + + assert_session_call( + mock_get, + 'https://1.2.3.4/api/v1/ports', + False, None, {}, BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.get')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_get(self, mock_validate, mock_get): + api = self.new_client(client.RESTClient, url_prefix='api/v1/ports') + api.get('unique-id') + + assert_session_call( + mock_get, + 'https://1.2.3.4/api/v1/ports/unique-id', + False, None, {}, BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.delete')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_delete(self, mock_validate, mock_delete): + api = self.new_client(client.RESTClient, url_prefix='api/v1/ports') + api.delete('unique-id') + + assert_session_call( + mock_delete, + 'https://1.2.3.4/api/v1/ports/unique-id', + False, None, {}, BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.put')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_update(self, mock_validate, mock_put): + api = self.new_client(client.RESTClient, url_prefix='api/v1/ports') + api.update('unique-id', {'name': 'a-new-name'}) + + assert_session_call( + mock_put, + 'https://1.2.3.4/api/v1/ports/unique-id', + False, {'name': 'a-new-name'}, + {}, BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.post')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_create(self, mock_validate, mock_post): + api = self.new_client(client.RESTClient, url_prefix='api/v1/ports') + api.create({'resource-name': 'port1'}) + + assert_session_call( + mock_post, + 'https://1.2.3.4/api/v1/ports', + False, {'resource-name': 'port1'}, + {}, BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.get')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_url_list(self, mock_validate, mock_get): + api = self.new_client(client.RESTClient, url_prefix='api/v1/ports') + api.url_list('/connections', {'Content-Type': 'application/json'}) + + assert_session_call( + mock_get, + 'https://1.2.3.4/api/v1/ports/connections', + False, None, + {'Content-Type': 'application/json'}, + BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.get')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_url_get(self, mock_validate, mock_get): + api = self.new_client(client.RESTClient, url_prefix='api/v1/ports') + api.url_get('connections/1') + + assert_session_call( + mock_get, + 'https://1.2.3.4/api/v1/ports/connections/1', + False, None, {}, BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.delete')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_url_delete(self, mock_validate, mock_delete): + api = self.new_client(client.RESTClient, url_prefix='api/v1/ports') + api.url_delete('1') + + assert_session_call( + mock_delete, + 'https://1.2.3.4/api/v1/ports/1', + False, None, {}, BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.put')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_url_put(self, mock_validate, mock_put): + api = self.new_client(client.RESTClient, url_prefix='api/v1/ports') + api.url_put('connections/1', {'name': 'conn1'}) + + assert_session_call( + mock_put, + 'https://1.2.3.4/api/v1/ports/connections/1', + False, {'name': 'conn1'}, + {}, BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.post')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_client_url_post(self, mock_validate, mock_post): + api = self.new_client(client.RESTClient, url_prefix='api/v1/ports') + api.url_post('1/connections', {'name': 'conn1'}) + + assert_session_call( + mock_post, + 'https://1.2.3.4/api/v1/ports/1/connections', + False, {'name': 'conn1'}, + {}, BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.get')) + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.put')) + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.post')) + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.delete')) + def test_client_validate_result(self, *args): + + def _verb_response_code(http_verb, status_code): + response = mocks.MockRequestsResponse(status_code, None) + api = self.new_client(client.RESTClient) + for mocked in args: + mocked.return_value = response + client_call = getattr(api, "url_%s" % http_verb) + client_call('', None) + + for verb in ['get', 'post', 'put', 'delete']: + for code in client.RESTClient._VERB_RESP_CODES.get(verb): + _verb_response_code(verb, code) + self.assertRaises( + exep.ManagerError, _verb_response_code, verb, 500) + + +class NsxV3JSONClientTestCase(BaseClientTestCase): + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.post')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_json_request(self, mock_validate, mock_post): + mock_post.return_value = mocks.MockRequestsResponse( + 200, jsonutils.dumps({'result': {'ok': 200}})) + + api = self.new_client(client.JSONRESTClient, url_prefix='api/v2/nat') + resp = api.create(body={'name': 'mgmt-egress'}) + + assert_session_call( + mock_post, + 'https://1.2.3.4/api/v2/nat', + False, jsonutils.dumps({'name': 'mgmt-egress'}), + client.JSONRESTClient._DEFAULT_HEADERS, + BaseClientTestCase.ca_file) + + self.assertEqual(resp, {'result': {'ok': 200}}) + + +class NsxV3APIClientTestCase(BaseClientTestCase): + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.get')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_api_call(self, mock_validate, mock_get): + api = self.new_client(client.NSX3Client) + api.get('ports') + + assert_session_call( + mock_get, + 'https://1.2.3.4/api/v1/ports', + False, None, + client.JSONRESTClient._DEFAULT_HEADERS, + NsxV3APIClientTestCase.ca_file) + + +# NOTE(boden): remove this when tmp brigding removed +class NsxV3APIClientBridgeTestCase(BaseClientTestCase): + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.get')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_get_resource(self, mock_validate, mock_get): + client.get_resource('ports') + + assert_session_call( + mock_get, + 'https://1.2.3.4/api/v1/ports', + False, None, + client.JSONRESTClient._DEFAULT_HEADERS, + NsxV3APIClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.post')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_create_resource(self, mock_validate, mock_post): + client.create_resource('ports', {'resource-name': 'port1'}) + + assert_session_call( + mock_post, + 'https://1.2.3.4/api/v1/ports', + False, jsonutils.dumps({'resource-name': 'port1'}), + client.JSONRESTClient._DEFAULT_HEADERS, + BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.put')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_update_resource(self, mock_validate, mock_put): + client.update_resource('ports/1', {'name': 'a-new-name'}) + + assert_session_call( + mock_put, + 'https://1.2.3.4/api/v1/ports/1', + False, jsonutils.dumps({'name': 'a-new-name'}), + client.JSONRESTClient._DEFAULT_HEADERS, + BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.delete')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_delete_resource(self, mock_validate, mock_delete): + client.delete_resource('ports/11') + + assert_session_call( + mock_delete, + 'https://1.2.3.4/api/v1/ports/11', + False, None, client.JSONRESTClient._DEFAULT_HEADERS, + BaseClientTestCase.ca_file) diff --git a/vmware_nsx/tests/unit/vmware/nsxlib/v3/test_resources.py b/vmware_nsx/tests/unit/vmware/nsxlib/v3/test_resources.py new file mode 100644 index 0000000000..1732cd9811 --- /dev/null +++ b/vmware_nsx/tests/unit/vmware/nsxlib/v3/test_resources.py @@ -0,0 +1,144 @@ +# 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 mock +import vmware_nsx.tests.unit.vmware.nsx_v3_mocks as mocks + +from oslo_serialization import jsonutils +from vmware_nsx.nsxlib.v3 import client +from vmware_nsx.nsxlib.v3 import resources +from vmware_nsx.tests.unit.vmware.nsxlib.v3 import test_client + + +CLIENT_PKG = test_client.CLIENT_PKG +profile_types = resources.SwitchingProfileTypes + + +class TestSwitchingProfileTestCase(test_client.BaseClientTestCase): + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.post')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_switching_profile_create(self, mock_validate, mock_post): + api = resources.SwitchingProfile(client.NSX3Client()) + api.create(profile_types.PORT_MIRRORING, + 'pm-profile', 'port mirror prof') + + test_client.assert_session_call( + mock_post, + 'https://1.2.3.4/api/v1/switching-profiles', + False, jsonutils.dumps({ + 'resource_type': profile_types.PORT_MIRRORING, + 'display_name': 'pm-profile', + 'description': 'port mirror prof' + }), + client.JSONRESTClient._DEFAULT_HEADERS, + test_client.BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.put')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_switching_profile_update(self, mock_validate, mock_put): + + tags = [ + { + 'scope': 'os-tid', + 'tag': 'tenant-1' + }, + { + 'scope': 'os-api-version', + 'tag': '2.1.1.0' + } + ] + + api = resources.SwitchingProfile(client.NSX3Client()) + api.update('a12bc1', profile_types.PORT_MIRRORING, tags=tags) + + test_client.assert_session_call( + mock_put, + 'https://1.2.3.4/api/v1/switching-profiles/a12bc1', + False, jsonutils.dumps({ + 'resource_type': profile_types.PORT_MIRRORING, + 'tags': tags + }), + client.JSONRESTClient._DEFAULT_HEADERS, + test_client.BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.post')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_spoofgaurd_profile_create(self, mock_validate, mock_post): + + tags = [ + { + 'scope': 'os-tid', + 'tag': 'tenant-1' + }, + { + 'scope': 'os-api-version', + 'tag': '2.1.1.0' + } + ] + + api = resources.SwitchingProfile(client.NSX3Client()) + api.create_spoofguard_profile( + 'neutron-spoof', 'spoofguard-for-neutron', + whitelist_ports=True, tags=tags) + + test_client.assert_session_call( + mock_post, + 'https://1.2.3.4/api/v1/switching-profiles', + False, + jsonutils.dumps({ + 'resource_type': profile_types.SPOOF_GUARD, + 'display_name': 'neutron-spoof', + 'description': 'spoofguard-for-neutron', + 'white_list_providers': ['LPORT_BINDINGS'], + 'tags': tags + }), + client.JSONRESTClient._DEFAULT_HEADERS, + test_client.BaseClientTestCase.ca_file) + + @mock.patch("%s.%s" % (CLIENT_PKG, 'requests.Session.get')) + @mock.patch(CLIENT_PKG + '.RESTClient._validate_result') + def test_find_by_display_name(self, mock_validate, mock_get): + resp_resources = { + 'results': [ + {'display_name': 'resource-1'}, + {'display_name': 'resource-2'}, + {'display_name': 'resource-3'} + ] + } + mock_get.return_value = mocks.MockRequestsResponse( + 200, jsonutils.dumps(resp_resources)) + api = resources.SwitchingProfile(client.NSX3Client()) + self.assertEqual([{'display_name': 'resource-1'}], + api.find_by_display_name('resource-1')) + self.assertEqual([{'display_name': 'resource-2'}], + api.find_by_display_name('resource-2')) + self.assertEqual([{'display_name': 'resource-3'}], + api.find_by_display_name('resource-3')) + + mock_get.reset_mock() + + resp_resources = { + 'results': [ + {'display_name': 'resource-1'}, + {'display_name': 'resource-1'}, + {'display_name': 'resource-1'} + ] + } + mock_get.return_value = mocks.MockRequestsResponse( + 200, jsonutils.dumps(resp_resources)) + api = resources.SwitchingProfile(client.NSX3Client()) + self.assertEqual(resp_resources['results'], + api.find_by_display_name('resource-1'))