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
This commit is contained in:
Abhishek Raut 2014-05-28 12:03:06 -07:00
parent e9edc63ac2
commit f653b13146
4 changed files with 80 additions and 137 deletions

View File

@ -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)
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 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())
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

View File

@ -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):

View File

@ -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.

View File

@ -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 = (
"""<?xml version="1.0" encoding="utf-8"?>
<set name="events_set">
<instance name="1" url="/api/hyper-v/events/1">
<properties>
<cmd>configure terminal ; port-profile type vethernet grizzlyPP
(SUCCESS)
</cmd>
<id>42227269-e348-72ed-bdb7-7ce91cd1423c</id>
<time>1369223611</time>
<name>grizzlyPP</name>
</properties>
</instance>
<instance name="2" url="/api/hyper-v/events/2">
<properties>
<cmd>configure terminal ; port-profile type vethernet havanaPP
(SUCCESS)
</cmd>
<id>3fc83608-ae36-70e7-9d22-dec745623d06</id>
<time>1369223661</time>
<name>havanaPP</name>
</properties>
</instance>
</set>
""")
# 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):