From c31369e33a7af155a236225211c64bd4df933ab1 Mon Sep 17 00:00:00 2001 From: miott Date: Mon, 4 May 2020 18:39:15 -0700 Subject: [PATCH 1/5] Add support for get and set in Nexus client. --- src/cisco_gnmi/client.py | 3 + src/cisco_gnmi/nx.py | 142 +++++++++++++++++++++++++++++++++++++-- src/cisco_gnmi/xe.py | 2 + 3 files changed, 143 insertions(+), 4 deletions(-) diff --git a/src/cisco_gnmi/client.py b/src/cisco_gnmi/client.py index 443219f..4bb01b7 100755 --- a/src/cisco_gnmi/client.py +++ b/src/cisco_gnmi/client.py @@ -272,6 +272,7 @@ def subscribe_xpaths( sample_interval=_NS_IN_S * 10, suppress_redundant=False, heartbeat_interval=None, + prefix=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, @@ -330,6 +331,8 @@ def subscribe_xpaths( subscription_list.encoding = util.validate_proto_enum( "encoding", encoding, "Encoding", proto.gnmi_pb2.Encoding ) + if prefix: + subscription_list.prefix.CopyFrom(prefix) if isinstance( xpath_subscriptions, (string_types, dict, proto.gnmi_pb2.Subscription) ): diff --git a/src/cisco_gnmi/nx.py b/src/cisco_gnmi/nx.py index 4aab05b..0c770bb 100644 --- a/src/cisco_gnmi/nx.py +++ b/src/cisco_gnmi/nx.py @@ -52,12 +52,146 @@ class NXClient(Client): >>> capabilities = client.capabilities() >>> print(capabilities) """ + def delete_xpaths(self, xpaths, prefix=None): + """A convenience wrapper for set() which constructs Paths from supplied xpaths + to be passed to set() as the delete parameter. - def get(self, *args, **kwargs): - raise NotImplementedError("Get not yet supported on NX-OS!") + Parameters + ---------- + xpaths : iterable of str + XPaths to specify to be deleted. + If prefix is specified these strings are assumed to be the suffixes. + prefix : str + The XPath prefix to apply to all XPaths for deletion. + + Returns + ------- + set() + """ + if isinstance(xpaths, string_types): + xpaths = [xpaths] + paths = [] + # prefix is not supported on NX yet + prefix = None + for xpath in xpaths: + if prefix: + if prefix.endswith("/") and xpath.startswith("/"): + xpath = "{prefix}{xpath}".format( + prefix=prefix[:-1], xpath=xpath[1:] + ) + elif prefix.endswith("/") or xpath.startswith("/"): + 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)) + return self.set(deletes=paths) + + def set_json(self, update_json_configs=None, replace_json_configs=None, ietf=False, prefix=None): + """A convenience wrapper for set() which assumes JSON payloads and constructs desired messages. + All parameters are optional, but at least one must be present. + + This method expects JSON in the same format as what you might send via the native gRPC interface + with a fully modeled configuration which is then parsed to meet the gNMI implementation. + + Parameters + ---------- + update_json_configs : iterable of JSON configurations, optional + JSON configs to apply as updates. + replace_json_configs : iterable of JSON configurations, optional + JSON configs to apply as replacements. + ietf : bool, optional + Use JSON_IETF vs JSON. + + Returns + ------- + set() + """ + # JSON_IETF and prefix are not supported on NX yet + ietf = False + prefix = None + + if not any([update_json_configs, replace_json_configs]): + raise Exception("Must supply at least one set of configurations to method!") + + def check_configs(name, configs): + if isinstance(configs, string_types): + logger.debug("Handling %s as JSON string.", name) + try: + configs = json.loads(configs) + except: + raise Exception("{name} is invalid JSON!".format(name=name)) + configs = [configs] + elif isinstance(configs, dict): + logger.debug("Handling %s as already serialized JSON object.", name) + configs = [configs] + elif not isinstance(configs, (list, set)): + raise Exception( + "{name} must be an iterable of configs!".format(name=name) + ) + return configs - def set(self, *args, **kwargs): - raise NotImplementedError("Set not yet supported on NX-OS!") + def create_updates(name, configs): + if not configs: + return None + configs = check_configs(name, configs) + updates = [] + for config in configs: + if not isinstance(config, dict): + raise Exception("config must be a JSON object!") + if len(config.keys()) > 1: + 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)) + config = config.pop(top_element) + if ietf: + update.val.json_ietf_val = json.dumps(config).encode("utf-8") + else: + update.val.json_val = json.dumps(config).encode("utf-8") + updates.append(update) + return updates + + updates = create_updates("update_json_configs", update_json_configs) + replaces = create_updates("replace_json_configs", replace_json_configs) + return self.set(prefix=prefix, updates=updates, replaces=replaces) + + def get_xpaths(self, xpaths, data_type="ALL", encoding="JSON_IETF"): + """A convenience wrapper for get() which forms proto.gnmi_pb2.Path from supplied xpaths. + + Parameters + ---------- + xpaths : iterable of str or str + An iterable of XPath strings to request data of + If simply a str, wraps as a list for convenience + data_type : proto.gnmi_pb2.GetRequest.DataType, optional + A direct value or key from the GetRequest.DataType enum + [ALL, CONFIG, STATE, OPERATIONAL] + encoding : proto.gnmi_pb2.GetRequest.Encoding, optional + A direct value or key from the Encoding enum + [JSON, JSON_IETF] + + Returns + ------- + get() + """ + supported_encodings = ["JSON", "JSON_IETF"] + encoding = util.validate_proto_enum( + "encoding", + encoding, + "Encoding", + proto.gnmi_pb2.Encoding, + supported_encodings, + ) + gnmi_path = None + if isinstance(xpaths, (list, set)): + gnmi_path = map(self.parse_xpath_to_gnmi_path, set(xpaths)) + elif isinstance(xpaths, string_types): + gnmi_path = [self.parse_xpath_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( self, diff --git a/src/cisco_gnmi/xe.py b/src/cisco_gnmi/xe.py index 783cd8e..685e0b2 100644 --- a/src/cisco_gnmi/xe.py +++ b/src/cisco_gnmi/xe.py @@ -223,6 +223,7 @@ def subscribe_xpaths( sample_interval=Client._NS_IN_S * 10, suppress_redundant=False, heartbeat_interval=None, + prefix=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, @@ -298,6 +299,7 @@ def subscribe_xpaths( sample_interval, suppress_redundant, heartbeat_interval, + prefix ) def parse_xpath_to_gnmi_path(self, xpath, origin=None): From ef247c83a8d29e6da8a78fe883644a1b7be6395e Mon Sep 17 00:00:00 2001 From: miott Date: Tue, 23 Jun 2020 13:05:12 -0700 Subject: [PATCH 2/5] Adding NX support --- src/cisco_gnmi/builder.py | 55 +++++++++++++++++++++++++++++++++++++++ src/cisco_gnmi/client.py | 1 - src/cisco_gnmi/nx.py | 19 +++++++------- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/cisco_gnmi/builder.py b/src/cisco_gnmi/builder.py index 8fba394..e99732c 100644 --- a/src/cisco_gnmi/builder.py +++ b/src/cisco_gnmi/builder.py @@ -271,6 +271,61 @@ def construct(self): """Constructs and returns the desired Client object. The instance of this class will reset to default values for further building. + Returns + ------- + Client or NXClient or XEClient or XRClient + """ + channel = None + channel_ssl_creds = None + channel_metadata_creds = None + channel_creds = None + channel_ssl_creds = None + if any((self.__root_certificates, self.__private_key, self.__certificate_chain)): + channel_ssl_creds = grpc.ssl_channel_credentials( + self.__root_certificates, self.__private_key, self.__certificate_chain + ) + if self.__username and self.__password: + channel_metadata_creds = grpc.metadata_call_credentials( + CiscoAuthPlugin(self.__username, self.__password) + ) + logging.debug("Using username/password call authentication.") + if channel_ssl_creds and channel_metadata_creds: + channel_creds = grpc.composite_channel_credentials( + channel_ssl_creds, channel_metadata_creds + ) + logging.debug("Using SSL/metadata authentication composite credentials.") + elif channel_ssl_creds: + channel_creds = channel_ssl_creds + logging.debug("Using SSL credentials, no metadata authentication.") + if channel_creds: + if self.__ssl_target_name_override is not False: + if self.__ssl_target_name_override is None: + if not self.__root_certificates: + raise Exception("Deriving override requires root certificate!") + self.__ssl_target_name_override = get_cn_from_cert( + self.__root_certificates + ) + logging.warning( + "Overriding SSL option from certificate could increase MITM susceptibility!" + ) + self.set_channel_option( + "grpc.ssl_target_name_override", self.__ssl_target_name_override + ) + channel = grpc.secure_channel( + self.__target_netloc.netloc, channel_creds, self.__channel_options + ) + else: + channel = grpc.insecure_channel(self.__target_netloc.netloc) + if self.__client_class is None: + self.set_os() + client = self.__client_class(channel) + self._reset() + return client + + def save_construct(self): + """Constructs and returns the desired Client object. + The instance of this class will reset to default values for further building. + Returns ------- Client or NXClient or XEClient or XRClient diff --git a/src/cisco_gnmi/client.py b/src/cisco_gnmi/client.py index 4f7b893..de970cd 100755 --- a/src/cisco_gnmi/client.py +++ b/src/cisco_gnmi/client.py @@ -217,7 +217,6 @@ def set( request.extension.extend(extensions) LOGGER.debug(str(request)) - response = self.service.Set(request) return response diff --git a/src/cisco_gnmi/nx.py b/src/cisco_gnmi/nx.py index ad13def..ff2b3b9 100644 --- a/src/cisco_gnmi/nx.py +++ b/src/cisco_gnmi/nx.py @@ -23,7 +23,7 @@ """Wrapper for NX-OS to simplify usage of gNMI implementation.""" - +import json import logging from six import string_types @@ -159,7 +159,7 @@ def create_updates(name, configs): replaces = create_updates("replace_json_configs", replace_json_configs) return self.set(prefix=prefix, updates=updates, replaces=replaces) - def get_xpaths(self, xpaths, data_type="ALL", encoding="JSON_IETF"): + def get_xpaths(self, xpaths, data_type="ALL", encoding="JSON"): """A convenience wrapper for get() which forms proto.gnmi_pb2.Path from supplied xpaths. Parameters @@ -178,7 +178,7 @@ def get_xpaths(self, xpaths, data_type="ALL", encoding="JSON_IETF"): ------- get() """ - supported_encodings = ["JSON", "JSON_IETF"] + supported_encodings = ["JSON"] encoding = util.validate_proto_enum( "encoding", encoding, @@ -288,19 +288,18 @@ def subscribe_xpaths( def parse_xpath_to_gnmi_path(self, xpath, origin=None): """Attempts to determine whether origin should be YANG (device) or DME. - Errors on OpenConfig until support is present. """ - if xpath.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(xpath.startswith, [ + "Cisco-NX-OS-device", + "/Cisco-NX-OS-device", + "cisco-nx-os-device", + "/Cisco-nx-os-device"]) ): origin = "device" # Remove the module xpath = xpath.split(":", 1)[1] else: - origin = "DME" + origin = "openconfig" return super(NXClient, self).parse_xpath_to_gnmi_path(xpath, origin) From f38fca3c11086ec56c22255f031a1ed2ab4352df Mon Sep 17 00:00:00 2001 From: miott Date: Tue, 7 Jul 2020 13:21:52 -0700 Subject: [PATCH 3/5] Addressing review comments --- src/cisco_gnmi/client.py | 3 +++ src/cisco_gnmi/nx.py | 5 ++++- src/cisco_gnmi/xe.py | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/cisco_gnmi/client.py b/src/cisco_gnmi/client.py index 20eb659..97949c0 100755 --- a/src/cisco_gnmi/client.py +++ b/src/cisco_gnmi/client.py @@ -321,6 +321,9 @@ def subscribe_xpaths( Specifies the maximum allowable silent period in nanoseconds when suppress_redundant is in use. The target should send a value at least once in the period specified. Also applies in ON_CHANGE. + prefix : proto.gnmi_pb2.Path, optional + A common path prepended to all path elements in the message. This reduces message size by + removing redundent path elements. Smaller message == improved thoughput. Returns ------- diff --git a/src/cisco_gnmi/nx.py b/src/cisco_gnmi/nx.py index ff2b3b9..bda7ef3 100644 --- a/src/cisco_gnmi/nx.py +++ b/src/cisco_gnmi/nx.py @@ -105,6 +105,9 @@ def set_json(self, update_json_configs=None, replace_json_configs=None, ietf=Fal JSON configs to apply as replacements. ietf : bool, optional Use JSON_IETF vs JSON. + prefix : proto.gnmi_pb2.Path, optional + A common path prepended to all path elements in the message. This reduces message size by + removing redundent path elements. Smaller message == improved thoughput. Returns ------- @@ -172,7 +175,7 @@ def get_xpaths(self, xpaths, data_type="ALL", encoding="JSON"): [ALL, CONFIG, STATE, OPERATIONAL] encoding : proto.gnmi_pb2.GetRequest.Encoding, optional A direct value or key from the Encoding enum - [JSON, JSON_IETF] + [JSON] is only setting supported at this time Returns ------- diff --git a/src/cisco_gnmi/xe.py b/src/cisco_gnmi/xe.py index 5d3a4da..139f9cd 100644 --- a/src/cisco_gnmi/xe.py +++ b/src/cisco_gnmi/xe.py @@ -266,6 +266,9 @@ def subscribe_xpaths( Specifies the maximum allowable silent period in nanoseconds when suppress_redundant is in use. The target should send a value at least once in the period specified. + prefix : proto.gnmi_pb2.Path, optional + A common path prepended to all path elements in the message. This reduces message size by + removing redundent path elements. Smaller message == improved thoughput. Returns ------- From 7c5cf502c9ac9b4edf0a58e91f9c794a39c2a8d2 Mon Sep 17 00:00:00 2001 From: miott Date: Wed, 8 Jul 2020 11:52:32 -0700 Subject: [PATCH 4/5] Fixed typo in origin checking --- src/cisco_gnmi/nx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cisco_gnmi/nx.py b/src/cisco_gnmi/nx.py index bda7ef3..182b31e 100644 --- a/src/cisco_gnmi/nx.py +++ b/src/cisco_gnmi/nx.py @@ -298,7 +298,7 @@ def parse_xpath_to_gnmi_path(self, xpath, origin=None): "Cisco-NX-OS-device", "/Cisco-NX-OS-device", "cisco-nx-os-device", - "/Cisco-nx-os-device"]) + "/cisco-nx-os-device"]) ): origin = "device" # Remove the module From 576e6f2f7c0f7e23d60e6b59b3edca16b58f4658 Mon Sep 17 00:00:00 2001 From: miott Date: Wed, 8 Jul 2020 15:06:54 -0700 Subject: [PATCH 5/5] Bumping version number --- src/cisco_gnmi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cisco_gnmi/__init__.py b/src/cisco_gnmi/__init__.py index 16a73b6..e0105ed 100644 --- a/src/cisco_gnmi/__init__.py +++ b/src/cisco_gnmi/__init__.py @@ -30,4 +30,4 @@ from .xe import XEClient from .builder import ClientBuilder -__version__ = "1.0.10" +__version__ = "1.0.11"