diff --git a/README.rst b/README.rst index e2e9294..f9b201a 100644 --- a/README.rst +++ b/README.rst @@ -86,6 +86,9 @@ Contributors Dev Log ======= +Release **0.8.14**: +- Number of minor bug fixes and improvements. + Release **0.8.13**: - Number of minor bug fixes and improvements. @@ -430,7 +433,7 @@ Release **0.1.0**: (c)2020-2022, karneliuk.com -.. |version| image:: https://img.shields.io/static/v1?label=latest&message=v0.8.13&color=success +.. |version| image:: https://img.shields.io/static/v1?label=latest&message=v0.8.14&color=success .. _version: https://pypi.org/project/pygnmi/ .. |tag| image:: https://img.shields.io/static/v1?label=status&message=stable&color=success .. _tag: https://pypi.org/project/pygnmi/ diff --git a/pygnmi/__init__.py b/pygnmi/__init__.py index fb7a267..646c041 100644 --- a/pygnmi/__init__.py +++ b/pygnmi/__init__.py @@ -2,4 +2,5 @@ pyGNMI module to manage network devices with gNMI (c)2020-2023, Karneliuk """ -__version__ = "0.8.12" + +__version__ = "0.8.14" diff --git a/pygnmi/arg_parser.py b/pygnmi/arg_parser.py index e9dba5f..de833e4 100644 --- a/pygnmi/arg_parser.py +++ b/pygnmi/arg_parser.py @@ -2,7 +2,6 @@ Module to process arguments used in pygnmi cli """ - # Modules import argparse import re @@ -122,6 +121,13 @@ def parse_args(msg): nargs="+", help="gNMI paths of interest in XPath format, space separated", ) + parser.add_argument( + "--gnmi-prefix", + type=str, + required=False, + default="", + help="gNMI prefix, which is shared across multiple paths of interest in XPath forma.", + ) parser.add_argument( "--gnmi-path-target", type=str, diff --git a/pygnmi/client.py b/pygnmi/client.py index b6fcec7..9aa992e 100644 --- a/pygnmi/client.py +++ b/pygnmi/client.py @@ -1,6 +1,6 @@ """ pyGNMI module to manage network devices with gNMI -(c)2020-2023, Karneliuk +(c)2020-2024, Karneliuk """ # Modules @@ -108,7 +108,7 @@ def __init__( if "keepalive_time_ms" in kwargs: self.configureKeepalive(**kwargs) - self.__grpc_proxy = os.getenv('grpc_proxy') + self.__grpc_proxy = os.getenv("grpc_proxy") def configureKeepalive( self, @@ -186,14 +186,14 @@ def connect(self, timeout: int = None): # create tunnel to target s.send(f"CONNECT {self.__target[0]}:{self.__target[1]} HTTP/1.0\r\n\r\n".encode()) buf = s.recv(8192) - if buf[9:12] != b'200': + if buf[9:12] != b"200": raise gNMIException(f"Didn't get a 200 from the proxy, instead: {buf})") # upgrade socket to ssl - ignore certifcate errors since we only want # to get the certificate and don't transfer sensitive data ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE - cert = ctx.wrap_socket(s, server_hostname = self.__target[0]).getpeercert(True) + cert = ctx.wrap_socket(s, server_hostname=self.__target[0]).getpeercert(True) ssl_cert = ssl.DER_cert_to_PEM_cert(cert).encode("utf-8") else: ssl_cert = ssl.get_server_certificate((self.__target[0], self.__target[1])).encode("utf-8") @@ -209,9 +209,9 @@ def connect(self, timeout: int = None): # Collect Certificate's Comman Name ssl_cert_common_name = None try: - ssl_cert_common_name = ssl_cert_deserialized.subject.get_attributes_for_oid((x509.oid.NameOID.COMMON_NAME))[ - 0 - ].value + ssl_cert_common_name = ssl_cert_deserialized.subject.get_attributes_for_oid( + (x509.oid.NameOID.COMMON_NAME) + )[0].value except BaseException as err: logger.warning(f"Cannot get Common Name: {err}") @@ -256,7 +256,9 @@ def connect(self, timeout: int = None): # Set up SSL channel credentials if self.__path_key and self.__path_root: - cert = grpc.ssl_channel_credentials(root_certificates=root_cert, private_key=key, certificate_chain=ssl_cert) + cert = grpc.ssl_channel_credentials( + root_certificates=root_cert, private_key=key, certificate_chain=ssl_cert + ) else: cert = grpc.ssl_channel_credentials(ssl_cert) @@ -421,7 +423,9 @@ def get(self, prefix: str = "", path: list = None, target: str = None, datatype: try: pb_datatype = GetRequest.DataType.Value(datatype.upper()) except ValueError: - logger.error(f"The GetRequest data type \"{datatype}\" is not within the defined range. Using default type 'all'.") + logger.error( + f"The GetRequest data type \"{datatype}\" is not within the defined range. Using default type 'all'." + ) pb_datatype = 0 # Set Protobuf value for encoding @@ -448,7 +452,9 @@ def get(self, prefix: str = "", path: list = None, target: str = None, datatype: raise gNMIException("Conversion of gNMI paths to the Protobuf format failed", e) try: - gnmi_message_request = GetRequest(prefix=protobuf_prefix, path=protobuf_paths, type=pb_datatype, encoding=pb_encoding) + gnmi_message_request = GetRequest( + prefix=protobuf_prefix, path=protobuf_paths, type=pb_datatype, encoding=pb_encoding + ) debug_gnmi_msg(self.__debug, gnmi_message_request, "gNMI request") gnmi_message_response = self.__stub.Get(gnmi_message_request, metadata=self.__metadata) @@ -465,19 +471,25 @@ def get(self, prefix: str = "", path: list = None, target: str = None, datatype: notification_container = {} # Message Notification, Key timestamp - notification_container.update( - {"timestamp": notification.timestamp} - ) if notification.timestamp else notification_container.update({"timestamp": 0}) + ( + notification_container.update({"timestamp": notification.timestamp}) + if notification.timestamp + else notification_container.update({"timestamp": 0}) + ) # Message Notification, Key prefix - notification_container.update( - {"prefix": gnmi_path_degenerator(notification.prefix)} - ) if notification.prefix else notification_container.update({"prefix": None}) + ( + notification_container.update({"prefix": gnmi_path_degenerator(notification.prefix)}) + if notification.prefix + else notification_container.update({"prefix": None}) + ) # Message Notification, Key alias - notification_container.update( - {"alias": notification.alias} - ) if notification.alias else notification_container.update({"alias": None}) + ( + notification_container.update({"alias": notification.alias}) + if notification.alias + else notification_container.update({"alias": None}) + ) # Message Notification, Key atomic notification_container.update({"atomic": notification.atomic}) @@ -490,9 +502,11 @@ def get(self, prefix: str = "", path: list = None, target: str = None, datatype: update_container = {} # Message Update, Key path - update_container.update( - {"path": gnmi_path_degenerator(update_msg.path)} - ) if update_msg.path else update_container.update({"path": None}) + ( + update_container.update({"path": gnmi_path_degenerator(update_msg.path)}) + if update_msg.path + else update_container.update({"path": None}) + ) # Message Update, Key val if update_msg.HasField("val"): @@ -502,7 +516,9 @@ def get(self, prefix: str = "", path: list = None, target: str = None, datatype: ) elif update_msg.val.HasField("json_val"): - update_container.update({"val": process_potentially_json_value(update_msg.val.json_val)}) + update_container.update( + {"val": process_potentially_json_value(update_msg.val.json_val)} + ) elif update_msg.val.HasField("string_val"): update_container.update({"val": update_msg.val.string_val}) @@ -627,13 +643,18 @@ def set( if replace: paths_to_collect_list.extend([path_tuple[0] for path_tuple in replace]) - pre_change_dict = self.get(prefix=prefix, - path=paths_to_collect_list, - encoding=encoding, - datatype='config') + pre_change_dict = self.get( + prefix=prefix, path=paths_to_collect_list, encoding=encoding, datatype="config" + ) if gnmi_extension: - gnmi_message_request = SetRequest(prefix=protobuf_prefix, delete=del_protobuf_paths, update=update_msg, replace=replace_msg, extension=[gnmi_extension]) + gnmi_message_request = SetRequest( + prefix=protobuf_prefix, + delete=del_protobuf_paths, + update=update_msg, + replace=replace_msg, + extension=[gnmi_extension], + ) else: gnmi_message_request = SetRequest( prefix=protobuf_prefix, delete=del_protobuf_paths, update=update_msg, replace=replace_msg @@ -647,14 +668,18 @@ def set( response = {} # Message SetResponse, Key timestamp - response.update( - {"timestamp": gnmi_message_response.timestamp} - ) if gnmi_message_response.timestamp else response.update({"timestamp": 0}) + ( + response.update({"timestamp": gnmi_message_response.timestamp}) + if gnmi_message_response.timestamp + else response.update({"timestamp": 0}) + ) # Message SetResponse, Key prefix - response.update( - {"prefix": gnmi_path_degenerator(gnmi_message_response.prefix)} - ) if gnmi_message_response.prefix else response.update({"prefix": None}) + ( + response.update({"prefix": gnmi_path_degenerator(gnmi_message_response.prefix)}) + if gnmi_message_response.prefix + else response.update({"prefix": None}) + ) if gnmi_message_response.response: response.update({"response": []}) @@ -663,9 +688,11 @@ def set( response_container = {} # Message UpdateResult, Key path - response_container.update( - {"path": gnmi_path_degenerator(response_entry.path)} - ) if response_entry.path else response_container.update({"path": None}) + ( + response_container.update({"path": gnmi_path_degenerator(response_entry.path)}) + if response_entry.path + else response_container.update({"path": None}) + ) ## Message UpdateResult, Key op if response_entry.op: @@ -684,11 +711,13 @@ def set( ## Adding collection of data for diff before the change if self.__show_diff: - post_change_dict = self.get(path=paths_to_collect_list, encoding=encoding, datatype='config') + post_change_dict = self.get(path=paths_to_collect_list, encoding=encoding, datatype="config") is_printable = True if self.__show_diff == "print" else False - diff_list = diff_openconfig(pre_dict=pre_change_dict, post_dict=post_change_dict, is_printable=is_printable) + diff_list = diff_openconfig( + pre_dict=pre_change_dict, post_dict=post_change_dict, is_printable=is_printable + ) if diff_list and self.__show_diff == "get": return response, diff_list @@ -1084,8 +1113,7 @@ def next(self): Blocks until one is available.""" return self._next_update(timeout=None) - def _next_update(self, timeout=None): - ... + def _next_update(self, timeout=None): ... # Overridden by each concrete class, as they each have slightly different # behaviour around waiting (or not) for a sync_response flag @@ -1203,9 +1231,11 @@ def telemetryParser(in_message=None, debug: bool = False): if in_message.HasField("update"): response.update({"update": {"update": []}}) - response["update"].update( - {"timestamp": in_message.update.timestamp} - ) if in_message.update.timestamp else in_message.update({"timestamp": 0}) + ( + response["update"].update({"timestamp": in_message.update.timestamp}) + if in_message.update.timestamp + else in_message.update({"timestamp": 0}) + ) if in_message.update.HasField("prefix"): resource_prefix = [] @@ -1227,9 +1257,11 @@ def telemetryParser(in_message=None, debug: bool = False): update_container = {} # Message Update, Key path - update_container.update( - {"path": gnmi_path_degenerator(update_msg.path)} - ) if update_msg.path else update_container.update({"path": None}) + ( + update_container.update({"path": gnmi_path_degenerator(update_msg.path)}) + if update_msg.path + else update_container.update({"path": None}) + ) if update_msg.val: if update_msg.val.HasField("json_ietf_val"): diff --git a/pygnmi/create_gnmi_extension.py b/pygnmi/create_gnmi_extension.py index 09ed72c..8ec702c 100644 --- a/pygnmi/create_gnmi_extension.py +++ b/pygnmi/create_gnmi_extension.py @@ -1,5 +1,6 @@ """This module contains the converter of dict() to Extension() GRPC class -(c)2019-2022, karneliuk.com""" +(c)2019-2024, karneliuk.com""" + # Modules import datetime from pygnmi.spec.v080.gnmi_ext_pb2 import Extension @@ -59,7 +60,7 @@ def _get_time_ns_epoch(time_in_question) -> int: result = int(time_stamp.timestamp() * pow(10, 9)) except Exception as err: - err.args += (f"Time conversion error: cannot convert {time_in_question}, use 'yyyy-mm-ddTHH:MM:SSZ' format. Details: {err}") + err.args += f"Time conversion error: cannot convert {time_in_question}, use 'yyyy-mm-ddTHH:MM:SSZ' format. Details: {err}" raise return result diff --git a/pygnmi/create_gnmi_path.py b/pygnmi/create_gnmi_path.py index 6338003..dea59e1 100644 --- a/pygnmi/create_gnmi_path.py +++ b/pygnmi/create_gnmi_path.py @@ -1,14 +1,14 @@ """This module contains the converter of str() to GNMI Path class. -#(c)2019-2022, karneliuk.com +#(c)2019-202$, karneliuk.com """ + # Modules import re from pygnmi.spec.v080.gnmi_pb2 import Path # User-defined functions -def gnmi_path_generator(path_in_question: str, - target: str = None): +def gnmi_path_generator(path_in_question: str, target: str = None): """Parses an XPath expression into a gNMI Path Accepted syntaxes: @@ -27,36 +27,36 @@ def gnmi_path_generator(path_in_question: str, """ gnmi_path = Path() keys = [] - temp_path = '' - temp_non_modified = '' + temp_path = "" + temp_non_modified = "" if target: gnmi_path.target = target # Subtracting all the keys from the elements and storing them separately if path_in_question: - if re.match(r'.*?\[.+?=.*?\].*?', path_in_question): - split_list = re.findall(r'.*?\[.+?=.*?\].*?', path_in_question) + if re.match(r".*?\[.+?=.*?\].*?", path_in_question): + split_list = re.findall(r".*?\[.+?=.*?\].*?", path_in_question) for sle in split_list: temp_non_modified += sle - temp_key, temp_value = re.sub(r'.*?\[(.+?)\].*?', r'\g<1>', sle).split('=') + temp_key, temp_value = re.sub(r".*?\[(.+?)\].*?", r"\g<1>", sle).split("=") keys.append({temp_key: temp_value}) - sle = re.sub(r'(.*?\[).+?(\].*?)', fr'\g<1>{len(keys) - 1}\g<2>', sle) + sle = re.sub(r"(.*?\[).+?(\].*?)", rf"\g<1>{len(keys) - 1}\g<2>", sle) temp_path += sle if len(temp_non_modified) < len(path_in_question): - temp_path += path_in_question.replace(temp_non_modified, '') + temp_path += path_in_question.replace(temp_non_modified, "") path_in_question = temp_path - path_elements = path_in_question.split('/') + path_elements = path_in_question.split("/") path_elements = list(filter(None, path_elements)) # Check if first path element contains a colon, and use that to set origin - if path_elements and re.match('.+?:.*?', path_elements[0]): + if path_elements and re.match(".+?:.*?", path_elements[0]): pe_entry = path_elements[0] - parts = pe_entry.split(':', 1) + parts = pe_entry.split(":", 1) gnmi_path.origin = parts[0] if len(parts) > 1 and parts[1]: @@ -65,9 +65,9 @@ def gnmi_path_generator(path_in_question: str, del path_elements[0] for pe_entry in path_elements: - if re.match(r'.+?\[\d+?\]', pe_entry): + if re.match(r".+?\[\d+?\]", pe_entry): element_keys = {} - path_info = [re.sub(']', '', en) for en in pe_entry.split('[')] + path_info = [re.sub("]", "", en) for en in pe_entry.split("[")] element = path_info.pop(0) for elem_key in path_info: @@ -82,22 +82,21 @@ def gnmi_path_generator(path_in_question: str, def gnmi_path_degenerator(gnmi_path) -> str: - """Parses a gNMI Path into an XPath expression - """ + """Parses a gNMI Path into an XPath expression""" result = None if gnmi_path and gnmi_path.elem: resource_path = [] for path_elem in gnmi_path.elem: - temp_path = '' + temp_path = "" if path_elem.name: temp_path += path_elem.name if path_elem.key: for pk_name, pk_value in sorted(path_elem.key.items()): - temp_path += f'[{pk_name}={pk_value}]' + temp_path += f"[{pk_name}={pk_value}]" resource_path.append(temp_path) - result = '/'.join(resource_path) + result = "/".join(resource_path) return result diff --git a/pygnmi/tools.py b/pygnmi/tools.py index d318425..fe0cada 100644 --- a/pygnmi/tools.py +++ b/pygnmi/tools.py @@ -1,4 +1,4 @@ -#(c)2019-2021, karneliuk.com +# (c)2019-2024, karneliuk.com # Modules import re @@ -7,18 +7,16 @@ # User-defined functions -def _dict_to_xpath(input_dict: dict, - parent_type_class: type, - pre_parent_type_class: type = dict) -> list: +def _dict_to_xpath(input_dict: dict, parent_type_class: type, pre_parent_type_class: type = dict) -> list: result = [] unique_id = "" if isinstance(input_dict, dict): for k1, v1 in input_dict.items(): if isinstance(v1, dict): - next_level_list = _dict_to_xpath(input_dict=v1, - parent_type_class=type(v1), - pre_parent_type_class=type(input_dict)) + next_level_list = _dict_to_xpath( + input_dict=v1, parent_type_class=type(v1), pre_parent_type_class=type(input_dict) + ) for nested_list in next_level_list: result.append(["/" + k1 + nested_list[0], nested_list[1]]) @@ -26,10 +24,10 @@ def _dict_to_xpath(input_dict: dict, elif isinstance(v1, list): for v2 in v1: if isinstance(v2, dict): - next_level_list = _dict_to_xpath(input_dict=v2, - parent_type_class=type(v2), - pre_parent_type_class=type(v1)) - + next_level_list = _dict_to_xpath( + input_dict=v2, parent_type_class=type(v2), pre_parent_type_class=type(v1) + ) + for nested_list in next_level_list: result.append(["/" + k1 + nested_list[0], nested_list[1]]) @@ -37,7 +35,7 @@ def _dict_to_xpath(input_dict: dict, if pre_parent_type_class == list and parent_type_class == dict: unique_id = f"[{k1}={v1}]" - result.append(["/" + k1, v1]) + result.append(["/" + k1, v1]) if unique_id: result = [[unique_id + nl[0], nl[1]] for nl in result] @@ -47,7 +45,7 @@ def _dict_to_xpath(input_dict: dict, def _get_unique_id(path_list: list, temp_elem: dict) -> str: result = "" - + for path_item in path_list: temp_elem = temp_elem[path_item] @@ -104,7 +102,7 @@ def diff_openconfig(pre_dict: dict, post_dict: dict, is_printable: bool = True) pre_path_list.append(entry_tuple[1].pop(0)) pre_path_list.append(entry_tuple[1].pop(0)) - + for elem in entry_tuple[1]: pre_path_list.append(elem) @@ -121,16 +119,32 @@ def diff_openconfig(pre_dict: dict, post_dict: dict, is_printable: bool = True) pre_path_list.append(elem_list[0]) if entry_tuple[0] == "remove": - result_list = _dict_to_xpath(input_dict=elem_list[-1], - parent_type_class=type(elem_list[-1])) - result_list = [["-", xpath_str + _get_unique_id(path_list=pre_path_list, - temp_elem=pre_dict) + nl[0], nl[1]] for nl in result_list] + result_list = _dict_to_xpath( + input_dict=elem_list[-1], parent_type_class=type(elem_list[-1]) + ) + result_list = [ + [ + "-", + xpath_str + _get_unique_id(path_list=pre_path_list, temp_elem=pre_dict) + nl[0], + nl[1], + ] + for nl in result_list + ] elif entry_tuple[0] == "add": - result_list = _dict_to_xpath(input_dict=elem_list[-1], - parent_type_class=type(elem_list[-1])) - result_list = [["+", xpath_str + _get_unique_id(path_list=pre_path_list, - temp_elem=post_dict) + nl[0], nl[1]] for nl in result_list] + result_list = _dict_to_xpath( + input_dict=elem_list[-1], parent_type_class=type(elem_list[-1]) + ) + result_list = [ + [ + "+", + xpath_str + + _get_unique_id(path_list=pre_path_list, temp_elem=post_dict) + + nl[0], + nl[1], + ] + for nl in result_list + ] else: result_list.append(["-", xpath_str, entry_tuple[2][0]]) @@ -143,21 +157,24 @@ def diff_openconfig(pre_dict: dict, post_dict: dict, is_printable: bool = True) if update_msg_list[0] == "update": if len(update_msg_list) > 1 and isinstance(update_msg_list[1], list): for update_content_dict in update_msg_list[1]: - if "path" in update_content_dict: + if "path" in update_content_dict: xpath_str += "/" + update_content_dict["path"] if "val" in update_content_dict: - result_list = _dict_to_xpath(input_dict=update_content_dict["val"], parent_type_class=type(update_content_dict["val"])) + result_list = _dict_to_xpath( + input_dict=update_content_dict["val"], + parent_type_class=type(update_content_dict["val"]), + ) result_list = [["+", xpath_str + nl[0], nl[1]] for nl in result_list] if result_list: - result.extend(result_list) + result.extend(result_list) if is_printable: for result_nested_list in result: print_str = " ".join([str(tr) for tr in result_nested_list]) - if re.match(r'^\+', print_str): + if re.match(r"^\+", print_str): print("\33[92m" + print_str + "\33[0m") else: diff --git a/scripts/pygnmicli b/scripts/pygnmicli index 30ef40c..a630369 100644 --- a/scripts/pygnmicli +++ b/scripts/pygnmicli @@ -1,5 +1,5 @@ #!/usr/bin/env python -# (c)2019-2022, karneliuk.com +# (c)2019-2024, karneliuk.com # Modules @@ -8,11 +8,13 @@ import logging import os import sys + # Own modules from pygnmi.arg_parser import parse_args from pygnmi.client import gNMIclient from pygnmi.artefacts.messages import msg + # Variables path_log = "log/execution.log" @@ -62,7 +64,13 @@ def main(): elif args.operation == "get": print(f"Doing {args.operation} request to {args.target}...") - result = GC.get(path=args.gnmi_path, datatype=args.datastore, encoding=args.encoding, target=args.gnmi_path_target) + result = GC.get( + prefix=args.gnmi_prefix, + path=args.gnmi_path, + datatype=args.datastore, + encoding=args.encoding, + target=args.gnmi_path_target, + ) elif args.operation.startswith("set"): print(f"Doing {args.operation} request to {args.target}...") @@ -82,7 +90,12 @@ def main(): elif args.operation.startswith("subscribe"): mode = args.operation.split("-")[1] subscription_list = [{"path": xpath, "mode": "target_defined"} for xpath in args.gnmi_path] - subscribe = {"subscription": subscription_list, "use_aliases": False, "mode": mode, "encoding": args.encoding} + subscribe = { + "subscription": subscription_list, + "use_aliases": False, + "mode": mode, + "encoding": args.encoding, + } # Set up extensions if args.ext_history_snapshot_time: diff --git a/setup.py b/setup.py index 6d44286..d721ece 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="pygnmi", packages=["pygnmi", "pygnmi.spec.v080", "pygnmi.artefacts"], - version="0.8.13", + version="0.8.14", license="bsd-3-clause", description="Pure Python gNMI client to manage network functions and collect telemetry.", long_description=long_description,