Skip to content

Commit d513ad2

Browse files
committed
Merge branch 'develop'
2 parents 9bd139a + 53aed63 commit d513ad2

File tree

4 files changed

+378
-284
lines changed

4 files changed

+378
-284
lines changed

src/cisco_gnmi/client.py

Lines changed: 307 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
import os
3232
from six import string_types
3333

34-
from . import proto
35-
from . import util
34+
from cisco_gnmi import proto
35+
from cisco_gnmi import util
3636

3737

3838
class Client(object):
@@ -257,6 +257,84 @@ def validate_request(request):
257257
)
258258
return response_stream
259259

260+
def check_configs(self, configs):
261+
if isinstance(configs, string_types):
262+
logger.debug("Handling as JSON string.")
263+
try:
264+
configs = json.loads(configs)
265+
except:
266+
raise Exception("{0}\n is invalid JSON!".format(configs))
267+
configs = [configs]
268+
elif isinstance(configs, dict):
269+
logger.debug("Handling already serialized JSON object.")
270+
configs = [configs]
271+
elif not isinstance(configs, (list, set)):
272+
raise Exception(
273+
"{0} must be an iterable of configs!".format(str(configs))
274+
)
275+
return configs
276+
277+
def create_updates(self, configs, origin, json_ietf=False):
278+
"""Check configs, and construct "Update" messages.
279+
280+
Parameters
281+
----------
282+
configs: dict of <xpath>: <dict val for JSON>
283+
origin: str [DME, device, openconfig]
284+
json_ietf: bool encoding type for Update val (default False)
285+
286+
Returns
287+
-------
288+
List of Update messages with val populated.
289+
290+
If a set of configs contain a common Xpath, the Update must contain
291+
a consolidation of xpath/values for 2 reasons:
292+
293+
1. Devices may have a restriction on how many Update messages it will
294+
accept at once.
295+
2. Some xpath/values are required to be set in same Update because of
296+
dependencies like leafrefs, mandatory settings, and if/when/musts.
297+
"""
298+
if not configs:
299+
return []
300+
configs = self.check_configs(configs)
301+
302+
xpaths = []
303+
updates = []
304+
for config in configs:
305+
xpath = next(iter(config.keys()))
306+
xpaths.append(xpath)
307+
common_xpath = os.path.commonprefix(xpaths)
308+
309+
if common_xpath:
310+
update_configs = self.get_payload(configs)
311+
for update_cfg in update_configs:
312+
xpath, payload = update_cfg
313+
update = proto.gnmi_pb2.Update()
314+
update.path.CopyFrom(
315+
self.parse_xpath_to_gnmi_path(
316+
xpath, origin=origin
317+
)
318+
)
319+
if json_ietf:
320+
update.val.json_ietf_val = payload
321+
else:
322+
update.val.json_val = payload
323+
updates.append(update)
324+
return updates
325+
else:
326+
for config in configs:
327+
top_element = next(iter(config.keys()))
328+
update = proto.gnmi_pb2.Update()
329+
update.path.CopyFrom(self.parse_xpath_to_gnmi_path(top_element))
330+
config = config.pop(top_element)
331+
if json_ietf:
332+
update.val.json_ietf_val = json.dumps(config).encode("utf-8")
333+
else:
334+
update.val.json_val = json.dumps(config).encode("utf-8")
335+
updates.append(update)
336+
return updates
337+
260338
def parse_xpath_to_gnmi_path(self, xpath, origin=None):
261339
"""Parses an XPath to proto.gnmi_pb2.Path.
262340
This function should be overridden by any child classes for origin logic.
@@ -540,3 +618,230 @@ def get_payload(self, configs):
540618
)
541619
)
542620
return updates
621+
622+
def xml_path_to_path_elem(self, request):
623+
"""Convert XML Path Language 1.0 Xpath to gNMI Path/PathElement.
624+
625+
Modeled after YANG/NETCONF Xpaths.
626+
627+
References:
628+
* https://www.w3.org/TR/1999/REC-xpath-19991116/#location-paths
629+
* https://www.w3.org/TR/1999/REC-xpath-19991116/#path-abbrev
630+
* https://tools.ietf.org/html/rfc6020#section-6.4
631+
* https://tools.ietf.org/html/rfc6020#section-9.13
632+
* https://tools.ietf.org/html/rfc6241
633+
634+
Parameters
635+
---------
636+
request: dict containing request namespace and nodes to be worked on.
637+
namespace: dict of <prefix>: <namespace>
638+
nodes: list of dict
639+
<xpath>: Xpath pointing to resource
640+
<value>: value to set resource to
641+
<edit-op>: equivelant NETCONF edit-config operation
642+
643+
Returns
644+
-------
645+
tuple: namespace_modules, message dict, origin
646+
namespace_modules: dict of <prefix>: <module name>
647+
Needed for future support.
648+
message dict: 4 lists containing possible updates, replaces,
649+
deletes, or gets derived form input nodes.
650+
origin str: DME, device, or openconfig
651+
"""
652+
653+
paths = []
654+
message = {
655+
'update': [],
656+
'replace': [],
657+
'delete': [],
658+
'get': [],
659+
}
660+
if 'nodes' not in request:
661+
# TODO: raw rpc?
662+
return paths
663+
else:
664+
namespace_modules = {}
665+
origin = 'DME'
666+
for prefix, nspace in request.get('namespace', {}).items():
667+
if '/Cisco-IOS-' in nspace:
668+
module = nspace[nspace.rfind('/') + 1:]
669+
elif '/cisco-nx' in nspace: # NXOS lowercases namespace
670+
module = 'Cisco-NX-OS-device'
671+
elif '/openconfig.net' in nspace:
672+
module = 'openconfig-'
673+
module += nspace[nspace.rfind('/') + 1:]
674+
elif 'urn:ietf:params:xml:ns:yang:' in nspace:
675+
module = nspace.replace(
676+
'urn:ietf:params:xml:ns:yang:', '')
677+
if module:
678+
namespace_modules[prefix] = module
679+
680+
for node in request.get('nodes', []):
681+
if 'xpath' not in node:
682+
log.error('Xpath is not in message')
683+
else:
684+
xpath = node['xpath']
685+
value = node.get('value', '')
686+
edit_op = node.get('edit-op', '')
687+
688+
for pfx, ns in namespace_modules.items():
689+
# NXOS does not support prefixes yet so clear them out
690+
if pfx in xpath and 'openconfig' in ns:
691+
origin = 'openconfig'
692+
xpath = xpath.replace(pfx + ':', '')
693+
if isinstance(value, string_types):
694+
value = value.replace(pfx + ':', '')
695+
elif pfx in xpath and 'device' in ns:
696+
origin = 'device'
697+
xpath = xpath.replace(pfx + ':', '')
698+
if isinstance(value, string_types):
699+
value = value.replace(pfx + ':', '')
700+
if edit_op:
701+
if edit_op in ['create', 'merge', 'replace']:
702+
xpath_lst = xpath.split('/')
703+
name = xpath_lst.pop()
704+
xpath = '/'.join(xpath_lst)
705+
if edit_op == 'replace':
706+
if not message['replace']:
707+
message['replace'] = [{
708+
xpath: {name: value}
709+
}]
710+
else:
711+
message['replace'].append(
712+
{xpath: {name: value}}
713+
)
714+
else:
715+
if not message['update']:
716+
message['update'] = [{
717+
xpath: {name: value}
718+
}]
719+
else:
720+
message['update'].append(
721+
{xpath: {name: value}}
722+
)
723+
elif edit_op in ['delete', 'remove']:
724+
if message['delete']:
725+
message['delete'].add(xpath)
726+
else:
727+
message['delete'] = set(xpath)
728+
else:
729+
message['get'].append(xpath)
730+
return namespace_modules, message, origin
731+
732+
733+
if __name__ == '__main__':
734+
from pprint import pprint as pp
735+
import grpc
736+
from cisco_gnmi import Client
737+
from cisco_gnmi.auth import CiscoAuthPlugin
738+
channel = grpc.secure_channel(
739+
'127.0.0.1:9339',
740+
grpc.composite_channel_credentials(
741+
grpc.ssl_channel_credentials(),
742+
grpc.metadata_call_credentials(
743+
CiscoAuthPlugin(
744+
'admin',
745+
'its_a_secret'
746+
)
747+
)
748+
)
749+
)
750+
client = Client(channel)
751+
request = {
752+
'namespace': {
753+
'oc-acl': 'http://openconfig.net/yang/acl'
754+
},
755+
'nodes': [
756+
{
757+
'value': 'testacl',
758+
'xpath': '/oc-acl:acl/oc-acl:acl-sets/oc-acl:acl-set/name',
759+
'edit-op': 'merge'
760+
},
761+
{
762+
'value': 'ACL_IPV4',
763+
'xpath': '/oc-acl:acl/oc-acl:acl-sets/oc-acl:acl-set/type',
764+
'edit-op': 'merge'
765+
},
766+
{
767+
'value': '10',
768+
'xpath': '/oc-acl:acl/oc-acl:acl-sets/oc-acl:acl-set[name="testacl"][type="ACL_IPV4"]/oc-acl:acl-entries/oc-acl:acl-entry/oc-acl:sequence-id',
769+
'edit-op': 'merge'
770+
},
771+
{
772+
'value': '20.20.20.1/32',
773+
'xpath': '/oc-acl:acl/oc-acl:acl-sets/oc-acl:acl-set[name="testacl"][type="ACL_IPV4"]/oc-acl:acl-entries/oc-acl:acl-entry[sequence-id="10"]/oc-acl:ipv4/oc-acl:config/oc-acl:destination-address',
774+
'edit-op': 'merge'
775+
},
776+
{
777+
'value': 'IP_TCP',
778+
'xpath': '/oc-acl:acl/oc-acl:acl-sets/oc-acl:acl-set[name="testacl"][type="ACL_IPV4"]/oc-acl:acl-entries/oc-acl:acl-entry[sequence-id="10"]/oc-acl:ipv4/oc-acl:config/oc-acl:protocol',
779+
'edit-op': 'merge'
780+
},
781+
{
782+
'value': '10.10.10.10/32',
783+
'xpath': '/oc-acl:acl/oc-acl:acl-sets/oc-acl:acl-set[name="testacl"][type="ACL_IPV4"]/oc-acl:acl-entries/oc-acl:acl-entry[sequence-id="10"]/oc-acl:ipv4/oc-acl:config/oc-acl:source-address',
784+
'edit-op': 'merge'
785+
},
786+
{
787+
'value': 'DROP',
788+
'xpath': '/oc-acl:acl/oc-acl:acl-sets/oc-acl:acl-set[name="testacl"][type="ACL_IPV4"]/oc-acl:acl-entries/oc-acl:acl-entry[sequence-id="10"]/oc-acl:actions/oc-acl:config/oc-acl:forwarding-action',
789+
'edit-op': 'merge'
790+
}
791+
]
792+
}
793+
modules, message, origin = client.xpath_to_path_elem(request)
794+
pp(modules)
795+
pp(message)
796+
pp(origin)
797+
"""
798+
# Expected output
799+
=================
800+
{'oc-acl': 'openconfig-acl'}
801+
{'delete': [],
802+
'get': [],
803+
'replace': [],
804+
'update': [{'/acl/acl-sets/acl-set': {'name': 'testacl'}},
805+
{'/acl/acl-sets/acl-set': {'type': 'ACL_IPV4'}},
806+
{'/acl/acl-sets/acl-set[name="testacl"][type="ACL_IPV4"]/acl-entries/acl-entry': {'sequence-id': '10'}},
807+
{'/acl/acl-sets/acl-set[name="testacl"][type="ACL_IPV4"]/acl-entries/acl-entry[sequence-id="10"]/ipv4/config': {'destination-address': '20.20.20.1/32'}},
808+
{'/acl/acl-sets/acl-set[name="testacl"][type="ACL_IPV4"]/acl-entries/acl-entry[sequence-id="10"]/ipv4/config': {'protocol': 'IP_TCP'}},
809+
{'/acl/acl-sets/acl-set[name="testacl"][type="ACL_IPV4"]/acl-entries/acl-entry[sequence-id="10"]/ipv4/config': {'source-address': '10.10.10.10/32'}},
810+
{'/acl/acl-sets/acl-set[name="testacl"][type="ACL_IPV4"]/acl-entries/acl-entry[sequence-id="10"]/actions/config': {'forwarding-action': 'DROP'}}]}
811+
'openconfig'
812+
"""
813+
# Feed converted XML Path Language 1.0 Xpaths to create updates
814+
updates = client.create_updates(message['update'], origin)
815+
pp(updates)
816+
"""
817+
# Expected output
818+
=================
819+
[path {
820+
origin: "openconfig"
821+
elem {
822+
name: "acl"
823+
}
824+
elem {
825+
name: "acl-sets"
826+
}
827+
elem {
828+
name: "acl-set"
829+
key {
830+
key: "name"
831+
value: "testacl"
832+
}
833+
key {
834+
key: "type"
835+
value: "ACL_IPV4"
836+
}
837+
}
838+
elem {
839+
name: "acl-entries"
840+
}
841+
}
842+
val {
843+
json_val: "{\"acl-entry\": [{\"actions\": {\"config\": {\"forwarding-action\": \"DROP\"}}, \"ipv4\": {\"config\": {\"destination-address\": \"20.20.20.1/32\", \"protocol\": \"IP_TCP\", \"source-address\": \"10.10.10.10/32\"}}, \"sequence-id\": \"10\"}]}"
844+
}
845+
]
846+
# update is now ready to be sent through gNMI SetRequest
847+
"""

0 commit comments

Comments
 (0)