From 8cc066b1e9a2bdd321b88afc9fc8f39eaca95078 Mon Sep 17 00:00:00 2001 From: Abhishek Raut Date: Wed, 28 May 2014 12:03:06 -0700 Subject: [PATCH] Replace XML with JSON for N1kv REST calls Currently the cisco n1kv plugin handles XML responses from VSM. This change will replace the httplib2 with requests library and handle JSON responses from the VSM. Implements: blueprint cisco-n1kv-json-support Change-Id: Icd32a6a2ab815ccd24ad86371e927c132e204413 --- neutron/plugins/cisco/n1kv/n1kv_client.py | 90 +++++-------------- .../plugins/cisco/n1kv/n1kv_neutron_plugin.py | 35 ++++---- neutron/tests/unit/cisco/n1kv/fake_client.py | 18 +++- .../tests/unit/cisco/n1kv/test_n1kv_plugin.py | 74 ++++++--------- 4 files changed, 80 insertions(+), 137 deletions(-) diff --git a/neutron/plugins/cisco/n1kv/n1kv_client.py b/neutron/plugins/cisco/n1kv/n1kv_client.py index 79abf715e..7389eadfb 100644 --- a/neutron/plugins/cisco/n1kv/n1kv_client.py +++ b/neutron/plugins/cisco/n1kv/n1kv_client.py @@ -18,18 +18,18 @@ # @author: Rudrajit Tapadar, Cisco Systems, Inc. import base64 -import httplib2 import netaddr +import requests from neutron.common import exceptions as n_exc from neutron.extensions import providernet +from neutron.openstack.common import jsonutils from neutron.openstack.common import log as logging from neutron.plugins.cisco.common import cisco_constants as c_const from neutron.plugins.cisco.common import cisco_credentials_v2 as c_cred from neutron.plugins.cisco.common import cisco_exceptions as c_exc from neutron.plugins.cisco.db import network_db_v2 from neutron.plugins.cisco.extensions import n1kv -from neutron import wsgi LOG = logging.getLogger(__name__) @@ -105,25 +105,6 @@ class Client(object): """ - # Metadata for deserializing xml - _serialization_metadata = { - "application/xml": { - "attributes": { - "network": ["id", "name"], - "port": ["id", "mac_address"], - "subnet": ["id", "prefix"] - }, - }, - "plurals": { - "networks": "network", - "ports": "port", - "set": "instance", - "subnets": "subnet", - "mappings": "mapping", - "segments": "segment" - } - } - # Define paths for the URI where the client connects for HTTP requests. port_profiles_path = "/virtual-port-profile" network_segment_path = "/network-segment/%s" @@ -152,7 +133,7 @@ class Client(object): """ Fetch all policy profiles from the VSM. - :returns: XML string + :returns: JSON string """ return self._get(self.port_profiles_path) @@ -430,8 +411,8 @@ class Client(object): """ Perform the HTTP request. - The response is in either XML format or plain text. A GET method will - invoke a XML response while a PUT/POST/DELETE returns message from the + The response is in either JSON format or plain text. A GET method will + invoke a JSON response while a PUT/POST/DELETE returns message from the VSM in plain text format. Exception is raised when VSM replies with an INTERNAL SERVER ERROR HTTP status code (500) i.e. an error has occurred on the VSM or SERVICE @@ -441,58 +422,35 @@ class Client(object): :param action: path to which the client makes request :param body: dict for arguments which are sent as part of the request :param headers: header for the HTTP request - :returns: XML or plain text in HTTP response + :returns: JSON or plain text in HTTP response """ action = self.action_prefix + action if not headers and self.hosts: headers = self._get_auth_header(self.hosts[0]) headers['Content-Type'] = self._set_content_type('json') + headers['Accept'] = self._set_content_type('json') if body: - body = self._serialize(body) + body = jsonutils.dumps(body, indent=2) LOG.debug(_("req: %s"), body) try: - resp, replybody = (httplib2.Http(timeout=self.timeout). - request(action, - method, - body=body, - headers=headers)) + resp = requests.request(method, + url=action, + data=body, + headers=headers, + timeout=self.timeout) except Exception as e: raise c_exc.VSMConnectionFailed(reason=e) - LOG.debug(_("status_code %s"), resp.status) - if resp.status == 200: - if 'application/xml' in resp['content-type']: - return self._deserialize(replybody, resp.status) - elif 'text/plain' in resp['content-type']: - LOG.debug(_("VSM: %s"), replybody) - else: - raise c_exc.VSMError(reason=replybody) - - def _serialize(self, data): - """ - Serialize a dictionary with a single key into either xml or json. - - :param data: data in the form of dict - """ - if data is None: - return None - elif type(data) is dict: - return wsgi.Serializer().serialize(data, self._set_content_type()) + LOG.debug(_("status_code %s"), resp.status_code) + if resp.status_code == requests.codes.OK: + if 'application/json' in resp.headers['content-type']: + try: + return resp.json() + except ValueError: + return {} + elif 'text/plain' in resp.headers['content-type']: + LOG.debug(_("VSM: %s"), resp.text) else: - raise Exception(_("Unable to serialize object of type = '%s'") % - type(data)) - - def _deserialize(self, data, status_code): - """ - Deserialize an XML string into a dictionary. - - :param data: XML string from the HTTP response - :param status_code: integer status code from the HTTP response - :return: data in the form of dict - """ - if status_code == 204: - return data - return wsgi.Serializer(self._serialization_metadata).deserialize( - data, self._set_content_type('xml')) + raise c_exc.VSMError(reason=resp.text) def _set_content_type(self, format=None): """ @@ -539,7 +497,7 @@ class Client(object): """ username = c_cred.Store.get_username(host_ip) password = c_cred.Store.get_password(host_ip) - auth = base64.encodestring("%s:%s" % (username, password)) + auth = base64.encodestring("%s:%s" % (username, password)).rstrip() header = {"Authorization": "Basic %s" % auth} return header diff --git a/neutron/plugins/cisco/n1kv/n1kv_neutron_plugin.py b/neutron/plugins/cisco/n1kv/n1kv_neutron_plugin.py index 1bc247224..168ba8b10 100644 --- a/neutron/plugins/cisco/n1kv/n1kv_neutron_plugin.py +++ b/neutron/plugins/cisco/n1kv/n1kv_neutron_plugin.py @@ -176,29 +176,24 @@ class N1kvNeutronPluginV2(db_base_plugin_v2.NeutronDbPluginV2, n1kvclient = n1kv_client.Client() policy_profiles = n1kvclient.list_port_profiles() vsm_profiles = {} - plugin_profiles = {} + plugin_profiles_set = set() # Fetch policy profiles from VSM - if policy_profiles: - for profile in policy_profiles['body'][c_const.SET]: - profile_name = (profile[c_const.PROPERTIES]. - get(c_const.NAME, None)) - profile_id = (profile[c_const.PROPERTIES]. - get(c_const.ID, None)) - if profile_id and profile_name: - vsm_profiles[profile_id] = profile_name - # Fetch policy profiles previously populated - for profile in n1kv_db_v2.get_policy_profiles(): - plugin_profiles[profile.id] = profile.name - vsm_profiles_set = set(vsm_profiles) - plugin_profiles_set = set(plugin_profiles) - # Update database if the profile sets differ. - if vsm_profiles_set ^ plugin_profiles_set: + for profile_name in policy_profiles: + profile_id = (policy_profiles + [profile_name][c_const.PROPERTIES][c_const.ID]) + vsm_profiles[profile_id] = profile_name + # Fetch policy profiles previously populated + for profile in n1kv_db_v2.get_policy_profiles(): + plugin_profiles_set.add(profile.id) + vsm_profiles_set = set(vsm_profiles) + # Update database if the profile sets differ. + if vsm_profiles_set ^ plugin_profiles_set: # Add profiles in database if new profiles were created in VSM - for pid in vsm_profiles_set - plugin_profiles_set: - self._add_policy_profile(vsm_profiles[pid], pid) + for pid in vsm_profiles_set - plugin_profiles_set: + self._add_policy_profile(vsm_profiles[pid], pid) # Delete profiles from database if profiles were deleted in VSM - for pid in plugin_profiles_set - vsm_profiles_set: - self._delete_policy_profile(pid) + for pid in plugin_profiles_set - vsm_profiles_set: + self._delete_policy_profile(pid) self._remove_all_fake_policy_profiles() except (cisco_exceptions.VSMError, cisco_exceptions.VSMConnectionFailed): diff --git a/neutron/tests/unit/cisco/n1kv/fake_client.py b/neutron/tests/unit/cisco/n1kv/fake_client.py index 6348bbfd0..2d1f0780e 100755 --- a/neutron/tests/unit/cisco/n1kv/fake_client.py +++ b/neutron/tests/unit/cisco/n1kv/fake_client.py @@ -51,9 +51,8 @@ class TestClient(n1kv_client.Client): return _validate_resource(action, body) elif method == 'GET': if 'virtual-port-profile' in action: - profiles = _policy_profile_generator_xml( + return _policy_profile_generator( self._get_total_profiles()) - return self._deserialize(profiles, 200) else: raise c_exc.VSMError(reason='VSM:Internal Server Error') @@ -82,6 +81,21 @@ def _validate_resource(action, body=None): return +def _policy_profile_generator(total_profiles): + """ + Generate policy profile response and return a dictionary. + + :param total_profiles: integer representing total number of profiles to + return + """ + profiles = {} + for num in range(1, total_profiles + 1): + name = "pp-%s" % num + profile_id = "00000000-0000-0000-0000-00000000000%s" % num + profiles[name] = {"properties": {"name": name, "id": profile_id}} + return profiles + + def _policy_profile_generator_xml(total_profiles): """ Generate policy profile response in XML format. diff --git a/neutron/tests/unit/cisco/n1kv/test_n1kv_plugin.py b/neutron/tests/unit/cisco/n1kv/test_n1kv_plugin.py index ad45b0910..b1833436e 100644 --- a/neutron/tests/unit/cisco/n1kv/test_n1kv_plugin.py +++ b/neutron/tests/unit/cisco/n1kv/test_n1kv_plugin.py @@ -50,20 +50,18 @@ VLAN_MAX = 110 class FakeResponse(object): """ - This object is returned by mocked httplib instead of a normal response. + This object is returned by mocked requests lib instead of normal response. - Initialize it with the status code, content type and buffer contents - you wish to return. + Initialize it with the status code, header and buffer contents you wish to + return. """ - def __init__(self, status, response_text, content_type): + def __init__(self, status, response_text, headers): self.buffer = response_text - self.status = status + self.status_code = status + self.headers = headers - def __getitem__(cls, val): - return "application/xml" - - def read(self, *args, **kwargs): + def json(self, *args, **kwargs): return self.buffer @@ -154,47 +152,28 @@ class N1kvPluginTestCase(test_plugin.NeutronDbPluginV2TestCase): """ if not self.DEFAULT_RESP_BODY: - self.DEFAULT_RESP_BODY = ( - """ - - - - configure terminal ; port-profile type vethernet grizzlyPP - (SUCCESS) - - 42227269-e348-72ed-bdb7-7ce91cd1423c - - grizzlyPP - - - - - configure terminal ; port-profile type vethernet havanaPP - (SUCCESS) - - 3fc83608-ae36-70e7-9d22-dec745623d06 - - havanaPP - - - - """) - # Creating a mock HTTP connection object for httplib. The N1KV client - # interacts with the VSM via HTTP. Since we don't have a VSM running - # in the unit tests, we need to 'fake' it by patching the HTTP library - # itself. We install a patch for a fake HTTP connection class. + self.DEFAULT_RESP_BODY = { + "icehouse-pp": {"properties": {"name": "icehouse-pp", + "id": "some-uuid-1"}}, + "havana_pp": {"properties": {"name": "havana_pp", + "id": "some-uuid-2"}}, + "dhcp_pp": {"properties": {"name": "dhcp_pp", + "id": "some-uuid-3"}}, + } + # Creating a mock HTTP connection object for requests lib. The N1KV + # client interacts with the VSM via HTTP. Since we don't have a VSM + # running in the unit tests, we need to 'fake' it by patching the HTTP + # library itself. We install a patch for a fake HTTP connection class. # Using __name__ to avoid having to enter the full module path. - http_patcher = mock.patch(n1kv_client.httplib2.__name__ + ".Http") + http_patcher = mock.patch(n1kv_client.requests.__name__ + ".request") FakeHttpConnection = http_patcher.start() # Now define the return values for a few functions that may be called # on any instance of the fake HTTP connection class. - instance = FakeHttpConnection.return_value - instance.getresponse.return_value = (FakeResponse( - self.DEFAULT_RESP_CODE, - self.DEFAULT_RESP_BODY, - 'application/xml')) - instance.request.return_value = (instance.getresponse.return_value, - self.DEFAULT_RESP_BODY) + self.resp_headers = {"content-type": "application/json"} + FakeHttpConnection.return_value = (FakeResponse( + self.DEFAULT_RESP_CODE, + self.DEFAULT_RESP_BODY, + self.resp_headers)) # Patch some internal functions in a few other parts of the system. # These help us move along, without having to mock up even more systems @@ -612,9 +591,6 @@ class TestN1kvSubnets(test_plugin.TestSubnetsV2, def setUp(self): super(TestN1kvSubnets, self).setUp() - # Create some of the database entries that we require. - self._make_test_policy_profile(name='dhcp_pp') - class TestN1kvL3Test(test_l3_plugin.L3NatExtensionTestCase): -- 2.45.2