From d266770aa2cb27070d5a8fe267525a08eab0cec1 Mon Sep 17 00:00:00 2001 From: Remington Campbell Date: Thu, 30 Apr 2020 15:26:58 -0700 Subject: [PATCH 1/3] Skeleton DME DN parsing function --- src/cisco_gnmi/nx.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/cisco_gnmi/nx.py b/src/cisco_gnmi/nx.py index 4aab05b..15054e7 100644 --- a/src/cisco_gnmi/nx.py +++ b/src/cisco_gnmi/nx.py @@ -166,3 +166,14 @@ def parse_xpath_to_gnmi_path(self, xpath, origin=None): else: origin = "DME" return super(NXClient, self).parse_xpath_to_gnmi_path(xpath, origin) + + def parse_dn_to_gnmi_path(self, dn, origin="DME"): + """Parses a DME DN to proto.gnmi_pb2.Path.""" + if not isinstance(dn, string_types): + raise Exception("dn must be a string!") + path = proto.gnmi_pb2.Path() + if origin: + if not isinstance(origin, string_types): + raise Exception("origin must be a string!") + path.origin = origin + return None \ No newline at end of file From 8e2e320710624c263a621dcc97261e850d7463ab Mon Sep 17 00:00:00 2001 From: Remington Campbell Date: Tue, 19 May 2020 08:29:42 -0700 Subject: [PATCH 2/3] DME/Separate path handling from OS --- src/cisco_gnmi/__init__.py | 1 + src/cisco_gnmi/client.py | 147 +++++++--------------------- src/cisco_gnmi/nx.py | 29 ++---- src/cisco_gnmi/path.py | 193 +++++++++++++++++++++++++++++++++++++ src/cisco_gnmi/xe.py | 22 ++--- src/cisco_gnmi/xr.py | 45 +++------ 6 files changed, 267 insertions(+), 170 deletions(-) create mode 100644 src/cisco_gnmi/path.py diff --git a/src/cisco_gnmi/__init__.py b/src/cisco_gnmi/__init__.py index 60f494f..ca8b0db 100644 --- a/src/cisco_gnmi/__init__.py +++ b/src/cisco_gnmi/__init__.py @@ -29,5 +29,6 @@ from .nx import NXClient from .xe import XEClient from .builder import ClientBuilder +from . import path __version__ = "1.0.7" diff --git a/src/cisco_gnmi/client.py b/src/cisco_gnmi/client.py index d3caf08..00aa92b 100755 --- a/src/cisco_gnmi/client.py +++ b/src/cisco_gnmi/client.py @@ -29,6 +29,7 @@ from . import proto from . import util +from . import path class Client(object): @@ -251,9 +252,9 @@ def validate_request(request): ) return response_stream - def subscribe_xpaths( + def subscribe_paths( self, - xpath_subscriptions, + path_subscriptions, request_mode="STREAM", sub_mode="SAMPLE", encoding="JSON", @@ -262,7 +263,7 @@ def subscribe_xpaths( heartbeat_interval=None, ): """A convenience wrapper of subscribe() which aids in building of SubscriptionRequest - with request as subscribe SubscriptionList. This method accepts an iterable of simply xpath strings, + with request as subscribe SubscriptionList. This method accepts an iterable of path (xpath-like) strings, dictionaries with Subscription attributes for more granularity, or already built Subscription objects and builds the SubscriptionList. Fields not supplied will be defaulted with the default arguments to the method. @@ -271,9 +272,9 @@ def subscribe_xpaths( Parameters ---------- - xpath_subscriptions : str or iterable of str, dict, Subscription + path_subscriptions : str or iterable of str, dict, Subscription An iterable which is parsed to form the Subscriptions in the SubscriptionList to be passed - to SubscriptionRequest. Strings are parsed as XPaths and defaulted with the default arguments, + to SubscriptionRequest. Strings are parsed as XPath-like and defaulted with the default arguments, dictionaries are treated as dicts of args to pass to the Subscribe init, and Subscription is treated as simply a pre-made Subscription. request_mode : proto.gnmi_pb2.SubscriptionList.Mode, optional @@ -292,7 +293,8 @@ def subscribe_xpaths( SAMPLE will stream the subscription at a regular cadence/interval. [TARGET_DEFINED, ON_CHANGE, SAMPLE] encoding : proto.gnmi_pb2.Encoding, optional - A member of the proto.gnmi_pb2.Encoding enum specifying desired encoding of returned data + A member of the proto.gnmi_pb2.Encoding enum specifying desired encoding of returned data. + Defaults to JSON per specification. [JSON, BYTES, PROTO, ASCII, JSON_IETF] sample_interval : int, optional Default nanoseconds for SAMPLE to occur. @@ -319,18 +321,18 @@ def subscribe_xpaths( "encoding", encoding, "Encoding", proto.gnmi_pb2.Encoding ) if isinstance( - xpath_subscriptions, (string_types, dict, proto.gnmi_pb2.Subscription) + path_subscriptions, (string_types, dict, proto.gnmi_pb2.Subscription) ): - xpath_subscriptions = [xpath_subscriptions] + path_subscriptions = [path_subscriptions] subscriptions = [] - for xpath_subscription in xpath_subscriptions: + for path_subscription in path_subscriptions: subscription = None - if isinstance(xpath_subscription, proto.gnmi_pb2.Subscription): - subscription = xpath_subscription - elif isinstance(xpath_subscription, string_types): + if isinstance(path_subscription, proto.gnmi_pb2.Subscription): + subscription = path_subscription + elif isinstance(path_subscription, string_types): subscription = proto.gnmi_pb2.Subscription() subscription.path.CopyFrom( - self.parse_xpath_to_gnmi_path(xpath_subscription) + self.parse_path_to_gnmi_path(path_subscription) ) subscription.mode = util.validate_proto_enum( "sub_mode", @@ -340,22 +342,22 @@ def subscribe_xpaths( ) if sub_mode == "SAMPLE": subscription.sample_interval = sample_interval - elif isinstance(xpath_subscription, dict): + elif isinstance(path_subscription, dict): subscription_dict = {} - if "path" not in xpath_subscription.keys(): + if "path" not in path_subscription.keys(): raise Exception("path must be specified in dict!") - if isinstance(xpath_subscription["path"], proto.gnmi_pb2.Path): - subscription_dict["path"] = xpath_subscription["path"] - elif isinstance(xpath_subscription["path"], string_types): - subscription_dict["path"] = self.parse_xpath_to_gnmi_path( - xpath_subscription["path"] + if isinstance(path_subscription["path"], proto.gnmi_pb2.Path): + subscription_dict["path"] = path_subscription["path"] + elif isinstance(path_subscription["path"], string_types): + subscription_dict["path"] = self.parse_path_to_gnmi_path( + path_subscription["path"] ) else: raise Exception("path must be string or Path proto!") sub_mode_name = ( sub_mode - if "mode" not in xpath_subscription.keys() - else xpath_subscription["mode"] + if "mode" not in path_subscription.keys() + else path_subscription["mode"] ) subscription_dict["mode"] = util.validate_proto_enum( "sub_mode", @@ -366,16 +368,16 @@ def subscribe_xpaths( if sub_mode_name == "SAMPLE": subscription_dict["sample_interval"] = ( sample_interval - if "sample_interval" not in xpath_subscription.keys() - else xpath_subscription["sample_interval"] + if "sample_interval" not in path_subscription.keys() + else path_subscription["sample_interval"] ) - if "suppress_redundant" in xpath_subscription.keys(): - subscription_dict["suppress_redundant"] = xpath_subscription[ + if "suppress_redundant" in path_subscription.keys(): + subscription_dict["suppress_redundant"] = path_subscription[ "suppress_redundant" ] if sub_mode_name != "TARGET_DEFINED": - if "heartbeat_interval" in xpath_subscription.keys(): - subscription_dict["heartbeat_interval"] = xpath_subscription[ + if "heartbeat_interval" in path_subscription.keys(): + subscription_dict["heartbeat_interval"] = path_subscription[ "heartbeat_interval" ] subscription = proto.gnmi_pb2.Subscription(**subscription_dict) @@ -385,86 +387,11 @@ def subscribe_xpaths( subscription_list.subscription.extend(subscriptions) return self.subscribe([subscription_list]) - def parse_xpath_to_gnmi_path(self, xpath, origin=None): - """Parses an XPath to proto.gnmi_pb2.Path. - This function should be overridden by any child classes for origin logic. - - Effectively wraps the std XML XPath tokenizer and traverses - the identified groups. Parsing robustness needs to be validated. - Probably best to formalize as a state machine sometime. - TODO: Formalize tokenizer traversal via state machine. + def subscribe_xpaths(self, xpath_subscriptions, *args, **kwargs): + """Compatibility with earlier versions. + Use subscribe_paths. """ - if not isinstance(xpath, string_types): - raise Exception("xpath must be a string!") - path = proto.gnmi_pb2.Path() - if origin: - if not isinstance(origin, string_types): - raise Exception("origin must be a string!") - path.origin = origin - curr_elem = proto.gnmi_pb2.PathElem() - in_filter = False - just_filtered = False - curr_key = None - # TODO: Lazy - xpath = xpath.strip("/") - xpath_elements = xpath_tokenizer_re.findall(xpath) - path_elems = [] - for index, element in enumerate(xpath_elements): - # stripped initial /, so this indicates a completed element - if element[0] == "/": - if not curr_elem.name: - raise Exception( - "Current PathElem has no name yet is trying to be pushed to path! Invalid XPath?" - ) - path_elems.append(curr_elem) - curr_elem = proto.gnmi_pb2.PathElem() - continue - # We are entering a filter - elif element[0] == "[": - in_filter = True - continue - # We are exiting a filter - elif element[0] == "]": - in_filter = False - continue - # If we're not in a filter then we're a PathElem name - elif not in_filter: - curr_elem.name = element[1] - # Skip blank spaces - elif not any([element[0], element[1]]): - continue - # If we're in the filter and just completed a filter expr, - # "and" as a junction should just be ignored. - elif in_filter and just_filtered and element[1] == "and": - just_filtered = False - continue - # Otherwise we're in a filter and this term is a key name - elif curr_key is None: - curr_key = element[1] - continue - # Otherwise we're an operator or the key value - elif curr_key is not None: - # I think = is the only possible thing to support with PathElem syntax as is - if element[0] in [">", "<"]: - raise Exception("Only = supported as filter operand!") - if element[0] == "=": - continue - else: - # We have a full key here, put it in the map - if curr_key in curr_elem.key.keys(): - raise Exception("Key already in key map!") - curr_elem.key[curr_key] = element[0].strip("'\"") - curr_key = None - just_filtered = True - # Keys/filters in general should be totally cleaned up at this point. - if curr_key: - raise Exception("Hanging key filter! Incomplete XPath?") - # If we have a dangling element that hasn't been completed due to no - # / element then let's just append the final element. - if curr_elem: - path_elems.append(curr_elem) - curr_elem = None - if any([curr_elem, curr_key, in_filter]): - raise Exception("Unfinished elements in XPath parsing!") - path.elem.extend(path_elems) - return path + return self.subscribe_paths(xpath_subscriptions, *args, **kwargs) + + def parse_path_to_gnmi_path(self, path, origin=None): + return path.parse_path_to_gnmi_path(path, origin) diff --git a/src/cisco_gnmi/nx.py b/src/cisco_gnmi/nx.py index 15054e7..212794b 100644 --- a/src/cisco_gnmi/nx.py +++ b/src/cisco_gnmi/nx.py @@ -59,9 +59,9 @@ def get(self, *args, **kwargs): def set(self, *args, **kwargs): raise NotImplementedError("Set not yet supported on NX-OS!") - def subscribe_xpaths( + def subscribe_paths( self, - xpath_subscriptions, + path_subscriptions, request_mode="STREAM", sub_mode="SAMPLE", encoding="PROTO", @@ -138,8 +138,8 @@ def subscribe_xpaths( subset=supported_sub_modes, return_name=True, ) - return super(NXClient, self).subscribe_xpaths( - xpath_subscriptions, + return super(NXClient, self).subscribe_paths( + path_subscriptions, request_mode, sub_mode, encoding, @@ -148,32 +148,21 @@ def subscribe_xpaths( heartbeat_interval, ) - def parse_xpath_to_gnmi_path(self, xpath, origin=None): + def parse_path_to_gnmi_path(self, path, origin=None): """Attempts to determine whether origin should be YANG (device) or DME. Errors on OpenConfig until support is present. """ - if xpath.startswith("openconfig"): + if path.startswith("openconfig"): raise NotImplementedError( "OpenConfig data models not yet supported on NX-OS!" ) if origin is None: if any( - map(xpath.startswith, ["Cisco-NX-OS-device", "/Cisco-NX-OS-device"]) + map(path.startswith, ["Cisco-NX-OS-device", "/Cisco-NX-OS-device"]) ): origin = "device" # Remove the module - xpath = xpath.split(":", 1)[1] + path = path.split(":", 1)[1] else: origin = "DME" - return super(NXClient, self).parse_xpath_to_gnmi_path(xpath, origin) - - def parse_dn_to_gnmi_path(self, dn, origin="DME"): - """Parses a DME DN to proto.gnmi_pb2.Path.""" - if not isinstance(dn, string_types): - raise Exception("dn must be a string!") - path = proto.gnmi_pb2.Path() - if origin: - if not isinstance(origin, string_types): - raise Exception("origin must be a string!") - path.origin = origin - return None \ No newline at end of file + return super(NXClient, self).parse_path_to_gnmi_path(path, origin) diff --git a/src/cisco_gnmi/path.py b/src/cisco_gnmi/path.py new file mode 100644 index 0000000..fea768d --- /dev/null +++ b/src/cisco_gnmi/path.py @@ -0,0 +1,193 @@ +from xml.etree.ElementPath import xpath_tokenizer_re +from six import string_types + +from . import proto + +def parse_path_to_gnmi_path(path, origin=None): + """Indirection method for parsing gNMI paths based upon + origin. + """ + gnmi_path = None + if origin == "DME": + gnmi_path = parse_dn_to_gnmi_path(path) + else: + gnmi_path = parse_xpath_to_gnmi_path(path) + return gnmi_path + +def parse_xpath_to_gnmi_path(xpath, origin=None): + """Parses an XPath to proto.gnmi_pb2.Path. + + Effectively wraps the std XML XPath tokenizer and traverses + the identified groups. Parsing robustness needs to be validated. + Probably best to formalize as a state machine sometime. + TODO: Formalize tokenizer traversal via state machine. + """ + if not isinstance(xpath, string_types): + raise Exception("xpath must be a string!") + path = proto.gnmi_pb2.Path() + if origin: + if not isinstance(origin, string_types): + raise Exception("origin must be a string!") + path.origin = origin + curr_elem = proto.gnmi_pb2.PathElem() + in_filter = False + just_filtered = False + curr_key = None + # TODO: Lazy + xpath = xpath.strip("/") + xpath_elements = xpath_tokenizer_re.findall(xpath) + path_elems = [] + for index, element in enumerate(xpath_elements): + # stripped initial /, so this indicates a completed element + if element[0] == "/": + if not curr_elem.name: + raise Exception( + "Current PathElem has no name yet is trying to be pushed to path! Invalid XPath?" + ) + path_elems.append(curr_elem) + curr_elem = proto.gnmi_pb2.PathElem() + continue + # We are entering a filter + elif element[0] == "[": + in_filter = True + continue + # We are exiting a filter + elif element[0] == "]": + in_filter = False + continue + # If we're not in a filter then we're a PathElem name + elif not in_filter: + curr_elem.name = element[1] + # Skip blank spaces + elif not any([element[0], element[1]]): + continue + # If we're in the filter and just completed a filter expr, + # "and" as a junction should just be ignored. + elif in_filter and just_filtered and element[1] == "and": + just_filtered = False + continue + # Otherwise we're in a filter and this term is a key name + elif curr_key is None: + curr_key = element[1] + continue + # Otherwise we're an operator or the key value + elif curr_key is not None: + # I think = is the only possible thing to support with PathElem syntax as is + if element[0] in [">", "<"]: + raise Exception("Only = supported as filter operand!") + if element[0] == "=": + continue + else: + # We have a full key here, put it in the map + if curr_key in curr_elem.key.keys(): + raise Exception("Key already in key map!") + curr_elem.key[curr_key] = element[0].strip("'\"") + curr_key = None + just_filtered = True + # Keys/filters in general should be totally cleaned up at this point. + if curr_key: + raise Exception("Hanging key filter! Incomplete XPath?") + # If we have a dangling element that hasn't been completed due to no + # / element then let's just append the final element. + if curr_elem: + path_elems.append(curr_elem) + curr_elem = None + if any([curr_elem, curr_key, in_filter]): + raise Exception("Unfinished elements in XPath parsing!") + path.elem.extend(path_elems) + return path + + +def parse_dn_to_gnmi_path(dn, origin="DME"): + """Parses a DME DN to proto.gnmi_pb2.Path. + NX-OS oriented. + """ + if not isinstance(dn, string_types): + raise Exception("DN must be a string!") + path = proto.gnmi_pb2.Path() + if origin: + if not isinstance(origin, string_types): + raise Exception("Origin must be a string!") + path.origin = origin + curr_elem = proto.gnmi_pb2.PathElem() + in_filter = False + just_filtered = False + curr_key = None + curr_key_operator_found = False + curr_key_val = None + # TODO: Lazy + dn = dn.strip("/") + dn_elements = xpath_tokenizer_re.findall(dn) + path_elems = [] + for index, element in enumerate(dn_elements): + # stripped initial /, so this indicates a completed element + if element[0] == "/": + if not curr_elem.name: + raise Exception( + "Current PathElem has no name yet is trying to be pushed to path! Invalid DN?" + ) + path_elems.append(curr_elem) + curr_elem = proto.gnmi_pb2.PathElem() + continue + # We are entering a filter + elif element[0] == "[": + in_filter = True + continue + # We are exiting a filter + elif element[0] == "]": + curr_elem.key[curr_key] = curr_key_val + curr_key = None + curr_key_operator_found = False + curr_key_val = None + in_filter = False + continue + # If we're not in a filter then we're a PathElem name + elif not in_filter: + curr_elem.name = element[1] + # Skip blank spaces + elif not any([element[0], element[1]]): + continue + # Otherwise we're in a filter and this term is a key name + elif curr_key is None: + curr_key = element[1] + continue + # Otherwise we're an operator or the key value + elif curr_key is not None: + if element[0] == "=" and not curr_key_operator_found: + curr_key_operator_found = True + continue + elif curr_key_operator_found: + if not curr_key_val: + curr_key_val = "" + if element[0]: + curr_key_val += element[0] + if element[1]: + curr_key_val += element[1] + else: + raise Exception("Entered unexpected DN key state!") + # Keys/filters in general should be totally cleaned up at this point. + if curr_key: + raise Exception("Hanging key filter! Incomplete DN?") + # If we have a dangling element that hasn't been completed due to no + # / element then let's just append the final element. + if curr_elem: + path_elems.append(curr_elem) + curr_elem = None + if any([curr_elem, curr_key, in_filter]): + raise Exception("Unfinished elements in DN parsing!") + path.elem.extend(path_elems) + return path + +def parse_cli_to_gnmi_path(command): + """Parses a CLI command to proto.gnmi_pb2.Path. + IOS XR appears to be the only OS with this functionality. + + The CLI command becomes a path element. + """ + if not isinstance(command, string_types): + raise Exception("command must be a string!") + path = proto.gnmi_pb2.Path() + curr_elem = proto.gnmi_pb2.PathElem() + curr_elem.name = command + path.elem.extend([curr_elem]) + return path \ No newline at end of file diff --git a/src/cisco_gnmi/xe.py b/src/cisco_gnmi/xe.py index 6c9e525..c7a64d6 100644 --- a/src/cisco_gnmi/xe.py +++ b/src/cisco_gnmi/xe.py @@ -105,7 +105,7 @@ def delete_xpaths(self, xpaths, prefix=None): xpath = "{prefix}{xpath}".format(prefix=prefix, xpath=xpath) else: xpath = "{prefix}/{xpath}".format(prefix=prefix, xpath=xpath) - paths.append(self.parse_xpath_to_gnmi_path(xpath)) + paths.append(self.parse_path_to_gnmi_path(xpath)) return self.set(deletes=paths) def set_json(self, update_json_configs=None, replace_json_configs=None, ietf=True): @@ -160,7 +160,7 @@ def create_updates(name, configs): raise Exception("config should only target one YANG module!") top_element = next(iter(config.keys())) update = proto.gnmi_pb2.Update() - update.path.CopyFrom(self.parse_xpath_to_gnmi_path(top_element)) + update.path.CopyFrom(self.parse_path_to_gnmi_path(top_element)) config = config.pop(top_element) if ietf: update.val.json_ietf_val = json.dumps(config).encode("utf-8") @@ -202,18 +202,18 @@ def get_xpaths(self, xpaths, data_type="ALL", encoding="JSON_IETF"): ) gnmi_path = None if isinstance(xpaths, (list, set)): - gnmi_path = map(self.parse_xpath_to_gnmi_path, set(xpaths)) + gnmi_path = map(self.parse_path_to_gnmi_path, set(xpaths)) elif isinstance(xpaths, string_types): - gnmi_path = [self.parse_xpath_to_gnmi_path(xpaths)] + gnmi_path = [self.parse_path_to_gnmi_path(xpaths)] else: raise Exception( "xpaths must be a single xpath string or iterable of xpath strings!" ) return self.get(gnmi_path, data_type=data_type, encoding=encoding) - def subscribe_xpaths( + def subscribe_paths( self, - xpath_subscriptions, + path_subscriptions, request_mode="STREAM", sub_mode="SAMPLE", encoding="JSON_IETF", @@ -287,8 +287,8 @@ def subscribe_xpaths( subset=supported_sub_modes, return_name=True, ) - return super(XEClient, self).subscribe_xpaths( - xpath_subscriptions, + return super(XEClient, self).subscribe_paths( + path_subscriptions, request_mode, sub_mode, encoding, @@ -297,15 +297,15 @@ def subscribe_xpaths( heartbeat_interval, ) - def parse_xpath_to_gnmi_path(self, xpath, origin=None): + def parse_path_to_gnmi_path(self, path, origin=None): """Naively tries to intelligently (non-sequitur!) origin Otherwise assume rfc7951 legacy is not considered """ if origin is None: # naive but effective - if ":" not in xpath: + if ":" not in path: origin = "openconfig" else: origin = "rfc7951" - return super(XEClient, self).parse_xpath_to_gnmi_path(xpath, origin) + return super(XEClient, self).parse_path_to_gnmi_path(path, origin) diff --git a/src/cisco_gnmi/xr.py b/src/cisco_gnmi/xr.py index f1bc809..119e2c4 100644 --- a/src/cisco_gnmi/xr.py +++ b/src/cisco_gnmi/xr.py @@ -28,6 +28,7 @@ from six import string_types from .client import Client, proto, util +from .path import parse_cli_to_gnmi_path class XRClient(Client): @@ -102,7 +103,7 @@ def delete_xpaths(self, xpaths, prefix=None): xpath = "{prefix}{xpath}".format(prefix=prefix, xpath=xpath) else: xpath = "{prefix}/{xpath}".format(prefix=prefix, xpath=xpath) - paths.append(self.parse_xpath_to_gnmi_path(xpath)) + paths.append(self.parse_path_to_gnmi_path(xpath)) return self.set(deletes=paths) def set_json(self, update_json_configs=None, replace_json_configs=None, ietf=True): @@ -173,7 +174,7 @@ def create_updates(name, configs): element = top_element_split[1] config = config.pop(top_element) update = proto.gnmi_pb2.Update() - update.path.CopyFrom(self.parse_xpath_to_gnmi_path(element, origin)) + update.path.CopyFrom(self.parse_path_to_gnmi_path(element, origin)) if ietf: update.val.json_ietf_val = json.dumps(config).encode("utf-8") else: @@ -206,9 +207,9 @@ def get_xpaths(self, xpaths, data_type="ALL", encoding="JSON_IETF"): """ gnmi_path = None if isinstance(xpaths, (list, set)): - gnmi_path = map(self.parse_xpath_to_gnmi_path, set(xpaths)) + gnmi_path = map(self.parse_path_to_gnmi_path, set(xpaths)) elif isinstance(xpaths, string_types): - gnmi_path = [self.parse_xpath_to_gnmi_path(xpaths)] + gnmi_path = [self.parse_path_to_gnmi_path(xpaths)] else: raise Exception( "xpaths must be a single xpath string or iterable of xpath strings!" @@ -231,18 +232,18 @@ def get_cli(self, commands): """ gnmi_path = None if isinstance(commands, (list, set)): - gnmi_path = list(map(self.parse_cli_to_gnmi_path, commands)) + gnmi_path = list(map(parse_cli_to_gnmi_path, commands)) elif isinstance(commands, string_types): - gnmi_path = [self.parse_cli_to_gnmi_path(commands)] + gnmi_path = [parse_cli_to_gnmi_path(commands)] else: raise Exception( "commands must be a single CLI command string or iterable of CLI commands as strings!" ) return self.get(gnmi_path, encoding="ASCII") - def subscribe_xpaths( + def subscribe_paths( self, - xpath_subscriptions, + path_subscriptions, request_mode="STREAM", sub_mode="SAMPLE", encoding="PROTO", @@ -260,7 +261,7 @@ def subscribe_xpaths( Parameters ---------- - xpath_subscriptions : str or iterable of str, dict, Subscription + path_subscriptions : str or iterable of str, dict, Subscription An iterable which is parsed to form the Subscriptions in the SubscriptionList to be passed to SubscriptionRequest. Strings are parsed as XPaths and defaulted with the default arguments, dictionaries are treated as dicts of args to pass to the Subscribe init, and Subscription is @@ -324,8 +325,8 @@ def subscribe_xpaths( subset=supported_sub_modes, return_name=True, ) - return super(XRClient, self).subscribe_xpaths( - xpath_subscriptions, + return super(XRClient, self).subscribe_paths( + path_subscriptions, request_mode, sub_mode, encoding, @@ -334,31 +335,17 @@ def subscribe_xpaths( heartbeat_interval, ) - def parse_xpath_to_gnmi_path(self, xpath, origin=None): + def parse_path_to_gnmi_path(self, path, origin=None): """No origin specified implies openconfig Otherwise origin is expected to be the module name """ if origin is None: # naive but effective - if xpath.startswith("openconfig") or ":" not in xpath: + if path.startswith("openconfig") or ":" not in path: # openconfig origin = None else: # module name - origin, xpath = xpath.split(":", 1) + origin, path = path.split(":", 1) origin = origin.strip("/") - return super(XRClient, self).parse_xpath_to_gnmi_path(xpath, origin) - - def parse_cli_to_gnmi_path(self, command): - """Parses a CLI command to proto.gnmi_pb2.Path. - IOS XR appears to be the only OS with this functionality. - - The CLI command becomes a path element. - """ - if not isinstance(command, string_types): - raise Exception("command must be a string!") - path = proto.gnmi_pb2.Path() - curr_elem = proto.gnmi_pb2.PathElem() - curr_elem.name = command - path.elem.extend([curr_elem]) - return path + return super(XRClient, self).parse_path_to_gnmi_path(path, origin) From 852a46126f244d2985f826dee1f72e710291b501 Mon Sep 17 00:00:00 2001 From: Remington Campbell Date: Tue, 19 May 2020 12:07:27 -0700 Subject: [PATCH 3/3] Fix path naming conflict --- src/cisco_gnmi/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cisco_gnmi/client.py b/src/cisco_gnmi/client.py index 00aa92b..bc8a3f8 100755 --- a/src/cisco_gnmi/client.py +++ b/src/cisco_gnmi/client.py @@ -29,7 +29,7 @@ from . import proto from . import util -from . import path +from . import path as path_helper class Client(object): @@ -394,4 +394,4 @@ def subscribe_xpaths(self, xpath_subscriptions, *args, **kwargs): return self.subscribe_paths(xpath_subscriptions, *args, **kwargs) def parse_path_to_gnmi_path(self, path, origin=None): - return path.parse_path_to_gnmi_path(path, origin) + return path_helper.parse_path_to_gnmi_path(path, origin)