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/grpc_client/grpc_client.py b/netconf/grpc_client/grpc_client.py
index d65410e..bce6de3 100644
--- a/netconf/grpc_client/grpc_client.py
+++ b/netconf/grpc_client/grpc_client.py
@@ -40,6 +40,17 @@
VolthaGlobalServiceStub
from google.protobuf import empty_pb2
from google.protobuf.json_format import MessageToDict, ParseDict
+from google.protobuf import descriptor
+import base64
+import math
+
+_INT64_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT64,
+ descriptor.FieldDescriptor.CPPTYPE_UINT64])
+_FLOAT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_FLOAT,
+ descriptor.FieldDescriptor.CPPTYPE_DOUBLE])
+_INFINITY = 'Infinity'
+_NEG_INFINITY = '-Infinity'
+_NAN = 'NaN'
log = get_logger()
@@ -281,17 +292,21 @@
service_method = key.split('-')
service = service_method[0]
method = service_method[1]
- stub = None
- # if service == 'VolthaGlobalService':
- # stub = VolthaGlobalServiceStub
- # elif service == 'VolthaLocalService':
- # stub = VolthaLocalServiceStub
- # else:
- # raise # Exception
+ if service == 'VolthaGlobalService':
+ stub = VolthaGlobalServiceStub
+ elif service == 'VolthaLocalService':
+ stub = VolthaLocalServiceStub
+ else:
+ raise # Exception
+
+ log.info('voltha-rpc', service=service, method=method, req=req,
+ depth=depth)
res, metadata = yield self.invoke(stub, method, req, depth)
- returnValue(MessageToDict(res, True, True))
+ # returnValue(MessageToDict(res, True, True))
+ returnValue(self.convertToDict(res))
+
except Exception, e:
log.error('failure', exception=repr(e))
@@ -339,3 +354,135 @@
log.exception(e)
raise e
+
+ # Below is an adaptation of Google's MessageToDict() which includes
+ # protobuf options extensions
+
+ class Error(Exception):
+ """Top-level module error for json_format."""
+
+ class SerializeToJsonError(Error):
+ """Thrown if serialization to JSON fails."""
+
+ def _IsMapEntry(self, field):
+ return (field.type == descriptor.FieldDescriptor.TYPE_MESSAGE and
+ field.message_type.has_options and
+ field.message_type.GetOptions().map_entry)
+
+ def convertToDict(self, message):
+ """Converts message to an object according to Proto3 JSON Specification."""
+
+ js = {}
+ return self._RegularMessageToJsonObject(message, js)
+
+ def get_yang_option(self, field):
+ opt = field.GetOptions()
+ yang_opt = {}
+ for fd, val in opt.ListFields():
+ if fd.full_name == 'voltha.yang_inline_node':
+ yang_opt['id'] = val.id
+ yang_opt['type'] = val.type
+ # Fow now, a max of 1 yang option is set per field
+ return yang_opt
+
+ def _RegularMessageToJsonObject(self, message, js):
+ """Converts normal message according to Proto3 JSON Specification."""
+ fields = message.ListFields()
+
+ try:
+ for field, value in fields:
+ # Check for options
+ yang_opt = self.get_yang_option(field)
+
+ name = field.name
+ if self._IsMapEntry(field):
+ # Convert a map field.
+ v_field = field.message_type.fields_by_name['value']
+ js_map = {}
+ for key in value:
+ if isinstance(key, bool):
+ if key:
+ recorded_key = 'true'
+ else:
+ recorded_key = 'false'
+ else:
+ recorded_key = key
+ js_map[recorded_key] = self._FieldToJsonObject(
+ v_field, value[key])
+ js[name] = js_map
+ elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED:
+ # Convert a repeated field.
+ js[name] = [self._FieldToJsonObject(field, k)
+ for k in value]
+ else:
+ # This specific yang option applies only to non-repeated
+ # fields
+ if yang_opt: # Create a map
+ js_map = {}
+ js_map['yang_field_option'] = True
+ js_map['yang_field_option_id'] = yang_opt['id']
+ js_map['yang_field_option_type'] = yang_opt['type']
+ js_map['name'] = name
+ js_map[name] = self._FieldToJsonObject(field, value)
+ js[name] = js_map
+ else:
+ js[name] = self._FieldToJsonObject(field, value)
+
+ # Serialize default value if including_default_value_fields is True.
+ message_descriptor = message.DESCRIPTOR
+ for field in message_descriptor.fields:
+ # Singular message fields and oneof fields will not be affected.
+ if ((
+ field.label != descriptor.FieldDescriptor.LABEL_REPEATED and
+ field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE) or
+ field.containing_oneof):
+ continue
+ name = field.name
+ if name in js:
+ # Skip the field which has been serailized already.
+ continue
+ if self._IsMapEntry(field):
+ js[name] = {}
+ elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED:
+ js[name] = []
+ else:
+ js[name] = self._FieldToJsonObject(field,
+ field.default_value)
+
+ except ValueError as e:
+ raise self.SerializeToJsonError(
+ 'Failed to serialize {0} field: {1}.'.format(field.name, e))
+
+ return js
+
+ def _FieldToJsonObject(self, field, value):
+ """Converts field value according to Proto3 JSON Specification."""
+ if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE:
+ return self.convertToDict(value)
+ elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_ENUM:
+ enum_value = field.enum_type.values_by_number.get(value, None)
+ if enum_value is not None:
+ return enum_value.name
+ else:
+ raise self.SerializeToJsonError('Enum field contains an '
+ 'integer value '
+ 'which can not mapped to an enum value.')
+ elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_STRING:
+ if field.type == descriptor.FieldDescriptor.TYPE_BYTES:
+ # Use base64 Data encoding for bytes
+ return base64.b64encode(value).decode('utf-8')
+ else:
+ return value
+ elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_BOOL:
+ return bool(value)
+ elif field.cpp_type in _INT64_TYPES:
+ return str(value)
+ elif field.cpp_type in _FLOAT_TYPES:
+ if math.isinf(value):
+ if value < 0.0:
+ return _NEG_INFINITY
+ else:
+ return _INFINITY
+ if math.isnan(value):
+ return _NAN
+ return value