From 33c6fc04b7743b787f2d016e3cb9f9eed9a933fb Mon Sep 17 00:00:00 2001 From: akarneliuk Date: Sat, 13 Nov 2021 17:27:48 +0000 Subject: [PATCH] 0.6.3 --- .gitignore | 3 +- README.rst | 8 ++++- pygnmi/__init__.py | 2 +- pygnmi/arg_parser.py | 9 +++-- pygnmi/client.py | 53 +++++++++++++-------------- pygnmi/path_generator.py | 22 ++++++++++++ setup.py | 4 +-- tests/test_connect_methods.py | 2 +- tests/test_functionality.py | 68 +++++++++++++++++++++++++++++++++++ 9 files changed, 137 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 9434a9b..3330e41 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ log/ **/.pytest_cache/ **/.coverage **/coverage.json -**/htmlcov/ \ No newline at end of file +**/htmlcov/ +dist/ \ No newline at end of file diff --git a/README.rst b/README.rst index 2fa8746..6a07a85 100644 --- a/README.rst +++ b/README.rst @@ -78,6 +78,12 @@ Contributors Dev Log ======= +Release **0.6.3**: + +- Implemented ``prefix`` key in the ``Update`` message. +- Added possibility to provide password in STDIN rather than key. +- Minor bug-fixing. + Release **0.6.2**: - Added support of keepalive timer for gRPC session to prevent automatic closure each 2 hours. @@ -296,7 +302,7 @@ Release **0.1.0**: (c)2020-2021, karneliuk.com -.. |version| image:: https://img.shields.io/static/v1?label=latest&message=v0.6.2&color=success +.. |version| image:: https://img.shields.io/static/v1?label=latest&message=v0.6.3&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 69eb407..fd22077 100644 --- a/pygnmi/__init__.py +++ b/pygnmi/__init__.py @@ -1,3 +1,3 @@ #(c)2019-2021, karneliuk.com -__version__ = "0.6.2" +__version__ = "0.6.3" diff --git a/pygnmi/arg_parser.py b/pygnmi/arg_parser.py index 8b483e1..b3c82c6 100644 --- a/pygnmi/arg_parser.py +++ b/pygnmi/arg_parser.py @@ -4,8 +4,10 @@ # Modules import argparse import re +from getpass import getpass +# Functions def parse_args(msg): parser = argparse.ArgumentParser() parser.add_argument( @@ -55,9 +57,9 @@ def parse_args(msg): required=False, choices=[ "capabilities", "get", "set-update", "set-replace", "set-delete", - "subscribe-stream", "subscribe-poll", "subscribe-once" + "subscribe-stream", "subscribe-poll", "subscribe-once", "subscribe2" ], - default="get", + default="capabilities", help="gNMI Request type", ) parser.add_argument( @@ -108,4 +110,7 @@ def parse_args(msg): if len(args.gnmi_path) > 1: parser.error(f"Only one path supported when doing a {args.operation} operation") + if not args.password: + args.password = getpass("Device password: ") + return args diff --git a/pygnmi/client.py b/pygnmi/client.py index 2fa9a3f..4046b4d 100644 --- a/pygnmi/client.py +++ b/pygnmi/client.py @@ -22,7 +22,7 @@ # Own modules -from pygnmi.path_generator import gnmi_path_generator +from pygnmi.path_generator import gnmi_path_generator, gnmi_path_degenerator # Logger @@ -61,11 +61,11 @@ def __init__(self, target: tuple, username: str = None, password: str = None, self.__target = target if 'interval_ms' in kwargs: - self.configureKeepalive( kwargs ) + self.configureKeepalive(**kwargs) - def configureKeepalive(self, interval_ms: int, timeout_ms: int = 20000, + def configureKeepalive(self, keepalive_time_ms: int, keepalive_timeout_ms: int = 20000, max_pings_without_data: int = 0, - permit_without_calls: bool = True): + keepalive_permit_without_calls: bool = True): """ Helper method to set relevant client-side gRPC options to control keep-alive messages Must be called before connect() @@ -75,9 +75,9 @@ def configureKeepalive(self, interval_ms: int, timeout_ms: int = 20000, max_pings_without_data: default 0 to enable long idle connections """ self.__options += [ - ("grpc.keepalive_time_ms", interval_ms), - ("grpc.keepalive_timeout_ms", timeout_ms), - ("grpc.keepalive_permit_without_calls", 1 if permit_without_calls else 0), + ("grpc.keepalive_time_ms", keepalive_time_ms), + ("grpc.keepalive_timeout_ms", keepalive_timeout_ms), + ("grpc.keepalive_permit_without_calls", 1 if keepalive_permit_without_calls else 0), ("grpc.http2.max_pings_without_data", max_pings_without_data), ] @@ -213,7 +213,7 @@ def capabilities(self): return None - def get(self, path: list, datatype: str = 'all', encoding: str = 'json'): + def get(self, prefix: str = "", path: list = [], datatype: str = 'all', encoding: str = 'json'): """ Collecting the information about the resources from defined paths. @@ -270,6 +270,15 @@ def get(self, path: list, datatype: str = 'all', encoding: str = 'json'): else: pb_encoding = 4 + ## Gnmi PREFIX + try: + protobuf_prefix = gnmi_path_generator(prefix) if prefix else gnmi_path_generator([]) + + except: + logger.error(f'Conversion of gNMI prefix to the Protobuf format failed') + raise Exception ('Conversion of gNMI prefix to the Protobuf format failed') + + ## Gnmi PATH try: if not path: protobuf_paths = [] @@ -288,7 +297,8 @@ def get(self, path: list, datatype: str = 'all', encoding: str = 'json'): pb_encoding = 4 try: - gnmi_message_request = GetRequest(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) if self.__debug: print("gNMI request:\n------------------------------------------------") @@ -311,32 +321,23 @@ def get(self, path: list, datatype: str = 'all', encoding: str = 'json'): for notification in gnmi_message_response.notification: notification_container = {} + ## Message Notification, Key timestamp 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}) + + ## Message Notification, Key update if notification.update: notification_container.update({'update': []}) for update_msg in notification.update: update_container = {} - if update_msg.path and update_msg.path.elem: - resource_path = [] - for path_elem in update_msg.path.elem: - tp = '' - if path_elem.name: - tp += path_elem.name - - if path_elem.key: - for pk_name, pk_value in sorted(path_elem.key.items()): - tp += f'[{pk_name}={pk_value}]' - - resource_path.append(tp) - - update_container.update({'path': '/'.join(resource_path)}) - - else: - update_container.update({'path': None}) + ## Message Update, Key path + 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'): if update_msg.val.HasField('json_ietf_val'): update_container.update({'val': json.loads(update_msg.val.json_ietf_val)}) diff --git a/pygnmi/path_generator.py b/pygnmi/path_generator.py index 140d54d..00dcf87 100644 --- a/pygnmi/path_generator.py +++ b/pygnmi/path_generator.py @@ -74,3 +74,25 @@ def gnmi_path_generator(path_in_question: str): gnmi_path.elem.add(name=pe_entry) return gnmi_path + + +def gnmi_path_degenerator(gnmi_path) -> str: + """Parses a gNMI Path int an XPath expression + """ + result = None + if gnmi_path and gnmi_path.elem: + resource_path = [] + for path_elem in gnmi_path.elem: + tp = '' + if path_elem.name: + tp += path_elem.name + + if path_elem.key: + for pk_name, pk_value in sorted(path_elem.key.items()): + tp += f'[{pk_name}={pk_value}]' + + resource_path.append(tp) + + result = '/'.join(resource_path) + + return result \ No newline at end of file diff --git a/setup.py b/setup.py index a5c0751..652f468 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name = 'pygnmi', packages = ['pygnmi', 'pygnmi.spec', 'pygnmi.artefacts'], - version = '0.6.2', + version = '0.6.3', license='bsd-3-clause', description = 'This repository contains pure Python implementation of the gNMI client to interact with the network functions.', long_description = long_description, @@ -14,7 +14,7 @@ author = 'Anton Karneliuk', author_email = 'anton@karneliuk.com', url = 'https://github.com/akarneliuk/pygnmi', - download_url = 'https://github.com/akarneliuk/pygnmi/archive/v0.6.2.tar.gz', + download_url = 'https://github.com/akarneliuk/pygnmi/archive/v0.6.3.tar.gz', keywords = ['gnmi', 'automation', 'grpc', 'network'], install_requires=[ 'grpcio', diff --git a/tests/test_connect_methods.py b/tests/test_connect_methods.py index d359214..b348280 100644 --- a/tests/test_connect_methods.py +++ b/tests/test_connect_methods.py @@ -44,4 +44,4 @@ def test_get_connect_method(): gc.close() assert "notification" in result - assert isinstance(result["notification"], list) + assert isinstance(result["notification"], list) \ No newline at end of file diff --git a/tests/test_functionality.py b/tests/test_functionality.py index 0750bee..dc2bab9 100644 --- a/tests/test_functionality.py +++ b/tests/test_functionality.py @@ -4,6 +4,7 @@ from pygnmi.client import gNMIclient, telemetryParser from dotenv import load_dotenv import os +import json # Messages @@ -28,6 +29,23 @@ def test_capabilities(): assert "gnmi_version" in result +def test_connectivity_custom_keepalive(): + load_dotenv() + username_str = os.getenv("PYGNMI_USER") + password_str = os.getenv("PYGNMI_PASS") + hostname_str = os.getenv("PYGNMI_HOST") + port_str = os.getenv("PYGNMI_PORT") + path_cert_str = os.getenv("PYGNMI_CERT") + + with gNMIclient(target=(hostname_str, port_str), username=username_str, + password=password_str, path_cert=path_cert_str, keepalive_time_ms=1000) as gc: + result = gc.capabilities() + + assert "supported_models" in result + assert "supported_encodings" in result + assert "gnmi_version" in result + + def test_get_signle_path_all_path_formats(): load_dotenv() username_str = os.getenv("PYGNMI_USER") @@ -113,6 +131,56 @@ def test_get_signle_path_all_path_formats(): assert len(result["notification"][0]["update"]) > 0 +def test_get_prefix_and_path(): + load_dotenv() + username_str = os.getenv("PYGNMI_USER") + password_str = os.getenv("PYGNMI_PASS") + hostname_str = os.getenv("PYGNMI_HOST") + port_str = os.getenv("PYGNMI_PORT") + path_cert_str = os.getenv("PYGNMI_CERT") + + with gNMIclient(target=(hostname_str, port_str), username=username_str, + password=password_str, path_cert=path_cert_str) as gc: + gc.capabilities() + + # Default GNMI path + result = gc.get(prefix="/") + assert "notification" in result + assert isinstance(result["notification"], list) + assert len(result["notification"]) == 1 + assert "update" in result["notification"][0] + assert isinstance(result["notification"][0]["update"], list) + assert len(result["notification"][0]["update"]) > 0 + + # "yang-model:top_element" GNMI path notation + result = gc.get(prefix="openconfig-interfaces:interfaces") + assert "notification" in result + assert isinstance(result["notification"], list) + assert len(result["notification"]) == 1 + assert "update" in result["notification"][0] + assert isinstance(result["notification"][0]["update"], list) + assert len(result["notification"][0]["update"]) == 1 + + # "yang-model:top_element/next_element" GNMI path notation + result = gc.get(prefix="openconfig-interfaces:interfaces", path=["interface"]) + assert "notification" in result + assert isinstance(result["notification"], list) + assert len(result["notification"]) == 1 + assert "update" in result["notification"][0] + assert isinstance(result["notification"][0]["update"], list) + assert len(result["notification"][0]["update"]) > 0 + + # "/yang-model:top_element/next_element" GNMI path notation + result = gc.get(prefix="/openconfig-interfaces:interfaces", path=["interface[name=Loopback51]"]) + open("log/execution.log", "w").write(json.dumps(result, indent=4)) + assert "notification" in result + assert isinstance(result["notification"], list) + assert len(result["notification"]) == 1 + assert "update" in result["notification"][0] + assert isinstance(result["notification"][0]["update"], list) + assert len(result["notification"][0]["update"]) > 0 + + def test_get_multiple_paths(): load_dotenv() username_str = os.getenv("PYGNMI_USER")