|
31 | 31 | import os
|
32 | 32 | from six import string_types
|
33 | 33 |
|
34 |
| -from . import proto |
35 |
| -from . import util |
| 34 | +from cisco_gnmi import proto |
| 35 | +from cisco_gnmi import util |
36 | 36 |
|
37 | 37 |
|
38 | 38 | class Client(object):
|
@@ -257,6 +257,84 @@ def validate_request(request):
|
257 | 257 | )
|
258 | 258 | return response_stream
|
259 | 259 |
|
| 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 | + |
260 | 338 | def parse_xpath_to_gnmi_path(self, xpath, origin=None):
|
261 | 339 | """Parses an XPath to proto.gnmi_pb2.Path.
|
262 | 340 | This function should be overridden by any child classes for origin logic.
|
@@ -540,3 +618,230 @@ def get_payload(self, configs):
|
540 | 618 | )
|
541 | 619 | )
|
542 | 620 | 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