diff --git a/MANIFEST b/MANIFEST index b1b6671..d805df2 100644 --- a/MANIFEST +++ b/MANIFEST @@ -4,7 +4,11 @@ setup.py pygnmi/__init__.py pygnmi/arg_parser.py pygnmi/client.py -pygnmi/path_generator.py +pygnmi/create_gnmi_extension.py +pygnmi/create_gnmi_path.py pygnmi/tools.py pygnmi/artefacts/messages.py +pygnmi/spec/v080/gnmi_ext_pb2.py +pygnmi/spec/v080/gnmi_pb2.py +pygnmi/spec/v080/gnmi_pb2_grpc.py scripts/pygnmicli diff --git a/README.rst b/README.rst index 3e5e572..272f042 100644 --- a/README.rst +++ b/README.rst @@ -57,11 +57,12 @@ Tested Network Operating Systems (NOS) - Nokia SR OS - Cisco IOS XR - Juniper JUNOS +- Nokia SRLinux Network Operating Systems (NOS) in test --------------------------------------- -- Nokia SRLinux - Cisco Nexus +- Broadcom SONiC ======= License @@ -79,11 +80,18 @@ Contributors - `Jeroen van Bemme `_ - `Frédéric Perrin `_ - `Malanovo `_ +- `Sebastian Lohff `_ ======= Dev Log ======= +Release **0.8.3**: + +- Changed behaviour of ``subscribe2()`` to RPC to avoid adding the empty ``Extension`` field for no extensions presenting. Fix for `Issue 83 `_. +- Uppdated documentation with examples in GitHub. +- Added support of History extensions to ``pygnmicli``. + Release **0.8.2**: - Implemented `History Extension `_. @@ -377,7 +385,7 @@ Release **0.1.0**: (c)2020-2022, karneliuk.com -.. |version| image:: https://img.shields.io/static/v1?label=latest&message=v0.8.2&color=success +.. |version| image:: https://img.shields.io/static/v1?label=latest&message=v0.8.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/ @@ -385,5 +393,5 @@ Release **0.1.0**: .. _license: https://github.com/akarneliuk/pygnmi/blob/master/LICENSE.txt .. |project| image:: https://img.shields.io/badge/akarneliuk%2Fpygnmi-blueviolet.svg?logo=github&color=success .. _project: https://github.com/akarneliuk/pygnmi/ -.. |coverage| image:: https://img.shields.io/static/v1?label=coverage&message=71%&color=yellow +.. |coverage| image:: https://img.shields.io/static/v1?label=coverage&message=66%&color=yellow .. _coverage: https://github.com/nedbat/coveragepy \ No newline at end of file diff --git a/examples/pure_python/autehentication_with_token.py b/examples/pure_python/autehentication_with_token.py new file mode 100644 index 0000000..ac48b2d --- /dev/null +++ b/examples/pure_python/autehentication_with_token.py @@ -0,0 +1,22 @@ +"""This example shows how to authenticate against the device +using token instead of username/password""" +# Modules +from pygnmi.client import gNMIclient + +# Variables +from inventory import hosts + +# Body +if __name__ == "__main__": + + # Get Token + with open("token.tok") as f: + TOKEN = f.read().strip('\n') + + for host in hosts: + with gNMIclient(target=(host["ip_address"], host["port"]), + token=TOKEN, skip_verify=False) as gc: + + result = gc.capabilities() + + print(f"{host['ip_address']}: {result}\n\n") diff --git a/examples/pure_python/subscribe_tls.log b/examples/pure_python/subscribe_tls.log deleted file mode 100755 index 0f55926..0000000 --- a/examples/pure_python/subscribe_tls.log +++ /dev/null @@ -1,138 +0,0 @@ -python subscribe_tls.py -{ 'update': { 'prefix': 'interfaces/interface[name=MgmtEth0/RP0/CPU0/0]/state', - 'timestamp': 1609946434369000000, - 'update': [ {'path': 'counters/in-pkts', 'val': 24568068}, - {'path': 'counters/in-octets', 'val': 4716880634}, - {'path': 'counters/out-pkts', 'val': 58763074}, - {'path': 'counters/out-octets', 'val': 74016845216}, - {'path': 'counters/in-multicast-pkts', 'val': 654309}, - {'path': 'counters/in-broadcast-pkts', 'val': 738104}, - {'path': 'counters/out-multicast-pkts', 'val': 0}, - {'path': 'counters/out-broadcast-pkts', 'val': 0}, - {'path': 'counters/in-unknown-protos', 'val': 0}, - {'path': 'counters/in-errors', 'val': 0}, - {'path': 'counters/in-fcs-errors', 'val': 0}, - {'path': 'counters/out-errors', 'val': 0}, - {'path': 'counters/carrier-transitions', 'val': 1}, - { 'path': 'counters/last-clear', - 'val': '2021-01-04T08:40:42Z'}, - {'path': 'counters/in-unicast-pkts', 'val': 23175655}, - {'path': 'counters/in-discards', 'val': 0}, - { 'path': 'counters/out-unicast-pkts', - 'val': 58763074}, - {'path': 'counters/out-discards', 'val': 0}]}} -{ 'update': { 'prefix': 'interfaces/interface[name=MgmtEth0/RP0/CPU0/1]/state', - 'timestamp': 1609946434369000000, - 'update': [ {'path': 'counters/in-pkts', 'val': 0}, - {'path': 'counters/in-octets', 'val': 0}, - {'path': 'counters/out-pkts', 'val': 0}, - {'path': 'counters/out-octets', 'val': 0}, - {'path': 'counters/in-multicast-pkts', 'val': 0}, - {'path': 'counters/in-broadcast-pkts', 'val': 0}, - {'path': 'counters/out-multicast-pkts', 'val': 0}, - {'path': 'counters/out-broadcast-pkts', 'val': 0}, - {'path': 'counters/in-unknown-protos', 'val': 0}, - {'path': 'counters/in-errors', 'val': 0}, - {'path': 'counters/in-fcs-errors', 'val': 0}, - {'path': 'counters/out-errors', 'val': 0}, - {'path': 'counters/carrier-transitions', 'val': 0}, - { 'path': 'counters/last-clear', - 'val': '2021-01-04T08:40:42Z'}, - {'path': 'counters/in-unicast-pkts', 'val': 0}, - {'path': 'counters/in-discards', 'val': 0}, - {'path': 'counters/out-unicast-pkts', 'val': 0}, - {'path': 'counters/out-discards', 'val': 0}]}} -{ 'update': { 'prefix': 'interfaces/interface[name=MgmtEth0/RP0/CPU0/2]/state', - 'timestamp': 1609946434369000000, - 'update': [ {'path': 'counters/in-pkts', 'val': 0}, - {'path': 'counters/in-octets', 'val': 0}, - {'path': 'counters/out-pkts', 'val': 0}, - {'path': 'counters/out-octets', 'val': 0}, - {'path': 'counters/in-multicast-pkts', 'val': 0}, - {'path': 'counters/in-broadcast-pkts', 'val': 0}, - {'path': 'counters/out-multicast-pkts', 'val': 0}, - {'path': 'counters/out-broadcast-pkts', 'val': 0}, - {'path': 'counters/in-unknown-protos', 'val': 0}, - {'path': 'counters/in-errors', 'val': 0}, - {'path': 'counters/in-fcs-errors', 'val': 0}, - {'path': 'counters/out-errors', 'val': 0}, - {'path': 'counters/carrier-transitions', 'val': 0}, - { 'path': 'counters/last-clear', - 'val': '2021-01-04T08:40:42Z'}, - {'path': 'counters/in-unicast-pkts', 'val': 0}, - {'path': 'counters/in-discards', 'val': 0}, - {'path': 'counters/out-unicast-pkts', 'val': 0}, - {'path': 'counters/out-discards', 'val': 0}]}} -{ 'update': { 'prefix': 'interfaces/interface[name=Null0]/state', - 'timestamp': 1609946434369000000, - 'update': [ {'path': 'counters/in-pkts', 'val': 0}, - {'path': 'counters/in-octets', 'val': 0}, - {'path': 'counters/out-pkts', 'val': 0}, - {'path': 'counters/out-octets', 'val': 0}, - {'path': 'counters/in-multicast-pkts', 'val': 0}, - {'path': 'counters/in-broadcast-pkts', 'val': 0}, - {'path': 'counters/out-multicast-pkts', 'val': 0}, - {'path': 'counters/out-broadcast-pkts', 'val': 0}, - {'path': 'counters/in-unknown-protos', 'val': 0}, - {'path': 'counters/in-errors', 'val': 0}, - {'path': 'counters/in-fcs-errors', 'val': 0}, - {'path': 'counters/out-errors', 'val': 0}, - {'path': 'counters/carrier-transitions', 'val': 0}, - { 'path': 'counters/last-clear', - 'val': '2021-01-04T08:40:58Z'}, - {'path': 'counters/in-unicast-pkts', 'val': 0}, - {'path': 'counters/in-discards', 'val': 0}, - {'path': 'counters/out-unicast-pkts', 'val': 0}, - {'path': 'counters/out-discards', 'val': 0}]}} -{ 'update': { 'prefix': 'interfaces/interface[name=MgmtEth0/RP0/CPU0/0]/state', - 'timestamp': 1609946434373000000, - 'update': [ {'path': 'name', 'val': 'MgmtEth0/RP0/CPU0/0'}, - { 'path': 'type', - 'val': 'iana-if-type:ethernetCsmacd'}, - {'path': 'mtu', 'val': 1513}, - {'path': 'loopback-mode', 'val': False}, - {'path': 'enabled', 'val': True}, - {'path': 'ifindex', 'val': 3}, - {'path': 'admin-status', 'val': 'UP'}, - {'path': 'oper-status', 'val': 'UP'}, - {'path': 'last-change', 'val': 1609749655558229584}, - {'path': 'logical', 'val': False}]}} -{ 'update': { 'prefix': 'interfaces/interface[name=MgmtEth0/RP0/CPU0/1]/state', - 'timestamp': 1609946434379000000, - 'update': [ {'path': 'name', 'val': 'MgmtEth0/RP0/CPU0/1'}, - { 'path': 'type', - 'val': 'iana-if-type:ethernetCsmacd'}, - {'path': 'mtu', 'val': 1514}, - {'path': 'loopback-mode', 'val': False}, - {'path': 'enabled', 'val': True}, - {'path': 'ifindex', 'val': 4}, - {'path': 'admin-status', 'val': 'UP'}, - {'path': 'oper-status', 'val': 'DOWN'}, - {'path': 'last-change', 'val': 0}, - {'path': 'logical', 'val': False}]}} -{ 'update': { 'prefix': 'interfaces/interface[name=MgmtEth0/RP0/CPU0/2]/state', - 'timestamp': 1609946434384000000, - 'update': [ {'path': 'name', 'val': 'MgmtEth0/RP0/CPU0/2'}, - { 'path': 'type', - 'val': 'iana-if-type:ethernetCsmacd'}, - {'path': 'mtu', 'val': 1514}, - {'path': 'loopback-mode', 'val': False}, - {'path': 'enabled', 'val': True}, - {'path': 'ifindex', 'val': 5}, - {'path': 'admin-status', 'val': 'UP'}, - {'path': 'oper-status', 'val': 'DOWN'}, - {'path': 'last-change', 'val': 0}, - {'path': 'logical', 'val': False}]}} -{ 'update': { 'prefix': 'interfaces/interface[name=Null0]/state', - 'timestamp': 1609946434388000000, - 'update': [ {'path': 'name', 'val': 'Null0'}, - {'path': 'type', 'val': 'iana-if-type:other'}, - {'path': 'mtu', 'val': 1500}, - {'path': 'loopback-mode', 'val': False}, - {'path': 'enabled', 'val': True}, - {'path': 'ifindex', 'val': 2}, - {'path': 'admin-status', 'val': 'UP'}, - {'path': 'oper-status', 'val': 'UP'}, - {'path': 'last-change', 'val': 1609749630376052491}, - {'path': 'logical', 'val': True}]}} -{'sync_response': True} diff --git a/examples/pure_python/subscribe_with_history_extension_1.py b/examples/pure_python/subscribe_with_history_extension_1.py new file mode 100644 index 0000000..e2a4d24 --- /dev/null +++ b/examples/pure_python/subscribe_with_history_extension_1.py @@ -0,0 +1,38 @@ +"""This example shows how to use the History extension +with range option + +Per GNMI spec, /extension/history/range requires +`STREAM` mode of telemetery subscription""" +# Modules +from pygnmi.client import gNMIclient + + +# Variables +TELEMETRY_REQUEST2 = { + 'subscription': [ + { + 'path': 'openconfig:/interfaces/interface/state/counters', + 'mode': 'target_defined' + } + ], + 'mode': 'stream', + 'encoding': 'proto' + } + +EXTENSION2 = { + 'history': { + 'range': { + 'start': '2022-07-23T09:47:00Z', + 'end': '2022-07-24T8:57:00Z' + } + } + } + + +# Body +with open("token.tok") as f: + TOKEN = f.read().strip('\n') + +with gNMIclient(target=('192.168.0.5', '443'), token=TOKEN, skip_verify=True) as gconn: + for item in gconn.subscribe2(subscribe=TELEMETRY_REQUEST2, target="leaf1", extension=EXTENSION2): + print(item) \ No newline at end of file diff --git a/examples/pure_python/subscribe_with_history_extension_2.py b/examples/pure_python/subscribe_with_history_extension_2.py new file mode 100644 index 0000000..48a92d4 --- /dev/null +++ b/examples/pure_python/subscribe_with_history_extension_2.py @@ -0,0 +1,35 @@ +"""This example shows how to use the History extension +with snapshit_time option. + +Per GNMI spec, /extension/history/snapshot_time requires +`ONCE` mode of telemetery subscription""" +# Modules +from pygnmi.client import gNMIclient + + +# Variables + +TELEMETRY_REQUEST1 = { + 'subscription': [ + { + 'path': 'openconfig:/interfaces/interface[name=Ethernet2]/state/counters', + 'mode': 'target_defined' + } + ], + 'mode': 'once', + 'encoding': 'proto' + } + +EXTENSION1 = { + 'history': { + 'snapshot_time': '2022-07-24T8:57:00Z' + } + } + +# Body +with open("token.tok") as f: + TOKEN = f.read().strip('\n') + +with gNMIclient(target=('192.168.0.5', '443'), token=TOKEN, skip_verify=True) as gconn: + for item in gconn.subscribe2(subscribe=TELEMETRY_REQUEST1, target="leaf1", extension=EXTENSION1): + print(item) diff --git a/pygnmi/__init__.py b/pygnmi/__init__.py index e4c3384..6577b3e 100644 --- a/pygnmi/__init__.py +++ b/pygnmi/__init__.py @@ -2,4 +2,4 @@ pyGNMI module to manage network devices with gNMI (c)2020-2022, Karneliuk """ -__version__ = "0.8.2" +__version__ = "0.8.3" diff --git a/pygnmi/arg_parser.py b/pygnmi/arg_parser.py index 624c8a5..b7a4bdd 100644 --- a/pygnmi/arg_parser.py +++ b/pygnmi/arg_parser.py @@ -36,19 +36,19 @@ def parse_args(msg): dest="password" ) parser.add_argument( - "-c", "--path_cert", + "-c", "--path-cert", type=str, required=False, help="Path to certificate chain file", ) parser.add_argument( - "-k", "--path_key", + "-k", "--path-key", type=str, required=False, help="Path to private key file" ) parser.add_argument( - "-r", "--path_root", + "-r", "--path-root", type=str, required=False, help="Path to root CA file" @@ -83,14 +83,14 @@ def parse_args(msg): help="gNMI Request type", ) parser.add_argument( - "-x", "--gnmi_path", + "-x", "--gnmi-path", type=str, required=False, default="", nargs="+", help="gNMI paths of interest in XPath format, space separated" ) parser.add_argument( - "--gnmi_path_target", + "--gnmi-path-target", type=str, required=False, default="", @@ -126,6 +126,27 @@ def parse_args(msg): default="", help="Compare the states of the devices before and after change to show difference", ) + parser.add_argument( + "--ext-history-range-start", + type=str, + required=False, + default="", + help="Specify the start timestamp for the GNMI history range", + ) + parser.add_argument( + "--ext-history-range-end", + type=str, + required=False, + default="", + help="Specify the end timestamp for the GNMI history range", + ) + parser.add_argument( + "--ext-history-snapshot-time", + type=str, + required=False, + default="", + help="Specify the snapshit time for the GNMI history", + ) args = parser.parse_args() diff --git a/pygnmi/client.py b/pygnmi/client.py index 533d67d..aa842f2 100644 --- a/pygnmi/client.py +++ b/pygnmi/client.py @@ -688,7 +688,7 @@ def _build_subscriptionrequest(self, subscribe: dict, target: str = None, extens raise ValueError('Subscribe subscribe request is specified, but the value is not dict.') request = SubscriptionList() - gnmi_extension = [get_gnmi_extension(ext=extension)] + gnmi_extension = get_gnmi_extension(ext=extension) # use_alias if 'use_aliases' not in subscribe: @@ -799,7 +799,11 @@ def _build_subscriptionrequest(self, subscribe: dict, target: str = None, extens request.subscription.add(path=se_path, mode=se_mode, sample_interval=se_sample_interval, suppress_redundant=se_suppress_redundant, heartbeat_interval=se_heartbeat_interval) - return SubscribeRequest(subscribe=request, extension=gnmi_extension) + if gnmi_extension: + return SubscribeRequest(subscribe=request, extension=[gnmi_extension]) + + else: + return SubscribeRequest(subscribe=request) def subscribe(self, subscribe: dict = None, poll: bool = False, aliases: list = None, timeout: float = 0.0): """ diff --git a/pygnmi/create_gnmi_extension.py b/pygnmi/create_gnmi_extension.py index d475f4c..1d7640d 100644 --- a/pygnmi/create_gnmi_extension.py +++ b/pygnmi/create_gnmi_extension.py @@ -8,12 +8,15 @@ # Functions def get_gnmi_extension(ext: dict = None) -> list: """This helper function allows conversion of dictinary to Extension class""" - result = Extension() + result = None # Don't build empty extension if not ext: return result + else: + result = Extension() + # Create history extension if "history" in ext: diff --git a/scripts/pygnmicli b/scripts/pygnmicli index e3782b1..0c25f45 100644 --- a/scripts/pygnmicli +++ b/scripts/pygnmicli @@ -1,5 +1,5 @@ #!/usr/bin/env python -#(c)2019-2021, karneliuk.com +#(c)2019-2022, karneliuk.com # Modules @@ -86,8 +86,44 @@ def main(): 'encoding': 'json' } + # Set up extensions + if args.ext_history_snapshot_time: + try: + time_to_int = int(args.ext_history_snapshot_time) + + except: + time_to_int = args.ext_history_snapshot_time + + EXT = { + "history": { + "snapshot_time": time_to_int + } + } + + elif args.ext_history_range_start and args.ext_history_range_end: + try: + time_to_int_1 = int(args.ext_history_range_start) + time_to_int_2 = int(args.ext_history_range_end) + + except: + time_to_int_1 = args.ext_history_range_start + time_to_int_2 = args.ext_history_range_start + + EXT = { + "history": { + "range": { + "start": time_to_int_1, + "end": time_to_int_2 + } + } + } + + else: + EXT = None + result = GC.subscribe2(subscribe=subscribe, - target=args.gnmi_path_target) + target=args.gnmi_path_target, + extension=EXT) if mode == "stream": try: diff --git a/setup.py b/setup.py index 2d0ac3d..c93947d 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='pygnmi', packages=['pygnmi', 'pygnmi.spec.v080', 'pygnmi.artefacts'], - version='0.8.2', + version='0.8.3', license='bsd-3-clause', description='Pure Python gNMI client to manage network functions and collect telemetry.', 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.8.2.tar.gz', + download_url='https://github.com/akarneliuk/pygnmi/archive/v0.8.3.tar.gz', keywords=['gnmi', 'automation', 'grpc', 'network'], install_requires=[ 'grpcio', diff --git a/tests/test_01_create_extensions.py b/tests/test_01_create_extensions.py index 711c81f..c1cc1dc 100644 --- a/tests/test_01_create_extensions.py +++ b/tests/test_01_create_extensions.py @@ -34,6 +34,10 @@ } } +EXT5 = {} + +EXT6 = None + # Tests def test_extension_history_snapshot_time_ns(ext=EXT1): @@ -64,3 +68,17 @@ def test_extension_history_range_str(ext=EXT4): gnmi_ext = get_gnmi_extension(ext) assert isinstance(gnmi_ext.history.range.start, int) assert isinstance(gnmi_ext.history.range.end, int) + + +def test_extension_empty_dict(ext=EXT5): + """Unit test to verify /extension is not created + for empty dict""" + gnmi_ext = get_gnmi_extension(ext) + assert not gnmi_ext + + +def test_extension_empty_none(ext=EXT6): + """Unit test to verify /extension is not created + for None""" + gnmi_ext = get_gnmi_extension(ext) + assert not gnmi_ext