This commit consists of:
1) Parsing protobuf responses from Voltha into a dict which will also
include the yang proto annotations
2) Converting a protobuf response into a yang-compatible XML format
3) Support for GET (no request params) for Voltha, VolthaInstance and VolthaInstances
4) Minor bug fixes
5) Testing done using the MG-Soft Netconf client
Change-Id: Ibb7f62a391e19b0240cc739919fccc689a316005
diff --git a/netconf/nc_rpc/base/close_session.py b/netconf/nc_rpc/base/close_session.py
index ce187cd..7e76b45 100644
--- a/netconf/nc_rpc/base/close_session.py
+++ b/netconf/nc_rpc/base/close_session.py
@@ -24,9 +24,8 @@
class CloseSession(Rpc):
- def __init__(self, rpc_request, rpc_method, grpc_client, session):
- super(CloseSession, self).__init__(rpc_request, rpc_method,
- grpc_client, session)
+ def __init__(self, request, grpc_client, session):
+ super(CloseSession, self).__init__(request, grpc_client, session)
self._validate_parameters()
def execute(self):
diff --git a/netconf/nc_rpc/base/commit.py b/netconf/nc_rpc/base/commit.py
index 8933dc3..06c1746 100644
--- a/netconf/nc_rpc/base/commit.py
+++ b/netconf/nc_rpc/base/commit.py
@@ -24,9 +24,8 @@
class Commit(Rpc):
- def __init__(self, rpc_request, rpc_method, grpc_client, session):
- super(Commit, self).__init__(rpc_request, rpc_method, grpc_client,
- session)
+ def __init__(self, request, grpc_client, session):
+ super(Commit, self).__init__(request, grpc_client, session)
self._validate_parameters()
def execute(self):
diff --git a/netconf/nc_rpc/base/copy_config.py b/netconf/nc_rpc/base/copy_config.py
index e287770..a59081d 100644
--- a/netconf/nc_rpc/base/copy_config.py
+++ b/netconf/nc_rpc/base/copy_config.py
@@ -23,9 +23,8 @@
class CopyConfig(Rpc):
- def __init__(self, rpc_request, rpc_method, grpc_client, session):
- super(CopyConfig, self).__init__(rpc_request, rpc_method,
- grpc_client, session)
+ def __init__(self, request, grpc_client, session):
+ super(CopyConfig, self).__init__(request, grpc_client, session)
self._validate_parameters()
def execute(self):
diff --git a/netconf/nc_rpc/base/delete_config.py b/netconf/nc_rpc/base/delete_config.py
index e267807..8ad5ea4 100644
--- a/netconf/nc_rpc/base/delete_config.py
+++ b/netconf/nc_rpc/base/delete_config.py
@@ -23,9 +23,8 @@
class DeleteConfig(Rpc):
- def __init__(self, rpc_request, rpc_method, grpc_client, session):
- super(DeleteConfig, self).__init__(rpc_request, rpc_method,
- grpc_client, session)
+ def __init__(self, request, grpc_client, session):
+ super(DeleteConfig, self).__init__(request, grpc_client, session)
self._validate_parameters()
def execute(self):
diff --git a/netconf/nc_rpc/base/discard_changes.py b/netconf/nc_rpc/base/discard_changes.py
index 57bdfed..961eea0 100644
--- a/netconf/nc_rpc/base/discard_changes.py
+++ b/netconf/nc_rpc/base/discard_changes.py
@@ -23,9 +23,8 @@
class DiscardChanges(Rpc):
- def __init__(self, rpc_request, rpc_method, grpc_client, session):
- super(DiscardChanges, self).__init__(rpc_request, rpc_method,
- grpc_client, session)
+ def __init__(self, request, grpc_client, session):
+ super(DiscardChanges, self).__init__(request, grpc_client, session)
self._validate_parameters()
def execute(self):
diff --git a/netconf/nc_rpc/base/edit_config.py b/netconf/nc_rpc/base/edit_config.py
index 469e957..92fa085 100644
--- a/netconf/nc_rpc/base/edit_config.py
+++ b/netconf/nc_rpc/base/edit_config.py
@@ -23,9 +23,8 @@
class EditConfig(Rpc):
- def __init__(self, rpc_request, rpc_method, grpc_client, session):
- super(EditConfig, self).__init__(rpc_request, rpc_method,
- grpc_client, session)
+ def __init__(self, request, grpc_client, session):
+ super(EditConfig, self).__init__(request, grpc_client, session)
self._validate_parameters()
def execute(self):
diff --git a/netconf/nc_rpc/base/get.py b/netconf/nc_rpc/base/get.py
index 7334dba..71449ad 100644
--- a/netconf/nc_rpc/base/get.py
+++ b/netconf/nc_rpc/base/get.py
@@ -28,55 +28,113 @@
class Get(Rpc):
- def __init__(self, rpc_request, rpc_method, voltha_method_ref, grpc_client,
- session):
- super(Get, self).__init__(rpc_request, rpc_method, voltha_method_ref,
- grpc_client, session)
+ def __init__(self, request, grpc_client, session):
+ super(Get, self).__init__(request, grpc_client, session)
self._validate_parameters()
@inlineCallbacks
def execute(self):
- log.info('get-request', session=self.session.session_id,
- method=self.rpc_method)
if self.rpc_response.is_error:
returnValue(self.rpc_response)
- # TODO: for debugging only, assume we are doing a voltha-getinstance
- self.voltha_method_ref='VolthaLocalService-GetVolthaInstance'
+ log.info('get-request', session=self.session.session_id,
+ request=self.request)
+
+ rpc = self.get_voltha_rpc(self.request)
+ if not rpc:
+ log.info('unsupported-request', request=self.request)
+ self.rpc_response.is_error = True
+ self.rpc_response.node = ncerror.BadMsg(self.request)
+ return
+
# Invoke voltha via the grpc client
- res_dict = yield self.grpc_client.invoke_voltha_api(self.voltha_method_ref)
+ res_dict = yield self.grpc_client.invoke_voltha_api(rpc)
# convert dict to xml
- xml = dicttoxml.dicttoxml(res_dict, attr_type=False)
+ xml = dicttoxml.dicttoxml(res_dict, attr_type=True)
log.info('voltha-info', res=res_dict, xml=xml)
root_elem = self.get_root_element(xml)
- root_elem.tag = 'data'
- log.info('rpc-method', etree.tounicode(self.rpc_method,
- pretty_print=True))
-
- self.rpc_method.append(root_elem)
- self.rpc_response.node = self.rpc_method
+ # Build the yang response
+ self.rpc_response.node = self.rpc_response.build_yang_response(
+ root_elem, self.request)
self.rpc_response.is_error = False
returnValue(self.rpc_response)
-
def _validate_parameters(self):
log.info('validate-parameters', session=self.session.session_id)
- self.params = self.rpc_method.getchildren()
- if len(self.params) > 1:
- self.rpc_response.is_error = True
- self.rpc_response.node = ncerror.BadMsg(self.rpc_request)
- return
+ # Validate the GET command
+ if self.request:
+ try:
+ if self.request['command'] != 'get':
+ self.rpc_response.is_error = True
+ self.rpc_response.node = ncerror.BadMsg('No GET in get '
+ 'request')
- if self.params and not filter_tag_match(self.params[0], C.NC_FILTER):
- self.rpc_response.is_error = True
- self.rpc_response.node = ncerror.UnknownElement(
- self.rpc_request, self.params[0])
- return
+ if self.request.has_key('filter'):
+ if not self.request.has_key('class'):
+ self.rpc_response.is_error = True
+ self.rpc_response.node = ncerror.BadMsg(
+ 'Missing filter sub-element')
- if not self.params:
- self.params = [None]
+ except Exception as e:
+ self.rpc_response.is_error = True
+ self.rpc_response.node = ncerror.BadMsg(self.request)
+ return
+ def get_voltha_rpc(self, request):
+ if request.has_key('class'):
+ rpcs = self.rpc_request_mapping.get(request['class'])
+ if rpcs is None:
+ return None
+ for rpc in rpcs:
+ if request.has_key('subclass'):
+ # search first for subclass
+ if rpc['subclass'] and request['subclass'] == rpc[
+ 'subclass']:
+ return rpc['rpc']
+
+ # If we are here then no subclass exists. Just return the rpc
+ # associated with theNone subclass
+ for rpc in rpcs:
+ if rpc['subclass'] is None:
+ return rpc['rpc']
+
+ return None
+
+ # Supported Get Methods
+ rpc_request_mapping = {
+ 'Voltha': [
+ {'subclass': None,
+ 'rpc': 'VolthaGlobalService-GetVoltha'
+ }],
+ 'VolthaInstance': [
+ {'subclass': None,
+ 'rpc': 'VolthaLocalService-GetVolthaInstance'
+ },
+ {'subclass': 'health',
+ 'rpc': 'VolthaLocalService-GetHealth'
+ },
+ {'subclass': 'adapters',
+ 'rpc': 'VolthaLocalService-ListAdapters'
+ },
+ {'subclass': 'logical_devices',
+ 'rpc': 'VolthaLocalService-ListLogicalDevices'
+ },
+ {'subclass': 'devices',
+ 'rpc': 'VolthaLocalService-ListDevices'
+ },
+ {'subclass': 'device_types',
+ 'rpc': 'VolthaLocalService-ListDeviceTypes'
+ },
+ {'subclass': 'device_groups',
+ 'rpc': 'VolthaLocalService-ListDeviceGroups'
+ },
+ ],
+ 'VolthaInstances': [
+ {'subclass': None,
+ 'rpc': 'VolthaGlobalService-ListVolthaInstances'
+ }],
+ }
diff --git a/netconf/nc_rpc/base/get_config.py b/netconf/nc_rpc/base/get_config.py
index 09f90b4..2fb8ad0 100644
--- a/netconf/nc_rpc/base/get_config.py
+++ b/netconf/nc_rpc/base/get_config.py
@@ -26,9 +26,8 @@
class GetConfig(Rpc):
- def __init__(self, rpc_request, rpc_method, grpc_client, session):
- super(GetConfig, self).__init__(rpc_request, rpc_method,
- grpc_client, session)
+ def __init__(self, request, grpc_client, session):
+ super(GetConfig, self).__init__(request, grpc_client, session)
self._validate_parameters()
def execute(self):
diff --git a/netconf/nc_rpc/base/kill_session.py b/netconf/nc_rpc/base/kill_session.py
index c9a3352..16461cd 100644
--- a/netconf/nc_rpc/base/kill_session.py
+++ b/netconf/nc_rpc/base/kill_session.py
@@ -25,9 +25,8 @@
class KillSession(Rpc):
- def __init__(self, rpc_request, rpc_method, grpc_client, session):
- super(KillSession, self).__init__(rpc_request, rpc_method,
- grpc_client, session)
+ def __init__(self, request, grpc_client, session):
+ super(KillSession, self).__init__(request, grpc_client, session)
self._validate_parameters()
def execute(self):
diff --git a/netconf/nc_rpc/base/lock.py b/netconf/nc_rpc/base/lock.py
index 2f0130d..2f41e24 100644
--- a/netconf/nc_rpc/base/lock.py
+++ b/netconf/nc_rpc/base/lock.py
@@ -23,9 +23,8 @@
class Lock(Rpc):
- def __init__(self, rpc_request, rpc_method, grpc_client, session):
- super(Lock, self).__init__(rpc_request, rpc_method, grpc_client,
- session)
+ def __init__(self, request, grpc_client, session):
+ super(Lock, self).__init__(request, grpc_client, session)
self._validate_parameters()
def execute(self):
diff --git a/netconf/nc_rpc/base/validate.py b/netconf/nc_rpc/base/validate.py
index 93faf60..8671b12 100644
--- a/netconf/nc_rpc/base/validate.py
+++ b/netconf/nc_rpc/base/validate.py
@@ -23,9 +23,8 @@
class Validate(Rpc):
- def __init__(self, rpc_request, rpc_method, grpc_client, session):
- super(Validate, self).__init__(rpc_request, rpc_method,
- grpc_client, session)
+ def __init__(self, request, grpc_client, session):
+ super(Validate, self).__init__(request, grpc_client, session)
self._validate_parameters()
def execute(self):
diff --git a/netconf/nc_rpc/ext/get_voltha.py b/netconf/nc_rpc/ext/get_voltha.py
index a083401..0c51e9a 100644
--- a/netconf/nc_rpc/ext/get_voltha.py
+++ b/netconf/nc_rpc/ext/get_voltha.py
@@ -28,10 +28,8 @@
class GetVoltha(Rpc):
- def __init__(self, rpc_request, rpc_method, voltha_method_ref, grpc_client,
- session):
- super(GetVoltha, self).__init__(rpc_request, rpc_method, voltha_method_ref,
- grpc_client, session)
+ def __init__(self, request, grpc_client, session):
+ super(GetVoltha, self).__init__(request, grpc_client, session)
self._validate_parameters()
@inlineCallbacks
diff --git a/netconf/nc_rpc/rpc.py b/netconf/nc_rpc/rpc.py
index 5b579c9..bafc58e 100644
--- a/netconf/nc_rpc/rpc.py
+++ b/netconf/nc_rpc/rpc.py
@@ -21,14 +21,13 @@
from lxml import etree
import io
+
+
class Rpc(object):
- def __init__(self,rpc_request, rpc_method, voltha_method_ref,
- grpc_client, session):
- self.rpc_request = rpc_request
- self.rpc_method = rpc_method
+ def __init__(self, request, grpc_client, session):
+ self.request = request
self.rpc_response = RpcResponse()
self.grpc_client = grpc_client
- self.voltha_method_ref = voltha_method_ref
self.session = session
diff --git a/netconf/nc_rpc/rpc_factory.py b/netconf/nc_rpc/rpc_factory.py
index aa0ced1..230c57a 100644
--- a/netconf/nc_rpc/rpc_factory.py
+++ b/netconf/nc_rpc/rpc_factory.py
@@ -35,6 +35,11 @@
log = structlog.get_logger()
from lxml import etree
+ns_map = {
+ 'base': '{urn:ietf:params:xml:ns:netconf:base:1.0}',
+ 'voltha': '{urn:opencord:params:xml:ns:voltha:ietf-voltha}'
+}
+
class RpcFactory:
instance = None
@@ -51,6 +56,39 @@
def _get_key(self, namespace, service, name):
return ''.join([namespace, service, name])
+ def get_attribute_value(self, name, attributes):
+ for tup in attributes.items():
+ if tup[0] == name:
+ return tup[1]
+
+ # Parse a request (node is an ElementTree) and return a dictionary
+ # TODO: This parser is specific to a GET request. Need to be it more
+ # generic
+ def parse_xml_request(self, node):
+ request = {}
+ if not len(node):
+ return request
+ for elem in node.iter():
+ if elem.tag.find(ns_map['base']) != -1: # found
+ elem_name = elem.tag.replace(ns_map['base'], "")
+ if elem_name == 'rpc':
+ request['type'] = 'rpc'
+ request['message_id'] = self.get_attribute_value(
+ 'message-id', elem.attrib)
+ elif elem_name == 'filter':
+ request['filter'] = self.get_attribute_value('type',
+ elem.attrib)
+ else:
+ request[
+ 'command'] = elem_name # attribute is empty for now
+ elif elem.tag.find(ns_map['voltha']) != -1: # found
+ if request.has_key('class'):
+ request['subclass'] = elem.tag.replace(ns_map['voltha'],
+ "")
+ else:
+ request['class'] = elem.tag.replace(ns_map['voltha'], "")
+ return request
+
def register_rpc(self, namespace, service, name, klass):
key = self._get_key(namespace, service, name)
if key not in self.rpc_map.keys():
@@ -63,33 +101,32 @@
def get_rpc_handler(self, rpc_node, msg, grpc_channel, session):
try:
- msg_id = rpc_node.get('message-id')
- log.info("Received-rpc-message-id", msg_id=msg_id)
+ # Parse the request into a dictionary
+ log.info("rpc-node",
+ node=etree.tostring(rpc_node, pretty_print=True))
- except (TypeError, ValueError):
- raise ncerror.SessionError(msg,
- "No valid message-id attribute found")
+ request = self.parse_xml_request(rpc_node)
+ if not request:
+ log.error("request-bad-format")
+ raise ncerror.BadMsg(rpc_node)
- log.info("rpc-node", node=etree.tostring(rpc_node, pretty_print=True))
+ if not request.has_key('message_id') or \
+ not request.has_key('command'):
+ log.error("request-no-message-id")
+ raise ncerror.BadMsg(rpc_node)
- # Get the first child of rpc as the method name
- rpc_method = rpc_node.getchildren()
- if len(rpc_method) != 1:
- log.error("badly-formatted-rpc-method", msg_id=msg_id)
+ log.info("parsed-request", request=request)
+
+ class_handler = self.rpc_class_handlers.get(request['command'],
+ None)
+ if class_handler is not None:
+ return class_handler(request, grpc_channel, session)
+
+ log.error("rpc-not-implemented", rpc=request['command'])
+
+ except Exception as e:
raise ncerror.BadMsg(rpc_node)
- rpc_method = rpc_method[0]
-
- rpc_name = rpc_method.tag.replace(qmap('nc'), "")
-
- log.info("rpc-request", rpc=rpc_name)
- class_handler = self.rpc_class_handlers.get(rpc_name, None)
- if class_handler is not None:
- return class_handler(rpc_node, rpc_method, None, grpc_channel,
- session)
-
- log.error("rpc-not-implemented", rpc=rpc_name)
-
rpc_class_handlers = {
'getvoltha': GetVoltha,
'get-config': GetConfig,
@@ -109,13 +146,3 @@
if RpcFactory.instance == None:
RpcFactory.instance = RpcFactory()
return RpcFactory.instance
-
-
-if __name__ == '__main__':
- fac = get_rpc_factory_instance()
- fac.register_rpc('urn:opencord:params:xml:ns:voltha:ietf-voltha',
- 'VolthaGlobalService', 'GetVoltha', GetVoltha)
- rpc = fac.get_handler('urn:opencord:params:xml:ns:voltha:ietf-voltha',
- 'VolthaGlobalService', 'GetVoltha')
- # rpc = fac.rpc_class_handlers.get('getvoltha', None)
- print rpc(None, None, None, None)
diff --git a/netconf/nc_rpc/rpc_response.py b/netconf/nc_rpc/rpc_response.py
index cdbe167..584e35b 100644
--- a/netconf/nc_rpc/rpc_response.py
+++ b/netconf/nc_rpc/rpc_response.py
@@ -16,6 +16,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+import structlog
+from lxml import etree
+import netconf.nc_common.error as ncerror
+
+log = structlog.get_logger()
+
class RpcResponse():
def __init__(self):
@@ -23,4 +29,144 @@
# if there is an error then the reply_node will contains an Error
# object
self.reply_node = None
- self.close_session = False
\ No newline at end of file
+ self.close_session = False
+
+ def build_xml_response(self, request, voltha_response):
+ if request is None:
+ return
+ voltha_xml_string = etree.tostring(voltha_response)
+
+ # Remove the leading and trailing <root> tags
+ if voltha_xml_string.startswith('<yang>'):
+ voltha_xml_string = voltha_xml_string[len('<yang>'):]
+ if voltha_xml_string.endswith('</yang>'):
+ voltha_xml_string = voltha_xml_string[:-len('</yang>')]
+
+ # Create the xml body as
+ if request.has_key('subclass'):
+ body = ''.join([
+ '<data>',
+ '<',
+ request['class'],
+ ' xmlns="urn:opencord:params:xml:ns:voltha:ietf-voltha">',
+ '<',
+ request['subclass'],
+ '>',
+ voltha_xml_string,
+ '</',
+ request['subclass'],
+ '>',
+ '</',
+ request['class'],
+ '>',
+ '</data>'
+ ])
+ else:
+ body = ''.join([
+ '<data>',
+ '<',
+ request['class'],
+ ' xmlns="urn:opencord:params:xml:ns:voltha:ietf-voltha">',
+ voltha_xml_string,
+ '</',
+ request['class'],
+ '>',
+ '</data>'
+ ])
+
+ return etree.fromstring(body)
+
+ def add_node(self, new_node, tree):
+ if new_node.tag == 'ignore':
+ # We want only sub-elements
+ for elem in list(new_node):
+ tree.append(elem)
+ else:
+ tree.append(new_node)
+
+ def copy_basic_element(self, elm):
+ new_elem = etree.Element(elm.tag)
+ new_elem.text = elm.text
+ return new_elem
+
+ def process_inline_option(self, elem):
+ inline_option = False
+ inline_node_name = None
+ for elm in list(elem):
+ if elm.tag == 'yang_field_option':
+ inline_option = True
+ if elm.tag == 'name':
+ inline_node_name = elm.text
+ if not inline_option:
+ new_elem = etree.Element(elem.tag)
+ return new_elem, elem
+
+ # look for the node with the inline_node_name
+ for elm in list(elem):
+ if elm.tag == inline_node_name:
+ new_elem = etree.Element('ignore')
+ return new_elem, elm
+
+ def process_element(self, elem):
+ attrib = elem.get('type')
+ if (attrib == 'list'):
+ if list(elem) is None:
+ return self.copy_basic_element(elem)
+ new_elem = etree.Element('ignore')
+ for elm in list(elem):
+ elm.tag = elem.tag
+ if elm.get('type') in ['list', 'dict']:
+ self.add_node(self.process_element(elm), new_elem)
+ else:
+ new_elem.append(self.copy_basic_element(elm))
+ return new_elem
+ elif (attrib == 'dict'):
+ # Empty case
+ if list(elem) is None:
+ return self.copy_basic_element(elem)
+
+ # Process field option.
+ new_elem, elem = self.process_inline_option(elem)
+
+ for elm in list(elem):
+ if elm.get('type') in ['list', 'dict']:
+ self.add_node(self.process_element(elm), new_elem)
+ else:
+ new_elem.append(self.copy_basic_element(elm))
+ return new_elem
+ else:
+ return self.copy_basic_element(elem)
+
+ def to_yang_xml(self, from_xml):
+ # Parse from_xml as follows:
+ # 1. Any element having a list attribute shoud have each item move 1 level
+ # up and retag using the parent tag
+ # 2. Any element having a dict attribute and has a <yang_field_option>
+ # sub-element should have all it's items move to teh parent level
+ top = etree.Element('yang')
+ elms = list(from_xml)
+
+ # special case the xml contain a list type
+ if len(elms) == 1:
+ item = elms[0]
+ if item.get('type') == 'list':
+ item.tag = 'ignore'
+ self.add_node(self.process_element(item), top)
+ return top
+
+ # Process normally for all other cases
+ for elm in elms:
+ self.add_node(self.process_element(elm), top)
+
+ return top
+
+ def build_yang_response(self, root, request):
+ try:
+ yang_xml = self.to_yang_xml(root)
+ log.info('yang-xml', yang_xml=etree.tounicode(yang_xml,
+ pretty_print=True))
+ return self.build_xml_response(request, yang_xml)
+ except Exception as e:
+ self.rpc_response.is_error = True
+ self.rpc_response.node = ncerror.BadMsg(request)
+ return