Skip to content

Commit

Permalink
0.6.3
Browse files Browse the repository at this point in the history
  • Loading branch information
akarneliuk committed Nov 13, 2021
1 parent 211fe57 commit 33c6fc0
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 34 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ log/
**/.pytest_cache/
**/.coverage
**/coverage.json
**/htmlcov/
**/htmlcov/
dist/
8 changes: 7 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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/
Expand Down
2 changes: 1 addition & 1 deletion pygnmi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#(c)2019-2021, karneliuk.com

__version__ = "0.6.2"
__version__ = "0.6.3"
9 changes: 7 additions & 2 deletions pygnmi/arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
# Modules
import argparse
import re
from getpass import getpass


# Functions
def parse_args(msg):
parser = argparse.ArgumentParser()
parser.add_argument(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
53 changes: 27 additions & 26 deletions pygnmi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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),
]

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 = []
Expand All @@ -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------------------------------------------------")
Expand All @@ -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)})
Expand Down
22 changes: 22 additions & 0 deletions pygnmi/path_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
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,
long_description_content_type = 'text/x-rst',
author = 'Anton Karneliuk',
author_email = '[email protected]',
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',
Expand Down
2 changes: 1 addition & 1 deletion tests/test_connect_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
68 changes: 68 additions & 0 deletions tests/test_functionality.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pygnmi.client import gNMIclient, telemetryParser
from dotenv import load_dotenv
import os
import json


# Messages
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit 33c6fc0

Please sign in to comment.