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
# @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__)
"""
- # 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"
"""
Fetch all policy profiles from the VSM.
- :returns: XML string
+ :returns: JSON string
"""
return self._get(self.port_profiles_path)
"""
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
: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):
"""
"""
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
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):
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')
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.
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
"""
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
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):