From 78a1043e5f1da9c38f2046adfc0d685632c0e22f Mon Sep 17 00:00:00 2001 From: Tim Raphael Date: Mon, 12 Aug 2024 16:29:10 +1000 Subject: [PATCH] Major simplifications and new abstractions, renamed napalm-srl -> napalm-srlinux, adding typing and reimplementing PoC methods for get_bgp_neighbors, get_lldp_neighbors, get_users. --- AUTHORS | 1 + examples/example_script.py | 10 +- napalm_srl/gnmi_pb2.py | 2041 -------------- napalm_srl/jsondiff.py | 262 -- napalm_srl/srl.py | 2888 -------------------- {napalm_srl => napalm_srlinux}/__init__.py | 6 +- napalm_srlinux/srlinux.py | 951 +++++++ off_box_diff.png | Bin 156946 -> 0 bytes pyproject.toml | 2 +- requirements.txt | 12 +- setup.cfg | 4 +- setup.py | 2 +- test/unit/TestNokiaSRLDriver.py | 4 +- test/unit/conftest.py | 6 +- tox.ini | 2 +- 15 files changed, 970 insertions(+), 5221 deletions(-) delete mode 100644 napalm_srl/gnmi_pb2.py delete mode 100644 napalm_srl/jsondiff.py delete mode 100644 napalm_srl/srl.py rename {napalm_srl => napalm_srlinux}/__init__.py (83%) create mode 100644 napalm_srlinux/srlinux.py delete mode 100755 off_box_diff.png diff --git a/AUTHORS b/AUTHORS index ed1aaeb..b5d3bb0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -5,3 +5,4 @@ Syed Shekupillai Ashna Shah Wim Henderickx Jeroen van Bemmel +Tim Raphael diff --git a/examples/example_script.py b/examples/example_script.py index cb689f8..171025b 100644 --- a/examples/example_script.py +++ b/examples/example_script.py @@ -5,18 +5,12 @@ from napalm import get_network_driver import json -driver = get_network_driver("srl") +driver = get_network_driver("srlinux") optional_args = { - "gnmi_port": 57400, "jsonrpc_port": 80, - "target_name": "172.20.20.2", - "tls_cert":"/root/gnmic_certs/srl_certs/clientCert.crt", - "tls_ca": "/root/gnmic_certs/srl_certs/RootCA.crt", - "tls_key": "/root/gnmic_certs/srl_certs/clientKey.pem", #"skip_verify": True, #"insecure": False - "encoding": "JSON_IETF" -} +} device = driver("172.20.20.2", "admin", "admin", 60, optional_args) device.open() #print(json.dumps(device.get_bgp_config(neighbor="", group=""))) #Done diff --git a/napalm_srl/gnmi_pb2.py b/napalm_srl/gnmi_pb2.py deleted file mode 100644 index 61a423e..0000000 --- a/napalm_srl/gnmi_pb2.py +++ /dev/null @@ -1,2041 +0,0 @@ -# Copyright 2020 Nokia -# Licensed under the Apache License 2.0. -# SPDX-License-Identifier: Apache-2.0 - -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: proto/gnmi/gnmi.proto - -import sys -_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) -from google.protobuf.internal import enum_type_wrapper -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database -from google.protobuf import descriptor_pb2 -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 -from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 - - -DESCRIPTOR = _descriptor.FileDescriptor( - name='proto/gnmi/gnmi.proto', - package='gnmi', - syntax='proto3', - serialized_pb=_b('\n\x15proto/gnmi/gnmi.proto\x12\x04gnmi\x1a\x19google/protobuf/any.proto\x1a google/protobuf/descriptor.proto\"\x86\x01\n\x0cNotification\x12\x11\n\ttimestamp\x18\x01 \x01(\x03\x12\x1a\n\x06prefix\x18\x02 \x01(\x0b\x32\n.gnmi.Path\x12\r\n\x05\x61lias\x18\x03 \x01(\t\x12\x1c\n\x06update\x18\x04 \x03(\x0b\x32\x0c.gnmi.Update\x12\x1a\n\x06\x64\x65lete\x18\x05 \x03(\x0b\x32\n.gnmi.Path\"u\n\x06Update\x12\x18\n\x04path\x18\x01 \x01(\x0b\x32\n.gnmi.Path\x12\x1e\n\x05value\x18\x02 \x01(\x0b\x32\x0b.gnmi.ValueB\x02\x18\x01\x12\x1d\n\x03val\x18\x03 \x01(\x0b\x32\x10.gnmi.TypedValue\x12\x12\n\nduplicates\x18\x04 \x01(\r\"\xce\x02\n\nTypedValue\x12\x14\n\nstring_val\x18\x01 \x01(\tH\x00\x12\x11\n\x07int_val\x18\x02 \x01(\x03H\x00\x12\x12\n\x08uint_val\x18\x03 \x01(\x04H\x00\x12\x12\n\x08\x62ool_val\x18\x04 \x01(\x08H\x00\x12\x13\n\tbytes_val\x18\x05 \x01(\x0cH\x00\x12\x13\n\tfloat_val\x18\x06 \x01(\x02H\x00\x12&\n\x0b\x64\x65\x63imal_val\x18\x07 \x01(\x0b\x32\x0f.gnmi.Decimal64H\x00\x12)\n\x0cleaflist_val\x18\x08 \x01(\x0b\x32\x11.gnmi.ScalarArrayH\x00\x12\'\n\x07\x61ny_val\x18\t \x01(\x0b\x32\x14.google.protobuf.AnyH\x00\x12\x12\n\x08json_val\x18\n \x01(\x0cH\x00\x12\x17\n\rjson_ietf_val\x18\x0b \x01(\x0cH\x00\x12\x13\n\tascii_val\x18\x0c \x01(\tH\x00\x42\x07\n\x05value\"Y\n\x04Path\x12\x13\n\x07\x65lement\x18\x01 \x03(\tB\x02\x18\x01\x12\x0e\n\x06origin\x18\x02 \x01(\t\x12\x1c\n\x04\x65lem\x18\x03 \x03(\x0b\x32\x0e.gnmi.PathElem\x12\x0e\n\x06target\x18\x04 \x01(\t\"j\n\x08PathElem\x12\x0c\n\x04name\x18\x01 \x01(\t\x12$\n\x03key\x18\x02 \x03(\x0b\x32\x17.gnmi.PathElem.KeyEntry\x1a*\n\x08KeyEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"8\n\x05Value\x12\r\n\x05value\x18\x01 \x01(\x0c\x12\x1c\n\x04type\x18\x02 \x01(\x0e\x32\x0e.gnmi.Encoding:\x02\x18\x01\"N\n\x05\x45rror\x12\x0c\n\x04\x63ode\x18\x01 \x01(\r\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\"\n\x04\x64\x61ta\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x18\x01\".\n\tDecimal64\x12\x0e\n\x06\x64igits\x18\x01 \x01(\x04\x12\x11\n\tprecision\x18\x02 \x01(\r\"0\n\x0bScalarArray\x12!\n\x07\x65lement\x18\x01 \x03(\x0b\x32\x10.gnmi.TypedValue\"\x8a\x01\n\x10SubscribeRequest\x12+\n\tsubscribe\x18\x01 \x01(\x0b\x32\x16.gnmi.SubscriptionListH\x00\x12\x1a\n\x04poll\x18\x03 \x01(\x0b\x32\n.gnmi.PollH\x00\x12\"\n\x07\x61liases\x18\x04 \x01(\x0b\x32\x0f.gnmi.AliasListH\x00\x42\t\n\x07request\"\x06\n\x04Poll\"\x80\x01\n\x11SubscribeResponse\x12$\n\x06update\x18\x01 \x01(\x0b\x32\x12.gnmi.NotificationH\x00\x12\x17\n\rsync_response\x18\x03 \x01(\x08H\x00\x12 \n\x05\x65rror\x18\x04 \x01(\x0b\x32\x0b.gnmi.ErrorB\x02\x18\x01H\x00\x42\n\n\x08response\"\xd7\x02\n\x10SubscriptionList\x12\x1a\n\x06prefix\x18\x01 \x01(\x0b\x32\n.gnmi.Path\x12(\n\x0csubscription\x18\x02 \x03(\x0b\x32\x12.gnmi.Subscription\x12\x13\n\x0buse_aliases\x18\x03 \x01(\x08\x12\x1d\n\x03qos\x18\x04 \x01(\x0b\x32\x10.gnmi.QOSMarking\x12)\n\x04mode\x18\x05 \x01(\x0e\x32\x1b.gnmi.SubscriptionList.Mode\x12\x19\n\x11\x61llow_aggregation\x18\x06 \x01(\x08\x12#\n\nuse_models\x18\x07 \x03(\x0b\x32\x0f.gnmi.ModelData\x12 \n\x08\x65ncoding\x18\x08 \x01(\x0e\x32\x0e.gnmi.Encoding\x12\x14\n\x0cupdates_only\x18\t \x01(\x08\"&\n\x04Mode\x12\n\n\x06STREAM\x10\x00\x12\x08\n\x04ONCE\x10\x01\x12\x08\n\x04POLL\x10\x02\"\x9f\x01\n\x0cSubscription\x12\x18\n\x04path\x18\x01 \x01(\x0b\x32\n.gnmi.Path\x12$\n\x04mode\x18\x02 \x01(\x0e\x32\x16.gnmi.SubscriptionMode\x12\x17\n\x0fsample_interval\x18\x03 \x01(\x04\x12\x1a\n\x12suppress_redundant\x18\x04 \x01(\x08\x12\x1a\n\x12heartbeat_interval\x18\x05 \x01(\x04\"\x1d\n\nQOSMarking\x12\x0f\n\x07marking\x18\x01 \x01(\r\"0\n\x05\x41lias\x12\x18\n\x04path\x18\x01 \x01(\x0b\x32\n.gnmi.Path\x12\r\n\x05\x61lias\x18\x02 \x01(\t\"\'\n\tAliasList\x12\x1a\n\x05\x61lias\x18\x01 \x03(\x0b\x32\x0b.gnmi.Alias\"\x81\x01\n\nSetRequest\x12\x1a\n\x06prefix\x18\x01 \x01(\x0b\x32\n.gnmi.Path\x12\x1a\n\x06\x64\x65lete\x18\x02 \x03(\x0b\x32\n.gnmi.Path\x12\x1d\n\x07replace\x18\x03 \x03(\x0b\x32\x0c.gnmi.Update\x12\x1c\n\x06update\x18\x04 \x03(\x0b\x32\x0c.gnmi.Update\"\x84\x01\n\x0bSetResponse\x12\x1a\n\x06prefix\x18\x01 \x01(\x0b\x32\n.gnmi.Path\x12$\n\x08response\x18\x02 \x03(\x0b\x32\x12.gnmi.UpdateResult\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0b.gnmi.ErrorB\x02\x18\x01\x12\x11\n\ttimestamp\x18\x04 \x01(\x03\"\xca\x01\n\x0cUpdateResult\x12\x15\n\ttimestamp\x18\x01 \x01(\x03\x42\x02\x18\x01\x12\x18\n\x04path\x18\x02 \x01(\x0b\x32\n.gnmi.Path\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0b.gnmi.ErrorB\x02\x18\x01\x12(\n\x02op\x18\x04 \x01(\x0e\x32\x1c.gnmi.UpdateResult.Operation\"=\n\tOperation\x12\x0b\n\x07INVALID\x10\x00\x12\n\n\x06\x44\x45LETE\x10\x01\x12\x0b\n\x07REPLACE\x10\x02\x12\n\n\x06UPDATE\x10\x03\"\xef\x01\n\nGetRequest\x12\x1a\n\x06prefix\x18\x01 \x01(\x0b\x32\n.gnmi.Path\x12\x18\n\x04path\x18\x02 \x03(\x0b\x32\n.gnmi.Path\x12\'\n\x04type\x18\x03 \x01(\x0e\x32\x19.gnmi.GetRequest.DataType\x12 \n\x08\x65ncoding\x18\x05 \x01(\x0e\x32\x0e.gnmi.Encoding\x12#\n\nuse_models\x18\x06 \x03(\x0b\x32\x0f.gnmi.ModelData\";\n\x08\x44\x61taType\x12\x07\n\x03\x41LL\x10\x00\x12\n\n\x06\x43ONFIG\x10\x01\x12\t\n\x05STATE\x10\x02\x12\x0f\n\x0bOPERATIONAL\x10\x03\"W\n\x0bGetResponse\x12(\n\x0cnotification\x18\x01 \x03(\x0b\x32\x12.gnmi.Notification\x12\x1e\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x0b.gnmi.ErrorB\x02\x18\x01\"\x13\n\x11\x43\x61pabilityRequest\"\x82\x01\n\x12\x43\x61pabilityResponse\x12)\n\x10supported_models\x18\x01 \x03(\x0b\x32\x0f.gnmi.ModelData\x12+\n\x13supported_encodings\x18\x02 \x03(\x0e\x32\x0e.gnmi.Encoding\x12\x14\n\x0cgNMI_version\x18\x03 \x01(\t\"@\n\tModelData\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0corganization\x18\x02 \x01(\t\x12\x0f\n\x07version\x18\x03 \x01(\t*D\n\x08\x45ncoding\x12\x08\n\x04JSON\x10\x00\x12\t\n\x05\x42YTES\x10\x01\x12\t\n\x05PROTO\x10\x02\x12\t\n\x05\x41SCII\x10\x03\x12\r\n\tJSON_IETF\x10\x04*A\n\x10SubscriptionMode\x12\x12\n\x0eTARGET_DEFINED\x10\x00\x12\r\n\tON_CHANGE\x10\x01\x12\n\n\x06SAMPLE\x10\x02\x32\xe3\x01\n\x04gNMI\x12\x41\n\x0c\x43\x61pabilities\x12\x17.gnmi.CapabilityRequest\x1a\x18.gnmi.CapabilityResponse\x12*\n\x03Get\x12\x10.gnmi.GetRequest\x1a\x11.gnmi.GetResponse\x12*\n\x03Set\x12\x10.gnmi.SetRequest\x1a\x11.gnmi.SetResponse\x12@\n\tSubscribe\x12\x16.gnmi.SubscribeRequest\x1a\x17.gnmi.SubscribeResponse(\x01\x30\x01:3\n\x0cgnmi_service\x12\x1c.google.protobuf.FileOptions\x18\xe9\x07 \x01(\tB\x08\xca>\x05\x30.5.0b\x06proto3') - , - dependencies=[google_dot_protobuf_dot_any__pb2.DESCRIPTOR,google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,]) - -_ENCODING = _descriptor.EnumDescriptor( - name='Encoding', - full_name='gnmi.Encoding', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='JSON', index=0, number=0, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='BYTES', index=1, number=1, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='PROTO', index=2, number=2, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='ASCII', index=3, number=3, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='JSON_IETF', index=4, number=4, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=3053, - serialized_end=3121, -) -_sym_db.RegisterEnumDescriptor(_ENCODING) - -Encoding = enum_type_wrapper.EnumTypeWrapper(_ENCODING) -_SUBSCRIPTIONMODE = _descriptor.EnumDescriptor( - name='SubscriptionMode', - full_name='gnmi.SubscriptionMode', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='TARGET_DEFINED', index=0, number=0, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='ON_CHANGE', index=1, number=1, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='SAMPLE', index=2, number=2, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=3123, - serialized_end=3188, -) -_sym_db.RegisterEnumDescriptor(_SUBSCRIPTIONMODE) - -SubscriptionMode = enum_type_wrapper.EnumTypeWrapper(_SUBSCRIPTIONMODE) -JSON = 0 -BYTES = 1 -PROTO = 2 -ASCII = 3 -JSON_IETF = 4 -TARGET_DEFINED = 0 -ON_CHANGE = 1 -SAMPLE = 2 - -GNMI_SERVICE_FIELD_NUMBER = 1001 -gnmi_service = _descriptor.FieldDescriptor( - name='gnmi_service', full_name='gnmi.gnmi_service', index=0, - number=1001, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=True, extension_scope=None, - options=None) - -_SUBSCRIPTIONLIST_MODE = _descriptor.EnumDescriptor( - name='Mode', - full_name='gnmi.SubscriptionList.Mode', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='STREAM', index=0, number=0, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='ONCE', index=1, number=1, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='POLL', index=2, number=2, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=1706, - serialized_end=1744, -) -_sym_db.RegisterEnumDescriptor(_SUBSCRIPTIONLIST_MODE) - -_UPDATERESULT_OPERATION = _descriptor.EnumDescriptor( - name='Operation', - full_name='gnmi.UpdateResult.Operation', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='INVALID', index=0, number=0, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='DELETE', index=1, number=1, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='REPLACE', index=2, number=2, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='UPDATE', index=3, number=3, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=2439, - serialized_end=2500, -) -_sym_db.RegisterEnumDescriptor(_UPDATERESULT_OPERATION) - -_GETREQUEST_DATATYPE = _descriptor.EnumDescriptor( - name='DataType', - full_name='gnmi.GetRequest.DataType', - filename=None, - file=DESCRIPTOR, - values=[ - _descriptor.EnumValueDescriptor( - name='ALL', index=0, number=0, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='CONFIG', index=1, number=1, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='STATE', index=2, number=2, - options=None, - type=None), - _descriptor.EnumValueDescriptor( - name='OPERATIONAL', index=3, number=3, - options=None, - type=None), - ], - containing_type=None, - options=None, - serialized_start=2683, - serialized_end=2742, -) -_sym_db.RegisterEnumDescriptor(_GETREQUEST_DATATYPE) - - -_NOTIFICATION = _descriptor.Descriptor( - name='Notification', - full_name='gnmi.Notification', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='timestamp', full_name='gnmi.Notification.timestamp', index=0, - number=1, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='prefix', full_name='gnmi.Notification.prefix', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='alias', full_name='gnmi.Notification.alias', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='update', full_name='gnmi.Notification.update', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='delete', full_name='gnmi.Notification.delete', index=4, - number=5, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=93, - serialized_end=227, -) - - -_UPDATE = _descriptor.Descriptor( - name='Update', - full_name='gnmi.Update', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='path', full_name='gnmi.Update.path', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='value', full_name='gnmi.Update.value', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001'))), - _descriptor.FieldDescriptor( - name='val', full_name='gnmi.Update.val', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='duplicates', full_name='gnmi.Update.duplicates', index=3, - number=4, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=229, - serialized_end=346, -) - - -_TYPEDVALUE = _descriptor.Descriptor( - name='TypedValue', - full_name='gnmi.TypedValue', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='string_val', full_name='gnmi.TypedValue.string_val', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='int_val', full_name='gnmi.TypedValue.int_val', index=1, - number=2, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='uint_val', full_name='gnmi.TypedValue.uint_val', index=2, - number=3, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='bool_val', full_name='gnmi.TypedValue.bool_val', index=3, - number=4, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='bytes_val', full_name='gnmi.TypedValue.bytes_val', index=4, - number=5, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='float_val', full_name='gnmi.TypedValue.float_val', index=5, - number=6, type=2, cpp_type=6, label=1, - has_default_value=False, default_value=float(0), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='decimal_val', full_name='gnmi.TypedValue.decimal_val', index=6, - number=7, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='leaflist_val', full_name='gnmi.TypedValue.leaflist_val', index=7, - number=8, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='any_val', full_name='gnmi.TypedValue.any_val', index=8, - number=9, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='json_val', full_name='gnmi.TypedValue.json_val', index=9, - number=10, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='json_ietf_val', full_name='gnmi.TypedValue.json_ietf_val', index=10, - number=11, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='ascii_val', full_name='gnmi.TypedValue.ascii_val', index=11, - number=12, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='value', full_name='gnmi.TypedValue.value', - index=0, containing_type=None, fields=[]), - ], - serialized_start=349, - serialized_end=683, -) - - -_PATH = _descriptor.Descriptor( - name='Path', - full_name='gnmi.Path', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='element', full_name='gnmi.Path.element', index=0, - number=1, type=9, cpp_type=9, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001'))), - _descriptor.FieldDescriptor( - name='origin', full_name='gnmi.Path.origin', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='elem', full_name='gnmi.Path.elem', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='target', full_name='gnmi.Path.target', index=3, - number=4, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=685, - serialized_end=774, -) - - -_PATHELEM_KEYENTRY = _descriptor.Descriptor( - name='KeyEntry', - full_name='gnmi.PathElem.KeyEntry', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='key', full_name='gnmi.PathElem.KeyEntry.key', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='value', full_name='gnmi.PathElem.KeyEntry.value', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=840, - serialized_end=882, -) - -_PATHELEM = _descriptor.Descriptor( - name='PathElem', - full_name='gnmi.PathElem', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='gnmi.PathElem.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='key', full_name='gnmi.PathElem.key', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[_PATHELEM_KEYENTRY, ], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=776, - serialized_end=882, -) - - -_VALUE = _descriptor.Descriptor( - name='Value', - full_name='gnmi.Value', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='value', full_name='gnmi.Value.value', index=0, - number=1, type=12, cpp_type=9, label=1, - has_default_value=False, default_value=_b(""), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='type', full_name='gnmi.Value.type', index=1, - number=2, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('\030\001')), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=884, - serialized_end=940, -) - - -_ERROR = _descriptor.Descriptor( - name='Error', - full_name='gnmi.Error', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='code', full_name='gnmi.Error.code', index=0, - number=1, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='message', full_name='gnmi.Error.message', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='data', full_name='gnmi.Error.data', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('\030\001')), - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=942, - serialized_end=1020, -) - - -_DECIMAL64 = _descriptor.Descriptor( - name='Decimal64', - full_name='gnmi.Decimal64', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='digits', full_name='gnmi.Decimal64.digits', index=0, - number=1, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='precision', full_name='gnmi.Decimal64.precision', index=1, - number=2, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1022, - serialized_end=1068, -) - - -_SCALARARRAY = _descriptor.Descriptor( - name='ScalarArray', - full_name='gnmi.ScalarArray', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='element', full_name='gnmi.ScalarArray.element', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1070, - serialized_end=1118, -) - - -_SUBSCRIBEREQUEST = _descriptor.Descriptor( - name='SubscribeRequest', - full_name='gnmi.SubscribeRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='subscribe', full_name='gnmi.SubscribeRequest.subscribe', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='poll', full_name='gnmi.SubscribeRequest.poll', index=1, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='aliases', full_name='gnmi.SubscribeRequest.aliases', index=2, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='request', full_name='gnmi.SubscribeRequest.request', - index=0, containing_type=None, fields=[]), - ], - serialized_start=1121, - serialized_end=1259, -) - - -_POLL = _descriptor.Descriptor( - name='Poll', - full_name='gnmi.Poll', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1261, - serialized_end=1267, -) - - -_SUBSCRIBERESPONSE = _descriptor.Descriptor( - name='SubscribeResponse', - full_name='gnmi.SubscribeResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='update', full_name='gnmi.SubscribeResponse.update', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='sync_response', full_name='gnmi.SubscribeResponse.sync_response', index=1, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='error', full_name='gnmi.SubscribeResponse.error', index=2, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001'))), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - _descriptor.OneofDescriptor( - name='response', full_name='gnmi.SubscribeResponse.response', - index=0, containing_type=None, fields=[]), - ], - serialized_start=1270, - serialized_end=1398, -) - - -_SUBSCRIPTIONLIST = _descriptor.Descriptor( - name='SubscriptionList', - full_name='gnmi.SubscriptionList', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='prefix', full_name='gnmi.SubscriptionList.prefix', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='subscription', full_name='gnmi.SubscriptionList.subscription', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='use_aliases', full_name='gnmi.SubscriptionList.use_aliases', index=2, - number=3, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='qos', full_name='gnmi.SubscriptionList.qos', index=3, - number=4, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='mode', full_name='gnmi.SubscriptionList.mode', index=4, - number=5, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='allow_aggregation', full_name='gnmi.SubscriptionList.allow_aggregation', index=5, - number=6, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='use_models', full_name='gnmi.SubscriptionList.use_models', index=6, - number=7, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='encoding', full_name='gnmi.SubscriptionList.encoding', index=7, - number=8, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='updates_only', full_name='gnmi.SubscriptionList.updates_only', index=8, - number=9, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _SUBSCRIPTIONLIST_MODE, - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1401, - serialized_end=1744, -) - - -_SUBSCRIPTION = _descriptor.Descriptor( - name='Subscription', - full_name='gnmi.Subscription', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='path', full_name='gnmi.Subscription.path', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='mode', full_name='gnmi.Subscription.mode', index=1, - number=2, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='sample_interval', full_name='gnmi.Subscription.sample_interval', index=2, - number=3, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='suppress_redundant', full_name='gnmi.Subscription.suppress_redundant', index=3, - number=4, type=8, cpp_type=7, label=1, - has_default_value=False, default_value=False, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='heartbeat_interval', full_name='gnmi.Subscription.heartbeat_interval', index=4, - number=5, type=4, cpp_type=4, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1747, - serialized_end=1906, -) - - -_QOSMARKING = _descriptor.Descriptor( - name='QOSMarking', - full_name='gnmi.QOSMarking', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='marking', full_name='gnmi.QOSMarking.marking', index=0, - number=1, type=13, cpp_type=3, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1908, - serialized_end=1937, -) - - -_ALIAS = _descriptor.Descriptor( - name='Alias', - full_name='gnmi.Alias', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='path', full_name='gnmi.Alias.path', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='alias', full_name='gnmi.Alias.alias', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1939, - serialized_end=1987, -) - - -_ALIASLIST = _descriptor.Descriptor( - name='AliasList', - full_name='gnmi.AliasList', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='alias', full_name='gnmi.AliasList.alias', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=1989, - serialized_end=2028, -) - - -_SETREQUEST = _descriptor.Descriptor( - name='SetRequest', - full_name='gnmi.SetRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='prefix', full_name='gnmi.SetRequest.prefix', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='delete', full_name='gnmi.SetRequest.delete', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='replace', full_name='gnmi.SetRequest.replace', index=2, - number=3, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='update', full_name='gnmi.SetRequest.update', index=3, - number=4, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2031, - serialized_end=2160, -) - - -_SETRESPONSE = _descriptor.Descriptor( - name='SetResponse', - full_name='gnmi.SetResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='prefix', full_name='gnmi.SetResponse.prefix', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='response', full_name='gnmi.SetResponse.response', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='message', full_name='gnmi.SetResponse.message', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001'))), - _descriptor.FieldDescriptor( - name='timestamp', full_name='gnmi.SetResponse.timestamp', index=3, - number=4, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2163, - serialized_end=2295, -) - - -_UPDATERESULT = _descriptor.Descriptor( - name='UpdateResult', - full_name='gnmi.UpdateResult', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='timestamp', full_name='gnmi.UpdateResult.timestamp', index=0, - number=1, type=3, cpp_type=2, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001'))), - _descriptor.FieldDescriptor( - name='path', full_name='gnmi.UpdateResult.path', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='message', full_name='gnmi.UpdateResult.message', index=2, - number=3, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001'))), - _descriptor.FieldDescriptor( - name='op', full_name='gnmi.UpdateResult.op', index=3, - number=4, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _UPDATERESULT_OPERATION, - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2298, - serialized_end=2500, -) - - -_GETREQUEST = _descriptor.Descriptor( - name='GetRequest', - full_name='gnmi.GetRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='prefix', full_name='gnmi.GetRequest.prefix', index=0, - number=1, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='path', full_name='gnmi.GetRequest.path', index=1, - number=2, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='type', full_name='gnmi.GetRequest.type', index=2, - number=3, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='encoding', full_name='gnmi.GetRequest.encoding', index=3, - number=5, type=14, cpp_type=8, label=1, - has_default_value=False, default_value=0, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='use_models', full_name='gnmi.GetRequest.use_models', index=4, - number=6, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - _GETREQUEST_DATATYPE, - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2503, - serialized_end=2742, -) - - -_GETRESPONSE = _descriptor.Descriptor( - name='GetResponse', - full_name='gnmi.GetResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='notification', full_name='gnmi.GetResponse.notification', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='error', full_name='gnmi.GetResponse.error', index=1, - number=2, type=11, cpp_type=10, label=1, - has_default_value=False, default_value=None, - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001'))), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2744, - serialized_end=2831, -) - - -_CAPABILITYREQUEST = _descriptor.Descriptor( - name='CapabilityRequest', - full_name='gnmi.CapabilityRequest', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2833, - serialized_end=2852, -) - - -_CAPABILITYRESPONSE = _descriptor.Descriptor( - name='CapabilityResponse', - full_name='gnmi.CapabilityResponse', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='supported_models', full_name='gnmi.CapabilityResponse.supported_models', index=0, - number=1, type=11, cpp_type=10, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='supported_encodings', full_name='gnmi.CapabilityResponse.supported_encodings', index=1, - number=2, type=14, cpp_type=8, label=3, - has_default_value=False, default_value=[], - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='gNMI_version', full_name='gnmi.CapabilityResponse.gNMI_version', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2855, - serialized_end=2985, -) - - -_MODELDATA = _descriptor.Descriptor( - name='ModelData', - full_name='gnmi.ModelData', - filename=None, - file=DESCRIPTOR, - containing_type=None, - fields=[ - _descriptor.FieldDescriptor( - name='name', full_name='gnmi.ModelData.name', index=0, - number=1, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='organization', full_name='gnmi.ModelData.organization', index=1, - number=2, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - _descriptor.FieldDescriptor( - name='version', full_name='gnmi.ModelData.version', index=2, - number=3, type=9, cpp_type=9, label=1, - has_default_value=False, default_value=_b("").decode('utf-8'), - message_type=None, enum_type=None, containing_type=None, - is_extension=False, extension_scope=None, - options=None), - ], - extensions=[ - ], - nested_types=[], - enum_types=[ - ], - options=None, - is_extendable=False, - syntax='proto3', - extension_ranges=[], - oneofs=[ - ], - serialized_start=2987, - serialized_end=3051, -) - -_NOTIFICATION.fields_by_name['prefix'].message_type = _PATH -_NOTIFICATION.fields_by_name['update'].message_type = _UPDATE -_NOTIFICATION.fields_by_name['delete'].message_type = _PATH -_UPDATE.fields_by_name['path'].message_type = _PATH -_UPDATE.fields_by_name['value'].message_type = _VALUE -_UPDATE.fields_by_name['val'].message_type = _TYPEDVALUE -_TYPEDVALUE.fields_by_name['decimal_val'].message_type = _DECIMAL64 -_TYPEDVALUE.fields_by_name['leaflist_val'].message_type = _SCALARARRAY -_TYPEDVALUE.fields_by_name['any_val'].message_type = google_dot_protobuf_dot_any__pb2._ANY -_TYPEDVALUE.oneofs_by_name['value'].fields.append( - _TYPEDVALUE.fields_by_name['string_val']) -_TYPEDVALUE.fields_by_name['string_val'].containing_oneof = _TYPEDVALUE.oneofs_by_name['value'] -_TYPEDVALUE.oneofs_by_name['value'].fields.append( - _TYPEDVALUE.fields_by_name['int_val']) -_TYPEDVALUE.fields_by_name['int_val'].containing_oneof = _TYPEDVALUE.oneofs_by_name['value'] -_TYPEDVALUE.oneofs_by_name['value'].fields.append( - _TYPEDVALUE.fields_by_name['uint_val']) -_TYPEDVALUE.fields_by_name['uint_val'].containing_oneof = _TYPEDVALUE.oneofs_by_name['value'] -_TYPEDVALUE.oneofs_by_name['value'].fields.append( - _TYPEDVALUE.fields_by_name['bool_val']) -_TYPEDVALUE.fields_by_name['bool_val'].containing_oneof = _TYPEDVALUE.oneofs_by_name['value'] -_TYPEDVALUE.oneofs_by_name['value'].fields.append( - _TYPEDVALUE.fields_by_name['bytes_val']) -_TYPEDVALUE.fields_by_name['bytes_val'].containing_oneof = _TYPEDVALUE.oneofs_by_name['value'] -_TYPEDVALUE.oneofs_by_name['value'].fields.append( - _TYPEDVALUE.fields_by_name['float_val']) -_TYPEDVALUE.fields_by_name['float_val'].containing_oneof = _TYPEDVALUE.oneofs_by_name['value'] -_TYPEDVALUE.oneofs_by_name['value'].fields.append( - _TYPEDVALUE.fields_by_name['decimal_val']) -_TYPEDVALUE.fields_by_name['decimal_val'].containing_oneof = _TYPEDVALUE.oneofs_by_name['value'] -_TYPEDVALUE.oneofs_by_name['value'].fields.append( - _TYPEDVALUE.fields_by_name['leaflist_val']) -_TYPEDVALUE.fields_by_name['leaflist_val'].containing_oneof = _TYPEDVALUE.oneofs_by_name['value'] -_TYPEDVALUE.oneofs_by_name['value'].fields.append( - _TYPEDVALUE.fields_by_name['any_val']) -_TYPEDVALUE.fields_by_name['any_val'].containing_oneof = _TYPEDVALUE.oneofs_by_name['value'] -_TYPEDVALUE.oneofs_by_name['value'].fields.append( - _TYPEDVALUE.fields_by_name['json_val']) -_TYPEDVALUE.fields_by_name['json_val'].containing_oneof = _TYPEDVALUE.oneofs_by_name['value'] -_TYPEDVALUE.oneofs_by_name['value'].fields.append( - _TYPEDVALUE.fields_by_name['json_ietf_val']) -_TYPEDVALUE.fields_by_name['json_ietf_val'].containing_oneof = _TYPEDVALUE.oneofs_by_name['value'] -_TYPEDVALUE.oneofs_by_name['value'].fields.append( - _TYPEDVALUE.fields_by_name['ascii_val']) -_TYPEDVALUE.fields_by_name['ascii_val'].containing_oneof = _TYPEDVALUE.oneofs_by_name['value'] -_PATH.fields_by_name['elem'].message_type = _PATHELEM -_PATHELEM_KEYENTRY.containing_type = _PATHELEM -_PATHELEM.fields_by_name['key'].message_type = _PATHELEM_KEYENTRY -_VALUE.fields_by_name['type'].enum_type = _ENCODING -_ERROR.fields_by_name['data'].message_type = google_dot_protobuf_dot_any__pb2._ANY -_SCALARARRAY.fields_by_name['element'].message_type = _TYPEDVALUE -_SUBSCRIBEREQUEST.fields_by_name['subscribe'].message_type = _SUBSCRIPTIONLIST -_SUBSCRIBEREQUEST.fields_by_name['poll'].message_type = _POLL -_SUBSCRIBEREQUEST.fields_by_name['aliases'].message_type = _ALIASLIST -_SUBSCRIBEREQUEST.oneofs_by_name['request'].fields.append( - _SUBSCRIBEREQUEST.fields_by_name['subscribe']) -_SUBSCRIBEREQUEST.fields_by_name['subscribe'].containing_oneof = _SUBSCRIBEREQUEST.oneofs_by_name['request'] -_SUBSCRIBEREQUEST.oneofs_by_name['request'].fields.append( - _SUBSCRIBEREQUEST.fields_by_name['poll']) -_SUBSCRIBEREQUEST.fields_by_name['poll'].containing_oneof = _SUBSCRIBEREQUEST.oneofs_by_name['request'] -_SUBSCRIBEREQUEST.oneofs_by_name['request'].fields.append( - _SUBSCRIBEREQUEST.fields_by_name['aliases']) -_SUBSCRIBEREQUEST.fields_by_name['aliases'].containing_oneof = _SUBSCRIBEREQUEST.oneofs_by_name['request'] -_SUBSCRIBERESPONSE.fields_by_name['update'].message_type = _NOTIFICATION -_SUBSCRIBERESPONSE.fields_by_name['error'].message_type = _ERROR -_SUBSCRIBERESPONSE.oneofs_by_name['response'].fields.append( - _SUBSCRIBERESPONSE.fields_by_name['update']) -_SUBSCRIBERESPONSE.fields_by_name['update'].containing_oneof = _SUBSCRIBERESPONSE.oneofs_by_name['response'] -_SUBSCRIBERESPONSE.oneofs_by_name['response'].fields.append( - _SUBSCRIBERESPONSE.fields_by_name['sync_response']) -_SUBSCRIBERESPONSE.fields_by_name['sync_response'].containing_oneof = _SUBSCRIBERESPONSE.oneofs_by_name['response'] -_SUBSCRIBERESPONSE.oneofs_by_name['response'].fields.append( - _SUBSCRIBERESPONSE.fields_by_name['error']) -_SUBSCRIBERESPONSE.fields_by_name['error'].containing_oneof = _SUBSCRIBERESPONSE.oneofs_by_name['response'] -_SUBSCRIPTIONLIST.fields_by_name['prefix'].message_type = _PATH -_SUBSCRIPTIONLIST.fields_by_name['subscription'].message_type = _SUBSCRIPTION -_SUBSCRIPTIONLIST.fields_by_name['qos'].message_type = _QOSMARKING -_SUBSCRIPTIONLIST.fields_by_name['mode'].enum_type = _SUBSCRIPTIONLIST_MODE -_SUBSCRIPTIONLIST.fields_by_name['use_models'].message_type = _MODELDATA -_SUBSCRIPTIONLIST.fields_by_name['encoding'].enum_type = _ENCODING -_SUBSCRIPTIONLIST_MODE.containing_type = _SUBSCRIPTIONLIST -_SUBSCRIPTION.fields_by_name['path'].message_type = _PATH -_SUBSCRIPTION.fields_by_name['mode'].enum_type = _SUBSCRIPTIONMODE -_ALIAS.fields_by_name['path'].message_type = _PATH -_ALIASLIST.fields_by_name['alias'].message_type = _ALIAS -_SETREQUEST.fields_by_name['prefix'].message_type = _PATH -_SETREQUEST.fields_by_name['delete'].message_type = _PATH -_SETREQUEST.fields_by_name['replace'].message_type = _UPDATE -_SETREQUEST.fields_by_name['update'].message_type = _UPDATE -_SETRESPONSE.fields_by_name['prefix'].message_type = _PATH -_SETRESPONSE.fields_by_name['response'].message_type = _UPDATERESULT -_SETRESPONSE.fields_by_name['message'].message_type = _ERROR -_UPDATERESULT.fields_by_name['path'].message_type = _PATH -_UPDATERESULT.fields_by_name['message'].message_type = _ERROR -_UPDATERESULT.fields_by_name['op'].enum_type = _UPDATERESULT_OPERATION -_UPDATERESULT_OPERATION.containing_type = _UPDATERESULT -_GETREQUEST.fields_by_name['prefix'].message_type = _PATH -_GETREQUEST.fields_by_name['path'].message_type = _PATH -_GETREQUEST.fields_by_name['type'].enum_type = _GETREQUEST_DATATYPE -_GETREQUEST.fields_by_name['encoding'].enum_type = _ENCODING -_GETREQUEST.fields_by_name['use_models'].message_type = _MODELDATA -_GETREQUEST_DATATYPE.containing_type = _GETREQUEST -_GETRESPONSE.fields_by_name['notification'].message_type = _NOTIFICATION -_GETRESPONSE.fields_by_name['error'].message_type = _ERROR -_CAPABILITYRESPONSE.fields_by_name['supported_models'].message_type = _MODELDATA -_CAPABILITYRESPONSE.fields_by_name['supported_encodings'].enum_type = _ENCODING -DESCRIPTOR.message_types_by_name['Notification'] = _NOTIFICATION -DESCRIPTOR.message_types_by_name['Update'] = _UPDATE -DESCRIPTOR.message_types_by_name['TypedValue'] = _TYPEDVALUE -DESCRIPTOR.message_types_by_name['Path'] = _PATH -DESCRIPTOR.message_types_by_name['PathElem'] = _PATHELEM -DESCRIPTOR.message_types_by_name['Value'] = _VALUE -DESCRIPTOR.message_types_by_name['Error'] = _ERROR -DESCRIPTOR.message_types_by_name['Decimal64'] = _DECIMAL64 -DESCRIPTOR.message_types_by_name['ScalarArray'] = _SCALARARRAY -DESCRIPTOR.message_types_by_name['SubscribeRequest'] = _SUBSCRIBEREQUEST -DESCRIPTOR.message_types_by_name['Poll'] = _POLL -DESCRIPTOR.message_types_by_name['SubscribeResponse'] = _SUBSCRIBERESPONSE -DESCRIPTOR.message_types_by_name['SubscriptionList'] = _SUBSCRIPTIONLIST -DESCRIPTOR.message_types_by_name['Subscription'] = _SUBSCRIPTION -DESCRIPTOR.message_types_by_name['QOSMarking'] = _QOSMARKING -DESCRIPTOR.message_types_by_name['Alias'] = _ALIAS -DESCRIPTOR.message_types_by_name['AliasList'] = _ALIASLIST -DESCRIPTOR.message_types_by_name['SetRequest'] = _SETREQUEST -DESCRIPTOR.message_types_by_name['SetResponse'] = _SETRESPONSE -DESCRIPTOR.message_types_by_name['UpdateResult'] = _UPDATERESULT -DESCRIPTOR.message_types_by_name['GetRequest'] = _GETREQUEST -DESCRIPTOR.message_types_by_name['GetResponse'] = _GETRESPONSE -DESCRIPTOR.message_types_by_name['CapabilityRequest'] = _CAPABILITYREQUEST -DESCRIPTOR.message_types_by_name['CapabilityResponse'] = _CAPABILITYRESPONSE -DESCRIPTOR.message_types_by_name['ModelData'] = _MODELDATA -DESCRIPTOR.enum_types_by_name['Encoding'] = _ENCODING -DESCRIPTOR.enum_types_by_name['SubscriptionMode'] = _SUBSCRIPTIONMODE -DESCRIPTOR.extensions_by_name['gnmi_service'] = gnmi_service -_sym_db.RegisterFileDescriptor(DESCRIPTOR) - -Notification = _reflection.GeneratedProtocolMessageType('Notification', (_message.Message,), dict( - DESCRIPTOR = _NOTIFICATION, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.Notification) - )) -_sym_db.RegisterMessage(Notification) - -Update = _reflection.GeneratedProtocolMessageType('Update', (_message.Message,), dict( - DESCRIPTOR = _UPDATE, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.Update) - )) -_sym_db.RegisterMessage(Update) - -TypedValue = _reflection.GeneratedProtocolMessageType('TypedValue', (_message.Message,), dict( - DESCRIPTOR = _TYPEDVALUE, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.TypedValue) - )) -_sym_db.RegisterMessage(TypedValue) - -Path = _reflection.GeneratedProtocolMessageType('Path', (_message.Message,), dict( - DESCRIPTOR = _PATH, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.Path) - )) -_sym_db.RegisterMessage(Path) - -PathElem = _reflection.GeneratedProtocolMessageType('PathElem', (_message.Message,), dict( - - KeyEntry = _reflection.GeneratedProtocolMessageType('KeyEntry', (_message.Message,), dict( - DESCRIPTOR = _PATHELEM_KEYENTRY, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.PathElem.KeyEntry) - )) - , - DESCRIPTOR = _PATHELEM, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.PathElem) - )) -_sym_db.RegisterMessage(PathElem) -_sym_db.RegisterMessage(PathElem.KeyEntry) - -Value = _reflection.GeneratedProtocolMessageType('Value', (_message.Message,), dict( - DESCRIPTOR = _VALUE, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.Value) - )) -_sym_db.RegisterMessage(Value) - -Error = _reflection.GeneratedProtocolMessageType('Error', (_message.Message,), dict( - DESCRIPTOR = _ERROR, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.Error) - )) -_sym_db.RegisterMessage(Error) - -Decimal64 = _reflection.GeneratedProtocolMessageType('Decimal64', (_message.Message,), dict( - DESCRIPTOR = _DECIMAL64, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.Decimal64) - )) -_sym_db.RegisterMessage(Decimal64) - -ScalarArray = _reflection.GeneratedProtocolMessageType('ScalarArray', (_message.Message,), dict( - DESCRIPTOR = _SCALARARRAY, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.ScalarArray) - )) -_sym_db.RegisterMessage(ScalarArray) - -SubscribeRequest = _reflection.GeneratedProtocolMessageType('SubscribeRequest', (_message.Message,), dict( - DESCRIPTOR = _SUBSCRIBEREQUEST, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.SubscribeRequest) - )) -_sym_db.RegisterMessage(SubscribeRequest) - -Poll = _reflection.GeneratedProtocolMessageType('Poll', (_message.Message,), dict( - DESCRIPTOR = _POLL, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.Poll) - )) -_sym_db.RegisterMessage(Poll) - -SubscribeResponse = _reflection.GeneratedProtocolMessageType('SubscribeResponse', (_message.Message,), dict( - DESCRIPTOR = _SUBSCRIBERESPONSE, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.SubscribeResponse) - )) -_sym_db.RegisterMessage(SubscribeResponse) - -SubscriptionList = _reflection.GeneratedProtocolMessageType('SubscriptionList', (_message.Message,), dict( - DESCRIPTOR = _SUBSCRIPTIONLIST, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.SubscriptionList) - )) -_sym_db.RegisterMessage(SubscriptionList) - -Subscription = _reflection.GeneratedProtocolMessageType('Subscription', (_message.Message,), dict( - DESCRIPTOR = _SUBSCRIPTION, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.Subscription) - )) -_sym_db.RegisterMessage(Subscription) - -QOSMarking = _reflection.GeneratedProtocolMessageType('QOSMarking', (_message.Message,), dict( - DESCRIPTOR = _QOSMARKING, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.QOSMarking) - )) -_sym_db.RegisterMessage(QOSMarking) - -Alias = _reflection.GeneratedProtocolMessageType('Alias', (_message.Message,), dict( - DESCRIPTOR = _ALIAS, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.Alias) - )) -_sym_db.RegisterMessage(Alias) - -AliasList = _reflection.GeneratedProtocolMessageType('AliasList', (_message.Message,), dict( - DESCRIPTOR = _ALIASLIST, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.AliasList) - )) -_sym_db.RegisterMessage(AliasList) - -SetRequest = _reflection.GeneratedProtocolMessageType('SetRequest', (_message.Message,), dict( - DESCRIPTOR = _SETREQUEST, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.SetRequest) - )) -_sym_db.RegisterMessage(SetRequest) - -SetResponse = _reflection.GeneratedProtocolMessageType('SetResponse', (_message.Message,), dict( - DESCRIPTOR = _SETRESPONSE, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.SetResponse) - )) -_sym_db.RegisterMessage(SetResponse) - -UpdateResult = _reflection.GeneratedProtocolMessageType('UpdateResult', (_message.Message,), dict( - DESCRIPTOR = _UPDATERESULT, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.UpdateResult) - )) -_sym_db.RegisterMessage(UpdateResult) - -GetRequest = _reflection.GeneratedProtocolMessageType('GetRequest', (_message.Message,), dict( - DESCRIPTOR = _GETREQUEST, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.GetRequest) - )) -_sym_db.RegisterMessage(GetRequest) - -GetResponse = _reflection.GeneratedProtocolMessageType('GetResponse', (_message.Message,), dict( - DESCRIPTOR = _GETRESPONSE, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.GetResponse) - )) -_sym_db.RegisterMessage(GetResponse) - -CapabilityRequest = _reflection.GeneratedProtocolMessageType('CapabilityRequest', (_message.Message,), dict( - DESCRIPTOR = _CAPABILITYREQUEST, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.CapabilityRequest) - )) -_sym_db.RegisterMessage(CapabilityRequest) - -CapabilityResponse = _reflection.GeneratedProtocolMessageType('CapabilityResponse', (_message.Message,), dict( - DESCRIPTOR = _CAPABILITYRESPONSE, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.CapabilityResponse) - )) -_sym_db.RegisterMessage(CapabilityResponse) - -ModelData = _reflection.GeneratedProtocolMessageType('ModelData', (_message.Message,), dict( - DESCRIPTOR = _MODELDATA, - __module__ = 'proto.gnmi.gnmi_pb2' - # @@protoc_insertion_point(class_scope:gnmi.ModelData) - )) -_sym_db.RegisterMessage(ModelData) - -google_dot_protobuf_dot_descriptor__pb2.FileOptions.RegisterExtension(gnmi_service) - -DESCRIPTOR.has_options = True -DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\312>\0050.5.0')) -_UPDATE.fields_by_name['value'].has_options = True -_UPDATE.fields_by_name['value']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001')) -_PATH.fields_by_name['element'].has_options = True -_PATH.fields_by_name['element']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001')) -_PATHELEM_KEYENTRY.has_options = True -_PATHELEM_KEYENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')) -_VALUE.has_options = True -_VALUE._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('\030\001')) -_ERROR.has_options = True -_ERROR._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('\030\001')) -_SUBSCRIBERESPONSE.fields_by_name['error'].has_options = True -_SUBSCRIBERESPONSE.fields_by_name['error']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001')) -_SETRESPONSE.fields_by_name['message'].has_options = True -_SETRESPONSE.fields_by_name['message']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001')) -_UPDATERESULT.fields_by_name['timestamp'].has_options = True -_UPDATERESULT.fields_by_name['timestamp']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001')) -_UPDATERESULT.fields_by_name['message'].has_options = True -_UPDATERESULT.fields_by_name['message']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001')) -_GETRESPONSE.fields_by_name['error'].has_options = True -_GETRESPONSE.fields_by_name['error']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), _b('\030\001')) -try: - # THESE ELEMENTS WILL BE DEPRECATED. - # Please use the generated *_pb2_grpc.py files instead. - import grpc - from grpc.beta import implementations as beta_implementations - from grpc.beta import interfaces as beta_interfaces - from grpc.framework.common import cardinality - from grpc.framework.interfaces.face import utilities as face_utilities - - - class gNMIStub(object): - # missing associated documentation comment in .proto file - pass - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.Capabilities = channel.unary_unary( - '/gnmi.gNMI/Capabilities', - request_serializer=CapabilityRequest.SerializeToString, - response_deserializer=CapabilityResponse.FromString, - ) - self.Get = channel.unary_unary( - '/gnmi.gNMI/Get', - request_serializer=GetRequest.SerializeToString, - response_deserializer=GetResponse.FromString, - ) - self.Set = channel.unary_unary( - '/gnmi.gNMI/Set', - request_serializer=SetRequest.SerializeToString, - response_deserializer=SetResponse.FromString, - ) - self.Subscribe = channel.stream_stream( - '/gnmi.gNMI/Subscribe', - request_serializer=SubscribeRequest.SerializeToString, - response_deserializer=SubscribeResponse.FromString, - ) - - - class gNMIServicer(object): - # missing associated documentation comment in .proto file - pass - - def Capabilities(self, request, context): - """Capabilities allows the client to retrieve the set of capabilities that - is supported by the target. This allows the target to validate the - service version that is implemented and retrieve the set of models that - the target supports. The models can then be specified in subsequent RPCs - to restrict the set of data that is utilized. - Reference: gNMI Specification Section 3.2 - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Get(self, request, context): - """Retrieve a snapshot of data from the target. A Get RPC requests that the - target snapshots a subset of the data tree as specified by the paths - included in the message and serializes this to be returned to the - client using the specified encoding. - Reference: gNMI Specification Section 3.3 - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Set(self, request, context): - """Set allows the client to modify the state of data on the target. The - paths to modified along with the new values that the client wishes - to set the value to. - Reference: gNMI Specification Section 3.4 - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - def Subscribe(self, request_iterator, context): - """Subscribe allows a client to request the target to send it values - of particular paths within the data tree. These values may be streamed - at a particular cadence (STREAM), sent one off on a long-lived channel - (POLL), or sent as a one-off retrieval (ONCE). - Reference: gNMI Specification Section 3.5 - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - - def add_gNMIServicer_to_server(servicer, server): - rpc_method_handlers = { - 'Capabilities': grpc.unary_unary_rpc_method_handler( - servicer.Capabilities, - request_deserializer=CapabilityRequest.FromString, - response_serializer=CapabilityResponse.SerializeToString, - ), - 'Get': grpc.unary_unary_rpc_method_handler( - servicer.Get, - request_deserializer=GetRequest.FromString, - response_serializer=GetResponse.SerializeToString, - ), - 'Set': grpc.unary_unary_rpc_method_handler( - servicer.Set, - request_deserializer=SetRequest.FromString, - response_serializer=SetResponse.SerializeToString, - ), - 'Subscribe': grpc.stream_stream_rpc_method_handler( - servicer.Subscribe, - request_deserializer=SubscribeRequest.FromString, - response_serializer=SubscribeResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'gnmi.gNMI', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - - - class BetagNMIServicer(object): - """The Beta API is deprecated for 0.15.0 and later. - - It is recommended to use the GA API (classes and functions in this - file not marked beta) for all further purposes. This class was generated - only to ease transition from grpcio<0.15.0 to grpcio>=0.15.0.""" - # missing associated documentation comment in .proto file - pass - def Capabilities(self, request, context): - """Capabilities allows the client to retrieve the set of capabilities that - is supported by the target. This allows the target to validate the - service version that is implemented and retrieve the set of models that - the target supports. The models can then be specified in subsequent RPCs - to restrict the set of data that is utilized. - Reference: gNMI Specification Section 3.2 - """ - context.code(beta_interfaces.StatusCode.UNIMPLEMENTED) - def Get(self, request, context): - """Retrieve a snapshot of data from the target. A Get RPC requests that the - target snapshots a subset of the data tree as specified by the paths - included in the message and serializes this to be returned to the - client using the specified encoding. - Reference: gNMI Specification Section 3.3 - """ - context.code(beta_interfaces.StatusCode.UNIMPLEMENTED) - def Set(self, request, context): - """Set allows the client to modify the state of data on the target. The - paths to modified along with the new values that the client wishes - to set the value to. - Reference: gNMI Specification Section 3.4 - """ - context.code(beta_interfaces.StatusCode.UNIMPLEMENTED) - def Subscribe(self, request_iterator, context): - """Subscribe allows a client to request the target to send it values - of particular paths within the data tree. These values may be streamed - at a particular cadence (STREAM), sent one off on a long-lived channel - (POLL), or sent as a one-off retrieval (ONCE). - Reference: gNMI Specification Section 3.5 - """ - context.code(beta_interfaces.StatusCode.UNIMPLEMENTED) - - - class BetagNMIStub(object): - """The Beta API is deprecated for 0.15.0 and later. - - It is recommended to use the GA API (classes and functions in this - file not marked beta) for all further purposes. This class was generated - only to ease transition from grpcio<0.15.0 to grpcio>=0.15.0.""" - # missing associated documentation comment in .proto file - pass - def Capabilities(self, request, timeout, metadata=None, with_call=False, protocol_options=None): - """Capabilities allows the client to retrieve the set of capabilities that - is supported by the target. This allows the target to validate the - service version that is implemented and retrieve the set of models that - the target supports. The models can then be specified in subsequent RPCs - to restrict the set of data that is utilized. - Reference: gNMI Specification Section 3.2 - """ - raise NotImplementedError() - Capabilities.future = None - def Get(self, request, timeout, metadata=None, with_call=False, protocol_options=None): - """Retrieve a snapshot of data from the target. A Get RPC requests that the - target snapshots a subset of the data tree as specified by the paths - included in the message and serializes this to be returned to the - client using the specified encoding. - Reference: gNMI Specification Section 3.3 - """ - raise NotImplementedError() - Get.future = None - def Set(self, request, timeout, metadata=None, with_call=False, protocol_options=None): - """Set allows the client to modify the state of data on the target. The - paths to modified along with the new values that the client wishes - to set the value to. - Reference: gNMI Specification Section 3.4 - """ - raise NotImplementedError() - Set.future = None - def Subscribe(self, request_iterator, timeout, metadata=None, with_call=False, protocol_options=None): - """Subscribe allows a client to request the target to send it values - of particular paths within the data tree. These values may be streamed - at a particular cadence (STREAM), sent one off on a long-lived channel - (POLL), or sent as a one-off retrieval (ONCE). - Reference: gNMI Specification Section 3.5 - """ - raise NotImplementedError() - - - def beta_create_gNMI_server(servicer, pool=None, pool_size=None, default_timeout=None, maximum_timeout=None): - """The Beta API is deprecated for 0.15.0 and later. - - It is recommended to use the GA API (classes and functions in this - file not marked beta) for all further purposes. This function was - generated only to ease transition from grpcio<0.15.0 to grpcio>=0.15.0""" - request_deserializers = { - ('gnmi.gNMI', 'Capabilities'): CapabilityRequest.FromString, - ('gnmi.gNMI', 'Get'): GetRequest.FromString, - ('gnmi.gNMI', 'Set'): SetRequest.FromString, - ('gnmi.gNMI', 'Subscribe'): SubscribeRequest.FromString, - } - response_serializers = { - ('gnmi.gNMI', 'Capabilities'): CapabilityResponse.SerializeToString, - ('gnmi.gNMI', 'Get'): GetResponse.SerializeToString, - ('gnmi.gNMI', 'Set'): SetResponse.SerializeToString, - ('gnmi.gNMI', 'Subscribe'): SubscribeResponse.SerializeToString, - } - method_implementations = { - ('gnmi.gNMI', 'Capabilities'): face_utilities.unary_unary_inline(servicer.Capabilities), - ('gnmi.gNMI', 'Get'): face_utilities.unary_unary_inline(servicer.Get), - ('gnmi.gNMI', 'Set'): face_utilities.unary_unary_inline(servicer.Set), - ('gnmi.gNMI', 'Subscribe'): face_utilities.stream_stream_inline(servicer.Subscribe), - } - server_options = beta_implementations.server_options(request_deserializers=request_deserializers, response_serializers=response_serializers, thread_pool=pool, thread_pool_size=pool_size, default_timeout=default_timeout, maximum_timeout=maximum_timeout) - return beta_implementations.server(method_implementations, options=server_options) - - - def beta_create_gNMI_stub(channel, host=None, metadata_transformer=None, pool=None, pool_size=None): - """The Beta API is deprecated for 0.15.0 and later. - - It is recommended to use the GA API (classes and functions in this - file not marked beta) for all further purposes. This function was - generated only to ease transition from grpcio<0.15.0 to grpcio>=0.15.0""" - request_serializers = { - ('gnmi.gNMI', 'Capabilities'): CapabilityRequest.SerializeToString, - ('gnmi.gNMI', 'Get'): GetRequest.SerializeToString, - ('gnmi.gNMI', 'Set'): SetRequest.SerializeToString, - ('gnmi.gNMI', 'Subscribe'): SubscribeRequest.SerializeToString, - } - response_deserializers = { - ('gnmi.gNMI', 'Capabilities'): CapabilityResponse.FromString, - ('gnmi.gNMI', 'Get'): GetResponse.FromString, - ('gnmi.gNMI', 'Set'): SetResponse.FromString, - ('gnmi.gNMI', 'Subscribe'): SubscribeResponse.FromString, - } - cardinalities = { - 'Capabilities': cardinality.Cardinality.UNARY_UNARY, - 'Get': cardinality.Cardinality.UNARY_UNARY, - 'Set': cardinality.Cardinality.UNARY_UNARY, - 'Subscribe': cardinality.Cardinality.STREAM_STREAM, - } - stub_options = beta_implementations.stub_options(host=host, metadata_transformer=metadata_transformer, request_serializers=request_serializers, response_deserializers=response_deserializers, thread_pool=pool, thread_pool_size=pool_size) - return beta_implementations.dynamic_stub(channel, 'gnmi.gNMI', cardinalities, options=stub_options) -except ImportError: - pass -# @@protoc_insertion_point(module_scope) diff --git a/napalm_srl/jsondiff.py b/napalm_srl/jsondiff.py deleted file mode 100644 index 5f079da..0000000 --- a/napalm_srl/jsondiff.py +++ /dev/null @@ -1,262 +0,0 @@ -# Copyright 2020 Nokia -# Licensed under the Apache License 2.0. -# SPDX-License-Identifier: Apache-2.0 - -import json -index_keys = ['name','id','index','sequence-id','buffer-name','address'] - -class jsondiff(object): - """ - Class to find json diff - """ - def __init__(self): - pass - def compare(self, newjsonfile, oldjsonfile): - with open(newjsonfile, "r") as f: - newjson = json.load(f) - with open(oldjsonfile, "r") as f: - oldjson = json.load(f) - return self.cmp_dict(newjson, oldjson) - - def cmp_dict(self, new, old, parent=""): - result=[] - keydiff = self._get_dict_key_diff(new, old) - - #step1 removed - for k in keydiff["---"]: - result.append({ - "---":"{}[{}]".format(parent, k), - "value":old[k] - }) - #step2 added - for k in keydiff["+++"]: - result.append({ - "+++":"{}[{}]".format(parent, k), - "value": new[k] - }) - - #step3 modified - for k in keydiff["common"]: - if isinstance(old[k] ,dict) and isinstance(new[k] ,dict): - res = self.cmp_dict(new[k], old[k], parent ="{}[{}]".format(parent, k)) - result.extend(res) - elif isinstance(old[k], list) and isinstance(new[k] ,list): - res = self._cmp_list(new[k],old[k],parent ="{}[{}]".format(parent,k)) - result.extend(res) - else: - if not old[k] == new[k]: - result.append({ - "chg": "{}[{}]".format(parent, k), - "old_value": old[k], - "new_value":new[k] - }) - - return result - - def _find_index_key(self, old_dicts): - max_ct = 0 - key = None - for i in index_keys: - ct = 0 - for o in old_dicts: - for k,v in o.items(): - if k==i: - ct = ct +1 - continue - if ct >max_ct: - max_ct = ct - key = i - if not max_ct==0: - return key - #find key either string or int and present in all/many dict and is unique - - #print("{}\nType index key for above list:".format(old_dicts)) - #return str(input()) - #=========================== - #find if there is a key ending with id/index/address/name and values unique - suffixes = ['id', 'index', 'address', 'name'] - key = None - max_ct = 0 - for s in suffixes: - ct = 0 - vals = [] - ct_key = None - for o in old_dicts: - for k,v in o.items(): - if str(k).lower().endswith(s): - ct = ct + 1 - vals.append(v) - ct_key = k - continue - if ct > max_ct and self.are_values_unique(vals): #unique and maximum count - max_ct = ct - key = ct_key - if not max_ct == 0: - return key - - #int and is unique - max_ct = 0 - key = None - int_keys = [k for o in old_dicts for k,v in o.items() if isinstance(v,int)] - int_keys = list(set(int_keys)) - for i in int_keys: - ct = 0 - vals = [] - for o in old_dicts: - for k, v in o.items(): - if k == i: - ct = ct + 1 - vals.append(v) - continue - if ct > max_ct and self.are_values_unique(vals): #unique and maximum count: - max_ct = ct - key = i - if not max_ct == 0: - return key - - - #string without space and is unique - max_ct = 0 - key = None - str_keys = [k for o in old_dicts for k,v in o.items() if isinstance(v,str) and " " not in v] - str_keys = list(set(str_keys)) - for i in str_keys: - ct = 0 - vals = [] - for o in old_dicts: - for k, v in o.items(): - if k == i: - ct = ct + 1 - vals.append(v) - continue - if ct > max_ct and self.are_values_unique(vals): #unique and maximum count:: - max_ct = ct - key = i - if not max_ct == 0: - return key - return None - - - - - def are_values_unique(self, vals): - return len(set(vals)) == len(vals) - - def _cmp_list(self, new, old_in, parent=""): - result = [] - old = old_in[:] # to capture --- - old_dicts = [o for o in old if isinstance(o, dict)] - old_has_list = any([o for o in old if isinstance(o, list)]) - ind_key = None - if len(old_dicts) >1 : - ind_key = self._find_index_key(old_dicts) - for index,n in enumerate(new): - - # either n shd be in old - # or n not in old , n is a dict and n not have indexkey - # or n not in old , n is a dict and n have indexkey but no matching o - # or n not in old , n is a dict and n have indexkey but has matching o to compare - - if n in old_in: - if n in old: #if duplicat n is already removed. - old.remove(n) - continue - elif isinstance(n, dict): - if len(old_dicts) == 0: - result.append({ - "+++": "{}[{}]".format(parent, index), - "value": n - }) - continue - elif len(old_dicts) == 1: - res = self.cmp_dict(n,old_dicts[0],"{}[{}]".format(parent,index) ) - result.extend(res) - old.remove(old_dicts[0]) - continue - elif not ind_key: - max_equal_flds = 0 - old_to_compare = None - ct_available_old_dicts_for_comp = [o for o in old if isinstance(o, dict)] - for o in ct_available_old_dicts_for_comp: - equal_flds_ct = 0 - for k,v in n.items(): - if k in o and o[k] == v: - equal_flds_ct = equal_flds_ct + 1 - if equal_flds_ct > max_equal_flds: - max_equal_flds = equal_flds_ct - old_to_compare = o - if max_equal_flds == 0: - result.append({ - "+++": "{}[{}]".format(parent, index), - "value": n - }) - else: - res = self.cmp_dict(n, old_to_compare, "{}[{}]".format(parent, index)) - result.extend(res) - old.remove(old_to_compare) - continue - elif ind_key not in n.keys(): - result.append({ - "+++":"{}[{}]".format(parent,index), - "value": n - }) - continue - else: - ind_key_value = n[ind_key] - o_to_compare = [o for o in old if ind_key in o and o[ind_key]==ind_key_value] - if len(o_to_compare)==0: - result.append({ - "+++": "{}[{}]".format(parent, index), - "value": n - }) - continue - else: - o_to_compare = o_to_compare[0] - old.remove(o_to_compare) - res = self.cmp_dict(n, o_to_compare, "{}[{}]".format(parent, index)) - result.extend(res) - elif isinstance(n, list): - if not old_has_list: - result.append({ - "+++": "{}[{}]".format(parent, index), - "value": n - }) - continue - else: - for o in old: - if isinstance(o, list): - old.remove(o) - res = self._cmp_list(n,o,"{}[{}]".format(parent, index)) - result.extend(res) - continue - #if no more list available in o - result.append({ - "+++": "{}[{}]".format(parent, index), - "value": n - }) - continue - else: - result.append({ - "+++": "{}[{}]".format(parent, index), - "value": n - }) - if old: - for index,o in enumerate(old_in): - if o in old: - result.append({ - "---": "{}[{}]".format(parent, index), - "value": o - }) - return result - - def _get_dict_key_diff(self, new, old): - d1_keys = new.keys() - d2_keys = old.keys() - plus_ks = [k for k in d1_keys if k not in d2_keys] - minus_ks = [k for k in d2_keys if k not in d1_keys] - return { - "+++":plus_ks, - "---": minus_ks, - "common": [k for k in d1_keys if k not in plus_ks], - } - diff --git a/napalm_srl/srl.py b/napalm_srl/srl.py deleted file mode 100644 index d13eb0a..0000000 --- a/napalm_srl/srl.py +++ /dev/null @@ -1,2888 +0,0 @@ - # -*- coding: utf-8 -*- -# Copyright 2021 Nokia. All rights reserved. -# -# The contents of this file are licensed under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with the -# License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations under -# the License. -# SPDX-License-Identifier: Apache-2.0 - -""" -Napalm driver for SR Linux. - -Read https://napalm.readthedocs.io for more information. -""" -import base64 -import json -import logging -import os -import re -import datetime -import grpc -import tempfile -from google.protobuf import json_format -from napalm_srl import gnmi_pb2, jsondiff - -from napalm.base import NetworkDriver -from napalm.base.helpers import convert, mac, as_number -from napalm.base.exceptions import ( - ConnectionException, - MergeConfigException, - ReplaceConfigException, - CommandErrorException, - CommitError, -) - -import requests - -from requests.packages.urllib3.poolmanager import PoolManager -from requests.packages.urllib3.util import ssl_ -from requests.adapters import HTTPAdapter - -class TLSHttpAdapter(HTTPAdapter): - """ - "Transport adapter" to re-enable the ECDHE-RSA-AES256-SHA cipher as fallback - - urllib3 version 2.0.2 reduced the list of ciphers offered by default, - removing the ECDHE-RSA-AES256-SHA cipher. When the cipher list is left empty - in SR Linux CLI, by default it only accepts this cipher - """ - def __init__(self, ciphers=None, **kwargs): - self.ciphers = ciphers - super(TLSHttpAdapter, self).__init__(**kwargs) - - def init_poolmanager(self, connections, maxsize, block=False): - logging.warning( f"Enabled TLS ciphers: {self.ciphers}" ) - ctx = ssl_.create_urllib3_context(ciphers=self.ciphers,cert_reqs=ssl_.CERT_REQUIRED) - ctx.check_hostname = False # for some reason, CERT_REQUIRED becomes None - self.poolmanager = PoolManager( - num_pools=connections, maxsize=maxsize, - ssl_context=ctx, block=block) - -class NokiaSRLDriver(NetworkDriver): - """Napalm driver for SRL.""" - - def __init__(self, hostname, username, password, timeout=60, optional_args=None): - """Constructor.""" - self.device = None - self._metadata = None - # still need to figure out why these variables are used - self.config_session = None - self.locked = False - self.profile = ["srl"] - self.platform = "srl" - - self.hostname = hostname - self.username = username - self.password = password - self.timeout = timeout - self.private_candidate_name = None - - self._stub = None - self._channel = None - self.running_format = optional_args.get("running_format","json") if optional_args else "json" - - self.device = SRLAPI(hostname, username, password, timeout=60, optional_args=optional_args) - - self.pending_commit = False - # Whether to save changes to startup config, default False - self.commit_mode = "save" if optional_args and optional_args.get("commit_save",False) else "now" - self.tmp_cfgfile = None - self.chkpoint_id = 0 - - def open(self): - self.device.open() - - def close(self): - self.device.close() - - def _find_txt(self, value_dict, key, default=""): - value = "" - try: - if isinstance(value_dict, dict): - value = value_dict.get(key) if value_dict.get(key) else default - except Exception as findTxtErr01: # in case of any exception, returns default - logging.error(findTxtErr01) - value = default - return str(value) - - def _str_to_dict(self, value): - if value: - return eval(value.replace("'", '"')) - else: - return "" - - def _str_to_list(self, value): - if value: - return list(eval(value.replace("'", '"'))) - else: - return "" - - def get_arp_table(self, vrf=""): - """ - Returns a list of dictionaries having the following set of keys: - interface (string) - mac (string) - ip (string) - age (float) - ‘vrf’ of null-string will default to all VRFs. - Specific ‘vrf’ will return the ARP table entries for that VRFs - (including potentially ‘default’ or ‘global’). - - In all cases the same data structure is returned and no reference to the VRF that was - used is included in the output. - """ - try: - arp_table = [] - subinterface_names = [] - - def _find_neighbors(is_ipv4, ip_dict): - ip_dict = eval(ip_dict.replace("'", '"')) - neighbor_list = self._find_txt(ip_dict, "neighbor") - if neighbor_list: - neighbor_list = list(eval(neighbor_list)) - for neighbor in neighbor_list: - ipv4_address = "" - ipv6_address = "" - timeout = -1.0 - reachable_time = -1.0 - if is_ipv4: - ipv4_address = self._find_txt(neighbor, "ipv4-address") - timeout = convert( - float, self._find_txt(ip_dict, "timeout"), default=-1.0 - ) - else: - ipv6_address = self._find_txt(neighbor, "ipv6-address") - reachable_time = convert( - float, - self._find_txt(ip_dict, "reachable-time"), - default=-1.0, - ) - arp_table.append( - { - "interface": sub_interface_name, - "mac": self._find_txt(neighbor, "link-layer-address"), - "ip": ipv4_address if is_ipv4 else ipv6_address, - "age": timeout if is_ipv4 else reachable_time, - } - ) - - if vrf: - vrf_path = {"network-instance[name={}]".format(vrf)} - else: - vrf_path = {"network-instance[name=*]"} - pathType = "STATE" - vrf_output = self.device._gnmiGet("", vrf_path, pathType) - if not vrf_output: - return [] - for vrf in vrf_output["srl_nokia-network-instance:network-instance"]: - if "interface" in vrf.keys(): - subinterface_list = self._find_txt(vrf, "interface") - subinterface_list = list(eval(subinterface_list)) - for dictionary in subinterface_list: - if "name" in dictionary.keys(): - subinterface_names.append(self._find_txt(dictionary, "name")) - - interface_path = {"interface[name=*]"} - interface_output = self.device._gnmiGet("", interface_path, pathType) - - for interface in interface_output["srl_nokia-interfaces:interface"]: - interface_name = self._find_txt(interface, "name") - if interface_name: - sub_interface = self._find_txt(interface, "subinterface") - if sub_interface: - sub_interface = list(eval(sub_interface)) - for dictionary in sub_interface: - sub_interface_name = self._find_txt(dictionary, "name") - if sub_interface_name in subinterface_names: - ipv4_data = self._find_txt(dictionary, "ipv4") - if ipv4_data: - ipv4_data = eval(ipv4_data.replace("'", '"')) - ipv4_arp_dict = self._find_txt( - ipv4_data, "srl_nokia-interfaces-nbr:arp" - ) - if ipv4_arp_dict: - _find_neighbors(True, ipv4_arp_dict) - - ipv6_data = self._find_txt(dictionary, "ipv6") - if ipv6_data: - ipv6_data = eval(ipv6_data.replace("'", '"')) - ipv6_neighbor_dict = self._find_txt( - ipv6_data, "srl_nokia-if-ip-nbr:neighbor-discovery" - ) - if ipv6_neighbor_dict: - _find_neighbors(False, ipv6_neighbor_dict) - return arp_table - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_bgp_neighbors(self): - """ - Returns a dictionary of dictionaries. The keys for the first dictionary will be the vrf - (global if no vrf). The inner dictionary will contain the following data for each vrf: - - router_id - peers - another dictionary of dictionaries. Outer keys are the IPs of the neighbors. - The inner keys are: - local_as (int) - remote_as (int) - remote_id - peer router id - is_up (True/False) - is_enabled (True/False) - description (string) - uptime (int in seconds) - address_family (dictionary) - A dictionary of address families available for - the neighbor. - So far it can be ‘ipv4’ or ‘ipv6’ - received_prefixes (int) - accepted_prefixes (int) - sent_prefixes (int) - Note, if is_up is False and uptime has a positive value then this indicates the - uptime of the last active BGP session. - """ - try: - bgp_neighbors = { - "global": { - "router_id": "", - "peers": {} - } - } - system_date_time = "" - - def _build_prefix_dict(): - prefix_limit = {} - ipv4_unicast = self._find_txt(bgp_neighbor, "ipv4-unicast") - if ipv4_unicast: - ipv4_unicast = eval(ipv4_unicast.replace("'", '"')) - prefix_limit.update( - { - "ipv4": { - "sent_prefixes": convert( - int, - self._find_txt(ipv4_unicast, "sent-routes"), - default=-1, - ), - "received_prefixes": convert( - int, - self._find_txt(ipv4_unicast, "received-routes"), - default=-1, - ), - "accepted_prefixes": convert( - int, - self._find_txt(ipv4_unicast, "active-routes"), - default=-1, - ), - } - } - ) - ipv6_unicast = self._find_txt(bgp_neighbor, "ipv6-unicast") - if ipv6_unicast: - ipv6_unicast = eval(ipv6_unicast.replace("'", '"')) - prefix_limit.update( - { - "ipv6": { - "sent_prefixes": convert( - int, - self._find_txt(ipv6_unicast, "sent-routes"), - default=-1, - ), - "received_prefixes": convert( - int, - self._find_txt(ipv6_unicast, "received-routes"), - default=-1, - ), - "accepted_prefixes": convert( - int, - self._find_txt(ipv6_unicast, "active-routes"), - default=-1, - ), - } - } - ) - return prefix_limit - - path = {"/network-instance[name=*]"} - system_path = {"system/information"} - pathType = "STATE" - output = self.device._gnmiGet("", path, pathType) - system_output = self.device._gnmiGet("", system_path, pathType) - - for key, value in system_output["srl_nokia-system:system"].items(): - system_date_time = self._find_txt(value, "current-datetime") - if system_date_time: - system_date_time = datetime.datetime.strptime( - system_date_time, "%Y-%m-%dT%H:%M:%S.%fZ" - ).timestamp() - - for network_instance in output["srl_nokia-network-instance:network-instance"]: - instance_name = self._find_txt(network_instance, "name") - router_id = self._find_txt(network_instance, "router-id") - global_autonomous_system_number = self._find_txt( - network_instance, "autonomous-system", - ) - bgp_neighbors.update({instance_name: {"router_id": router_id, "peers": {}}}) - protocols = self._find_txt(network_instance, "protocols") - if protocols: - protocols = eval(protocols.replace("'", '"')) - bgp_dict = self._find_txt(protocols, "srl_nokia-bgp:bgp") - if bgp_dict: - bgp_dict = eval(bgp_dict.replace("'", '"')) - bgp_neighbors_list = self._find_txt(bgp_dict, "neighbor") - if bgp_neighbors_list: - bgp_neighbors_list = list( - eval(bgp_neighbors_list.replace("'", '"')) - ) - for bgp_neighbor in bgp_neighbors_list: - peer_ip = self._find_txt(bgp_neighbor, "peer-address") - if peer_ip: - local_as = self._find_txt(bgp_neighbor, "local-as") - explicit_peer_as = self._find_txt( - bgp_neighbor, "peer-as" - ) - - local_as_number = -1 - peer_as_number = ( - explicit_peer_as - if explicit_peer_as - else global_autonomous_system_number - ) - if local_as: - local_as = list(eval(local_as.replace("'", '"'))) - - for dictionary in local_as: - explicit_local_as_number = self._find_txt( - dictionary, "as-number" - ) - local_as_number = ( - explicit_local_as_number - if explicit_local_as_number - else global_autonomous_system_number - ) - last_established = self._find_txt( - bgp_neighbor, "last-established" - ) - if last_established: - last_established = datetime.datetime.strptime( - last_established, "%Y-%m-%dT%H:%M:%S.%fZ" - ).timestamp() - bgp_neighbors[instance_name]["peers"].update( - { - peer_ip: { - "local_as": as_number(local_as_number), - "remote_as": as_number(peer_as_number), - "remote_id": peer_ip, - "is_up": True - if self._find_txt( - bgp_neighbor, "session-state" - ) - == "established" - else False, - "is_enabled": True - if self._find_txt( - bgp_neighbor, "admin-state" - ) - == "enable" - else False, - "description": self._find_txt( - bgp_neighbor, "description" - ), - "uptime": convert( - int, - (system_date_time - last_established) if isinstance(last_established, - float) else -1, - default=-1, - ), - "address_family": _build_prefix_dict(), - } - } - ) - - return bgp_neighbors - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_bgp_neighbors_detail(self, neighbor_address=""): - """ - :param neighbor_address: - :return: - Returns a dictionary of dictionaries. The keys for the first dictionary will be the vrf (global if no vrf). - The keys of the inner dictionary represent the AS number of the neighbors. - Leaf dictionaries contain the following fields: - up (True/False) - local_as (int) - remote_as (int) - router_id (string) - local_address (string) - routing_table (string) - local_address_configured (True/False) - local_port (int) - remote_address (string) - remote_port (int) - multihop (True/False) - multipath (True/False) - remove_private_as (True/False) - import_policy (string) - export_policy (string) - input_messages (int) - output_messages (int) - input_updates (int) - output_updates (int) - messages_queued_out (int) - connection_state (string) - previous_connection_state (string) - last_event (string) - suppress_4byte_as (True/False) - local_as_prepend (True/False) - holdtime (int) - configured_holdtime (int) - keepalive (int) - configured_keepalive (int) - active_prefix_count (int) - received_prefix_count (int) - accepted_prefix_count (int) - suppressed_prefix_count (int) - advertised_prefix_count (int) - flap_count (int) - - """ - try: - bgp_neighbor_detail = {} - - path = {"/network-instance[name=*]"} - pathType = "STATE" - output = self.device._gnmiGet("", path, pathType) - - for network_instance in output["srl_nokia-network-instance:network-instance"]: - instance_name = self._find_txt(network_instance, "name") - router_id = self._find_txt(network_instance, "router-id") - global_autonomous_system_number = self._find_txt( - network_instance, "autonomous-system", - ) - protocols = self._find_txt(network_instance, "protocols") - if protocols: - protocols = eval(protocols.replace("'", '"')) - bgp_dict = self._find_txt(protocols, "srl_nokia-bgp:bgp") - if bgp_dict: - bgp_dict = eval(bgp_dict.replace("'", '"')) - bgp_neighbors_list = self._find_txt(bgp_dict, "neighbor") - if bgp_neighbors_list: - bgp_neighbors_list = list( - eval(bgp_neighbors_list.replace("'", '"')) - ) - bgp_neighbor_detail[instance_name] = {} - for bgp_neighbor in bgp_neighbors_list: - peer_ip = self._find_txt(bgp_neighbor, "peer-address") - if peer_ip: - if neighbor_address and not neighbor_address == peer_ip: - continue - local_as = self._find_txt(bgp_neighbor, "local-as") - explicit_peer_as = self._find_txt( - bgp_neighbor, "peer-as" - ) - local_as_number = -1 - peer_as_number = ( - explicit_peer_as - if explicit_peer_as - else global_autonomous_system_number - ) - - if local_as: - local_as = list( - eval(local_as.replace("'", '"')) - ) - for dictionary in local_as: - explicit_local_as_number = self._find_txt( - dictionary, "as-number" - ) - local_as_number = ( - explicit_local_as_number - if explicit_local_as_number - else global_autonomous_system_number - ) - transport = self._str_to_dict( - self._find_txt(bgp_neighbor, "transport") - ) - local_address = "" - if transport: - local_address = self._find_txt( - transport, "local-address" - ) - timers = self._str_to_dict( - self._find_txt(bgp_neighbor, "timers") - ) - sent_messages = self._str_to_dict( - self._find_txt(bgp_neighbor, "sent-messages") - ) - received_messages = self._str_to_dict( - self._find_txt( - bgp_neighbor, "received-messages" - ) - ) - ipv4_unicast = self._str_to_dict( - self._find_txt(bgp_neighbor, "ipv4-unicast") - ) - active_ipv4 = -1 - received_ipv4 = -1 - suppressed_ipv4 = -1 - advertised_ipv4 = -1 - if ipv4_unicast: - active_ipv4 = convert( - int, - self._find_txt( - ipv4_unicast, "active-routes" - ), - default=-1, - ) - received_ipv4 = convert( - int, - self._find_txt( - ipv4_unicast, "received-routes" - ), - default=-1, - ) - suppressed_ipv4 = convert( - int, - self._find_txt( - ipv4_unicast, "rejected-routes" - ), - default=-1, - ) - advertised_ipv4 = convert( - int, - self._find_txt(ipv4_unicast, "sent-routes"), - default=-1, - ) - ipv6_unicast = self._str_to_dict( - self._find_txt(bgp_neighbor, "ipv6-unicast") - ) - # bgp_neighbor_detail[instance_name][ - # as_number(peer_as_number) - # ].append( - peer_data = { - "up": True - if self._find_txt( - bgp_neighbor, "session-state" - ) - == "established" - else False, - "local_as": as_number(local_as_number), - "remote_as": as_number(peer_as_number), - "router_id": router_id, - "local_address": local_address, - "routing_table": self._find_txt( - bgp_neighbor, "peer-group" - ), - "local_address_configured": False - if local_address - else True, - "local_port": convert( - int, - self._find_txt(transport, "local-port"), - default=-1, - ) - if transport - else -1, - "remote_address": peer_ip, - "remote_port": convert( - int, - self._find_txt( - transport, "remote-port" - ), - default=-1, - ), - "multihop": False, # Not yet supported in SRLinux - "multipath": False, # Not yet supported in SRLinux - "remove_private_as": False, # Not yet supported in SRLinux - "import_policy": self._find_txt( - bgp_neighbor, "import-policy" - ), - "export_policy": self._find_txt( - bgp_neighbor, "export-policy" - ), - "input_messages": convert( - int, - self._find_txt( - received_messages, "total-messages" - ), - default=-1, - ), - "output_messages": convert( - int, - self._find_txt( - sent_messages, "total-messages" - ), - default=-1, - ), - "input_updates": convert( - int, - self._find_txt( - received_messages, "total-updates" - ), - default=-1, - ), - "output_updates": convert( - int, - self._find_txt( - sent_messages, "total-updates" - ), - default=-1, - ), - "messages_queued_out": convert( - int, - self._find_txt( - sent_messages, "queue-depth" - ), - default=-1, - ), - "connection_state": self._find_txt( - bgp_neighbor, "session-state" - ), - "previous_connection_state": self._find_txt( - bgp_neighbor, "last-state" - ), - "last_event": self._find_txt( - bgp_neighbor, "last-event" - ), - "suppress_4byte_as": False, # Not yet supported in SRLinux - "local_as_prepend": convert( - bool, - self._find_txt( - local_as, "prepend-local-as" - ), - default=False, - ), - "holdtime": convert( - int, - self._find_txt(timers, "hold-time"), - default=-1, - ), - "configured_holdtime": convert( - int, - self._find_txt( - timers, "negotiated-hold-time" - ), - default=-1, - ), - "keepalive": convert( - int, - self._find_txt( - timers, "keepalive-interval" - ), - default=-1, - ), - "configured_keepalive": convert( - int, - self._find_txt( - timers, - "negotiated-keepalive-interval", - ), - default=-1, - ), - "active_prefix_count": active_ipv4 - if active_ipv4 != -1 - else convert( - int, - self._find_txt( - ipv6_unicast, "active-routes" - ), - default=-1, - ), - "received_prefix_count": received_ipv4 - if received_ipv4 != -1 - else convert( - int, - self._find_txt( - ipv6_unicast, "received-routes" - ), - default=-1, - ), - "accepted_prefix_count": active_ipv4 - if active_ipv4 != -1 - else convert( - int, - self._find_txt( - ipv6_unicast, "active-routes" - ), - default=-1, - ), - "suppressed_prefix_count": suppressed_ipv4 - if suppressed_ipv4 != -1 - else convert( - int, - self._find_txt( - ipv6_unicast, "rejected-routes" - ), - default=-1, - ), - "advertised_prefix_count": advertised_ipv4 - if advertised_ipv4 != -1 - else convert( - int, - self._find_txt( - ipv6_unicast, "sent-routes" - ), - default=-1, - ), - "flap_count": -1, # Not yet supported in SRLinux - } - # ) - peer_as_number = as_number(peer_as_number) - if peer_as_number in bgp_neighbor_detail[instance_name]: - bgp_neighbor_detail[instance_name][peer_as_number].append(peer_data) - else: - bgp_neighbor_detail[instance_name][peer_as_number] = [peer_data] - return bgp_neighbor_detail - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_environment(self): - """ - Returns a dictionary where: - - fans is a dictionary of dictionaries where the key is the location and the values: - status (True/False) - True if it’s ok, false if it’s broken - temperature is a dict of dictionaries where the key is the location and the values: - temperature (float) - Temperature in celsius the sensor is reporting. - is_alert (True/False) - True if the temperature is above the alert threshold - is_critical (True/False) - True if the temp is above the critical threshold - power is a dictionary of dictionaries where the key is the PSU id and the values: - status (True/False) - True if it’s ok, false if it’s broken - capacity (float) - Capacity in W that the power supply can support - output (float) - Watts drawn by the system - cpu is a dictionary of dictionaries where the key is the ID and the values - %usage - memory is a dictionary with: - available_ram (int) - Total amount of RAM installed in the device - used_ram (int) - RAM in use in the device - """ - try: - environment_data = { - "fans": {}, - "power": {}, - "temperature": {}, - "memory": {}, - "cpu": {} - } - path = {"/platform"} - pathType = "STATE" - output = self.device._gnmiGet("", path, pathType) - - for component in output["srl_nokia-platform:platform"][ - "srl_nokia-platform-control:control" - ]: - slot = self._find_txt(component, "slot") - if slot: - temperature = self._find_txt(component, "temperature") - if temperature: - temperature = eval(temperature.replace("'", '"')) - environment_data["temperature"].update( - { - slot: { - "temperature": convert( - float, - self._find_txt(temperature, "instant"), - default=-1.0, - ), - "is_alert": convert( - bool, - self._find_txt(temperature, "alarm-status"), # Not able to detect alarm-status - default=False), - "is_critical": False, # Not supported yet in SRLinux - } - } - ) - memory = self._find_txt(component, "srl_nokia-platform-memory:memory") - environment_data["memory"] = { - "available_ram": -1, - "used_ram": -1 - - } - if memory: - memory = eval(memory.replace("'", '"')) - physical = convert( - int, self._find_txt(memory, "physical"), default=-1 - ) - free_memory = convert( - int, self._find_txt(memory, "free"), default=-1 - ) - environment_data["memory"].update( - { - "available_ram": physical, - "used_ram": physical - free_memory - if physical and free_memory > -1 - else -1, - } - ) - cpus = self._getObj(component, *["srl_nokia-platform-cpu:cpu"]) - if cpus: - for cpu in cpus: - environment_data["cpu"].update( - { - self._getObj(cpu, *["index"]): { - "%usage": float(self._getObj(cpu, *["total", "instant"], default=-1.0)) - } - } - ) - - for power_supply in output["srl_nokia-platform:platform"][ - "srl_nokia-platform-psu:power-supply" - ]: - environment_data["power"].update( - { - self._find_txt(power_supply, "id"): { - "status": True - if self._find_txt(power_supply, "oper-state") == "up" - else False, - "capacity": convert( - float, self._find_txt(power_supply, "capacity"), default=-1.0 - ), - "output": -1.0, # Not supported yet in SRLinx - } - } - ) - - # fan_ouput = self.device._gnmiGet("", fan_path, pathType) - # print("OUTPUT FAN:", fan_ouput) - - for fans in output["srl_nokia-platform:platform"][ - "srl_nokia-platform-fan:fan-tray" - ]: - environment_data["fans"].update( - { - self._find_txt(fans, "id"): { - "status": True - if self._find_txt(fans, "oper-state") == "up" - else False - } - } - ) - # for fan in output["srl-nokia-platform:platform"]: - # print("FAN:", fan) - - return environment_data - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_facts(self): - """ - Returns a dictionary containing the following information: - uptime - Uptime of the device in seconds. - vendor - Manufacturer of the device. - model - Device model. - hostname - Hostname of the device - fqdn - Fqdn of the device - os_version - String with the OS version running on the device. - serial_number - Serial number of the device - interface_list - List of the interfaces of the device - """ - - # Providing path for getting information from router - try: - path = {"/platform/chassis", "system/information", "system/name/host-name"} - interface_path = {"interface[name=*]"} - pathType = "STATE" - - output = self.device._gnmiGet("", path, pathType) - interface_output = self.device._gnmiGet("", interface_path, pathType) - - # defining output variables - interface_list = [] - uptime = -1.0 - version = "" - hostname = "" - serial_number = "" - chassis_type = "" - # getting interface names from the list - for interface in interface_output["srl_nokia-interfaces:interface"]: - interface_list.append(interface["name"]) - # getting system and platform information - for key, value in output.items(): - if "system" in key and isinstance(value, dict): - for key_1, value_1 in value.items(): - if "information" in key_1: - version = self._find_txt(value_1, "version") - uptime = self._find_txt(value_1, "uptime") - if uptime: - uptime = datetime.datetime.strptime( - uptime, "%Y-%m-%dT%H:%M:%S.%fZ" - ).timestamp() - if "name" in key_1: - hostname = self._find_txt(value_1, "host-name") - if "platform" in key and isinstance(value, dict): - for key_1, value_1 in value.items(): - if "chassis" in key_1: - chassis_type = self._find_txt(value_1, "type") - serial_number = self._find_txt(value_1, "serial-number") - return { - "hostname": hostname, - "fqdn": hostname, - "vendor": u"Nokia", - "model": chassis_type, - "serial_number": serial_number, - "os_version": version, - "uptime": convert(float, uptime, default=-1.0), - "interface_list": interface_list, - } - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_interfaces(self): - """ - Returns a dictionary of dictionaries. - The keys for the first dictionary will be the interfaces in the devices. - The inner dictionary will containing the following data for each interface: - is_up (True/False) - is_enabled (True/False) - description (string) - last_flapped (float in seconds) - speed (float in Mbit) - MTU (in Bytes) - mac_address (string) - """ - try: - interfaces = {} - path = {"interface[name=*]"} - pathType = "STATE" - output = self.device._gnmiGet("", path, pathType) - - # looping over interfaces to get information - for interface in output["srl_nokia-interfaces:interface"]: - interface_name = self._find_txt(interface, "name") - if interface_name: - last_flapped = self._find_txt(interface, "last-change") - if last_flapped: - last_flapped = datetime.datetime.strptime( - last_flapped, "%Y-%m-%dT%H:%M:%S.%fZ" - ).timestamp() - speed = -1.0 - mac_address = "" - for key, value in interface.items(): - if "ethernet" in key: - speed = self._find_txt(value, "port-speed") - if speed: - regex = re.compile(r"(\d+|\s+)") - speed = regex.split(speed) - speed = convert(float, speed[1], default=-1.0) - mac_address = self._find_txt( - value, "hw-mac-address", default="" - ) - interfaces.update( - { - interface_name: { - "is_up": True - if self._find_txt(interface, "oper-state") == "up" - else False, - "is_enabled": True - if self._find_txt(interface, "admin-state") == "enable" - else False, - "description": self._find_txt(interface, "description"), - "last_flapped": last_flapped if last_flapped else -1.0, - "mtu": convert( - int, self._find_txt(interface, "mtu"), default=-1 - ), - "speed": convert(float, speed, default=-1.0), - "mac_address": mac(mac_address) if mac_address else "", - } - } - ) - - return interfaces - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_interfaces_counters(self): - """ - Returns a dictionary of dictionaries where the first key is an interface name - and the inner dictionary contains the following keys: - tx_errors (int) - rx_errors (int) - tx_discards (int) - rx_discards (int) - tx_octets (int) - rx_octets (int) - tx_unicast_packets (int) - rx_unicast_packets (int) - tx_multicast_packets (int) - rx_multicast_packets (int) - tx_broadcast_packets (int) - rx_broadcast_packets (int) - """ - try: - interface_counters = {} - - path = {"interface[name=*]"} - pathType = "STATE" - output = self.device._gnmiGet("", path, pathType) - - for interface in output["srl_nokia-interfaces:interface"]: - interface_name = self._find_txt(interface, "name") - if interface_name: - statistics_interface = self._find_txt(interface, "statistics") - if statistics_interface: - statistics_interface = self._str_to_dict(statistics_interface) - sub_interface = self._find_txt(interface, "subinterface") - if sub_interface: - sub_interface = self._str_to_list(sub_interface) - for dictionary in sub_interface: - sub_interface_name = self._find_txt(dictionary, "name") - if sub_interface_name: - ifctrs = { - "tx_errors": -1, - "rx_errors": -1, - "tx_discards": -1, - "rx_discards": -1, - "tx_octets": -1, - "rx_octets": -1, - "tx_unicast_packets": -1, - "rx_unicast_packets": -1, - "tx_multicast_packets": -1, - "rx_multicast_packets": -1, - "tx_broadcast_packets": -1, - "rx_broadcast_packets": -1 - } - interface_counters[sub_interface_name] = ifctrs - statistics = self._find_txt(dictionary, "statistics") - if statistics: - statistics = self._str_to_dict(statistics) - ifctrs = { - "tx_errors": convert( - int, - self._find_txt( - statistics, "out-error-packets" - ), - default=-1, - ), - "rx_errors": convert( - int, - self._find_txt( - statistics, "in-error-packets" - ), - default=-1, - ), - "tx_discards": convert( - int, - self._find_txt( - statistics, "out-discarded-packets" - ), - default=-1, - ), - "rx_discards": convert( - int, - self._find_txt( - statistics, "in-discarded-packets" - ), - default=-1, - ), - "tx_octets": convert( - int, - self._find_txt(statistics, "out-octets"), - default=-1, - ), - "rx_octets": convert( - int, - self._find_txt(statistics, "in-octets"), - default=-1, - ), - # unicast, broadcast, multicast packet statistics - # are taken at the interface level - "tx_unicast_packets": convert( - int, - self._find_txt( - statistics_interface, - "out-unicast-packets", - ), - default=-1, - ), - "rx_unicast_packets": convert( - int, - self._find_txt( - statistics_interface, - "in-unicast-packets", - ), - default=-1, - ), - "tx_multicast_packets": convert( - int, - self._find_txt( - statistics_interface, - "out-multicast-packets", - ), - default=-1, - ), - "rx_multicast_packets": convert( - int, - self._find_txt( - statistics_interface, - "in-multicast-packets", - ), - default=-1, - ), - "tx_broadcast_packets": convert( - int, - self._find_txt( - statistics_interface, - "out-broadcast-packets", - ), - default=-1, - ), - "rx_broadcast_packets": convert( - int, - self._find_txt( - statistics_interface, - "in-broadcast-packets", - ), - default=-1, - ), - } - interface_counters[sub_interface_name].update(ifctrs) - - return interface_counters - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_interfaces_ip(self): - """ - Returns all configured IP addresses on all interfaces as a dictionary of dictionaries. - of the main dictionary represent the name of the interface. - Values of the main dictionary represent are dictionaries that may consist of two keys - ‘ipv4’ and ‘ipv6’ (one, both or none) which are themselves dictionaries with the IP addresses as keys. - Each IP Address dictionary has the following keys: - prefix_length (int) - """ - try: - interfaces_ip = {} - - path = {"interface[name=*]/subinterface"} - pathType = "STATE" - output = self.device._gnmiGet("", path, pathType) - - for interface in output["srl_nokia-interfaces:interface"]: - for s in interface["subinterface"]: - ip_addr = {} - for v in ["ipv4","ipv6"]: - if v in s and 'address' in s[v]: - for addr in s[v]["address"]: - ip_l = addr['ip-prefix'].split('/') - e = { ip_l[0]: { "prefix_length": int(ip_l[1]) } } - if v not in ip_addr: - ip_addr[v] = e - else: - ip_addr[v].update( e ) - interfaces_ip[ s['name'] ] = ip_addr - - return interfaces_ip - except Exception as e: - logging.exception(f"Error occurred : {e}") - - def get_ipv6_neighbors_table(self): - """ - Get IPv6 neighbors table information. - - Return a list of dictionaries having the following set of keys: - - interface (string) - mac (string) - ip (string) - age (float) in seconds - state (string) - """ - try: - ipv6_neighbor_list = [] - - path = {"interface[name=*]"} - pathType = "STATE" - output = self.device._gnmiGet("", path, pathType) - - for interface in output["srl_nokia-interfaces:interface"]: - interface_name = self._find_txt(interface, "name") - if interface_name: - sub_interface = self._str_to_list( - self._find_txt(interface, "subinterface") - ) - if sub_interface: - for dictionary in sub_interface: - sub_interface_name = self._find_txt(dictionary, "name") - if sub_interface_name: - ipv6 = self._str_to_dict(self._find_txt(dictionary, "ipv6")) - if ipv6: - neighbour_discovery = self._str_to_dict( - self._find_txt( - ipv6, - "srl_nokia-interfaces-nbr:neighbor-discovery", - ) - ) - - if neighbour_discovery: - neighbors = self._str_to_dict( - self._find_txt(neighbour_discovery, "neighbor", ) - ) - if neighbors: - for neighbor in neighbors: - next_state_time = self._find_txt( - neighbor, "next-state-time" - ) - if next_state_time: - next_state_time = datetime.datetime.strptime( - next_state_time, - "%Y-%m-%dT%H:%M:%S.%fZ", - ).timestamp() - ipv6_neighbor_list.append( - { - "interface": sub_interface_name, - "mac": self._find_txt( - neighbor, "link-layer-address", - ), - "ip": self._find_txt( - neighbor, "ipv6-address" - ), - "age": convert( - float, - next_state_time, - default=-1.0, - ), - "state": self._find_txt( - neighbor, "current-state" - ), - } - ) - return ipv6_neighbor_list - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_lldp_neighbors(self): - """ - Returns a dictionary where the keys are local ports and the value is a list of dictionaries - with the following information: - hostname - port - """ - try: - lldp_neighbors = {} - - path = {"/system/lldp"} - pathType = "STATE" - output = self.device._gnmiGet("", path, pathType) - - for key, value in output["srl_nokia-system:system"].items(): - chassis_id = self._find_txt(value, "chassis-id") - if chassis_id == "": - continue - interfaces = self._str_to_list(self._find_txt(value, "interface")) - if interfaces: - for dictionary in interfaces: - interface_name = self._find_txt(dictionary, "name") - neighbors = self._str_to_list( - self._find_txt(dictionary, "neighbor") - ) - neighbor_data = [] - for neighbor in neighbors: - neighbor_data.append({ - "hostname": self._find_txt(neighbor, "system-name"), - "port": self._find_txt(neighbor, "port-id"), - }) - lldp_neighbors.update({interface_name: neighbor_data}) - - return lldp_neighbors - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_lldp_neighbors_detail(self, interface=""): - """ - Returns a detailed view of the LLDP neighbors as a dictionary containing lists - of dictionaries for each interface. - - Empty entries are returned as an empty string (e.g. ‘’) or list where applicable. - - Inner dictionaries contain fields: - parent_interface (string) - remote_port (string) - remote_port_description (string) - remote_chassis_id (string) - remote_system_name (string) - remote_system_description (string) - remote_system_capab (list) with any of these values - other - repeater - bridge - wlan-access-point - router - telephone - docsis-cable-device - station - remote_system_enabled_capab (list) - """ - try: - lldp_neighbors_detail = {} - - path = {"/system/lldp"} - pathType = "STATE" - output = self.device._gnmiGet("", path, pathType) - - interface_name_list = [] - if not output: - return {} - for key, value in output["srl_nokia-system:system"].items(): - chassis_id = self._find_txt(value, "chassis-id") - if chassis_id == "": - continue - interfaces = self._str_to_list(self._find_txt(value, "interface")) - if interfaces: - for dictionary in interfaces: - interface_name = self._find_txt(dictionary, "name") - if interface: - if interface_name == interface: - interface_name_list.append(interface_name) - else: - continue - else: - interface_name_list.append(interface_name) - neighbors = self._str_to_list( - self._find_txt(dictionary, "neighbor") - ) - neighbor_data = [] - for neighbor in neighbors: - capability_list = self._str_to_list( - self._find_txt(neighbor, "capability") - ) - capabilities = [] - capabilities_enabled = [] - for capability in capability_list: - capabilities.append(capability["name"]) - if capability["enabled"] is True: - capabilities_enabled.append(capability["name"]) - neighbor_data.append({ - "parent_interface": interface_name, - "remote_port": self._find_txt(neighbor, "port-id"), - "remote_port_description": self._find_txt( - neighbor, "port-description" - ), - "remote_chassis_id": self._find_txt( - neighbor, "chassis-id" - ), - "remote_system_name": self._find_txt( - neighbor, "system-name" - ), - "remote_system_description": self._find_txt( - neighbor, "system-description" - ), - "remote_system_capab": capabilities, - "remote_system_enable_capab": capabilities_enabled, - }) - lldp_neighbors_detail.update({interface_name: neighbor_data}) - - return lldp_neighbors_detail - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_network_instances(self, name=""): - """ - Return a dictionary of network instances (VRFs) configured, including default/global - Parameters: name (string) – - Returns: - name (dict) - name (unicode) - type (unicode) - state (dict) - route_distinguisher (unicode) - interfaces (dict) - interface (dict) - interface name: (dict) - """ - try: - network_instances = {} - - if name: - vrf_path = {"network-instance[name={}]".format(name)} - else: - vrf_path = {"network-instance[name=*]"} - pathType = "STATE" - vrf_output = self.device._gnmiGet("", vrf_path, pathType) - if not vrf_output: - return {} - for vrf in vrf_output["srl_nokia-network-instance:network-instance"]: - # vrf_name = self._find_txt(vrf, "name") - vrf_name = name if name else self._find_txt(vrf, "name") - vrf_type = self._find_txt(vrf, "type") - network_instances.update( - { - vrf_name: { - "name": vrf_name, - "type": vrf_type, - "state": { - "route_distinguisher": "" # Not supported yet in SRLinux - }, - "interfaces": {"interface": {}}, - } - } - ) - interface_list = self._str_to_list(self._find_txt(vrf, "interface")) - if interface_list: - for interface in interface_list: - interface_name = self._find_txt(interface, "name") - network_instances[vrf_name]["interfaces"]["interface"].update( - {interface_name: {}} - ) - - return network_instances - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_users(self): - """ - Returns a dictionary with the configured users. - The keys of the main dictionary represents the username. - The values represent the details of the user, represented by the following keys: - level (int) - password (str) - sshkeys (list) - The level is an integer between 0 and 15, where 0 is the lowest access - and 15 represents full access to the device. - """ - try: - users_dict = {} - - path = {"system/aaa/authentication/admin-user"} - path_type = "STATE" - output = self.device._gnmiGet("", path, path_type) - - for key, value in output["srl_nokia-system:system"]["srl_nokia-aaa:aaa"]["authentication"].items(): - username = self._find_txt(value, "username") - users_dict.update({ - username: { - "level": 0, # Not supported yet in SRLinux - "password": self._find_txt(value, "password"), - "sshkeys": [] # Not supported yet in SRLinux - } - }) - return users_dict - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_bgp_config(self, group="", neighbor=""): - """ - Returns a dictionary containing the BGP configuration. Can return either the whole config, either the config only for a group or neighbor. - - :param neighbor: specific BGP neighbor. - :param group: specific BGP group. - :return: Returns the configuration of a specific BGP neighbor /BGP group - """ - try: - path = {"/network-instance/protocols/bgp"} - path_type = "STATE" - output = self.device._gnmiGet("", path, path_type) - groups_keys_path = ["srl_nokia-network-instance:network-instance", 0, "protocols", "srl_nokia-bgp:bgp", "group"] - neighbors_keys_path = ["srl_nokia-network-instance:network-instance", 0, "protocols", "srl_nokia-bgp:bgp", - "neighbor"] - multipath_keys_path = ["srl_nokia-network-instance:network-instance", 0, "protocols", "srl_nokia-bgp:bgp", - "ipv4-unicast", "multipath", "allow-multiple-as"] - groups = self._getObj(output, *groups_keys_path, default=[]) - neighbors = self._getObj(output, *neighbors_keys_path, default=[]) - multipath = self._getObj(output, *multipath_keys_path, default=False) - groups_data = {} - for g in groups: - group_name = self._getObj(g, *["group-name"]) - g_description = self._getObj(g, *["description"]) - local_address = self._getObj(g, *["transport", "local-address"]) - g_local_as = self._getObj(g, *["local-as", 0, "as-number"]) - g_remote_as = self._getObj(g, *["peer-as"]) - g_export_policy = self._getObj(g, *["export-policy"]) - g_import_policy = self._getObj(g, *["import-policy"]) - g_ipv4_unicast = self._getObj(g, *["ipv4-unicast"]) - g_ipv6_unicast = self._getObj(g, *["ipv6-unicast"]) - ct_neighbors = [n for n in neighbors if self._getObj(n, *["peer-group"]) == group_name] - neighbors_data = {} - for n in ct_neighbors: - n_ip_address = self._getObj(n, *["peer-address"]) - n_description = self._getObj(n, *["description"]) - n_import_policy = self._getObj(n, *["import-policy"]) - n_export_policy = self._getObj(n, *["export-policy"]) - n_local_address = self._getObj(n, *["transport", "local-address"]) - n_local_as = self._getObj(n, *["local-as", 0, "as-number"], default=-1) - n_remote_as = self._getObj(n, *["peer-as"], default=-1) - n_ipv4_unicast = self._getObj(n, *["ipv4-unicast"]) - n_ipv6_unicast = self._getObj(n, *["ipv6-unicast"]) - n_route_reflector_client = self._getObj(n, *["route-reflector", "client"], default=False) - n_nhs = self._getObj(n, *["next-hop-self"], default=False) - neighbors_data.update({ - n_ip_address: { - "description": n_description, - "import_policy": n_import_policy, - "export_policy": n_export_policy, - "local_address": n_local_address, - "local_as": n_local_as, - "remote_as": n_remote_as, - "authentication_key": "", - "prefix_limit": { - "inet": { - "unicast": { - 'limit': self._getObj(n_ipv4_unicast, *["prefix-limit", "max-received-routes"], - default=-1), - 'teardown': { - 'threshold': self._getObj(n_ipv4_unicast, - *["prefix-limit", "warning-threshold-pct"], - default=-1), - "timeout": -1, - } - } - }, - "inet6": { - "unicast": { - 'limit': self._getObj(n_ipv6_unicast, *["prefix-limit", "max-received-routes"], - default=-1), - 'teardown': { - 'threshold': self._getObj(n_ipv6_unicast, - *["prefix-limit", "warning-threshold-pct"], - default=-1), - "timeout": -1, - } - } - } - }, - "route_reflector_client": n_route_reflector_client, - "nhs": n_nhs - } - }) - ct_grp_data = { - group_name: { - "type": "internal" if g_local_as == g_remote_as else "external", - "description": g_description, - "apply_groups": [], # Not Supported - "multihop_ttl": -1, # Not Supported - "multipath": multipath, - "local_address": local_address, - "local_as": g_local_as, - "remote_as": g_remote_as, - "import_policy": g_import_policy, - "export_policy": g_export_policy, - "remove_private_as": False, # Not Supported - "prefix_limit": { - "inet": { - "unicast": { - 'limit': self._getObj(g_ipv4_unicast, *["prefix-limit", "max-received-routes"], - default=-1), - 'teardown': { - 'threshold': self._getObj(g_ipv4_unicast, - *["prefix-limit", "warning-threshold-pct"], default=-1), - "timeout": -1, - } - } - }, - "inet6": { - "unicast": { - 'limit': self._getObj(g_ipv6_unicast, *["prefix-limit", "max-received-routes"], - default=-1), - 'teardown': { - 'threshold': self._getObj(g_ipv6_unicast, - *["prefix-limit", "warning-threshold-pct"], default=-1), - "timeout": -1, - } - } - } - }, - "neighbors": neighbors_data - } - } - if group and group == group_name: - return ct_grp_data - # return self._removeNotFound(ct_grp_data) - if neighbor and neighbor in neighbors_data.keys(): - ct_grp_data[group_name]["neighbors"] = {} - ct_grp_data[group_name]["neighbors"][neighbor] = neighbors_data[neighbor] - return ct_grp_data - # return self._removeNotFound(ct_grp_data) - groups_data.update(ct_grp_data) - # return groups_data if not group or not neighbor else {} - # if group or neighbor is true and is present , then return shd have happened in for loop - return {} if group or neighbor else groups_data - # return {} if group or neighbor else self._removeNotFound(groups_data) - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_snmp_information(self): - """ - :return: - Returns a dict of dicts containing SNMP configuration. Each inner dictionary contains these fields - - chassis_id (string) - community (dictionary) - contact (string) - location (string) - """ - try: - contact_path = {"/system/information/contact"} - location_path = {"/system/information/location"} - path_type = "STATE" - contact_output = self.device._gnmiGet("", contact_path, path_type) - location_output = self.device._gnmiGet("", location_path, path_type) - contact = self._getObj(contact_output, - *['srl_nokia-system:system', 'srl_nokia-system-info:information', 'contact']) - location = self._getObj(location_output, - *['srl_nokia-system:system', 'srl_nokia-system-info:information', 'location']) - output = { - "chassis_id": "", - "community": {}, - "contact": contact, - "location": location - } - return output - except Exception as e: - logging.error("Error occurred : {}".format(e)) - # def get_config_jsonrpc(self, retrieve='all', full=False, sanitized=False): - # """ - # :param retrieve: Which configuration type you want to populate, default is all of them. The rest will be set to “”. - # :param full:Retrieve all the configuration. For instance, on ios, “sh run all”. - # :param sanitized:Remove secret data. Default: False. - # :return:Return the configuration of a device. - # """ - # cmds = [ - # { - # "datastore": "running", - # "path": "/" - # } - # ] - # running = self.device._jsonrpcGet(cmds) - # if retrieve == 'all': - # return { - # "running": str(running), - # "candidate": "", - # "startup": "" - # } - # if retrieve == 'running': - # return { - # "running": str(running), - # "candidate": "", - # "startup": "" - # } - # - # if retrieve == 'candidate': - # return { - # "running": "", - # "candidate": "", - # "startup": "" - # } - # if retrieve == 'startup': - # return { - # "running": "", - # "candidate": "", - # "startup": "" - # } - - - def get_config( - self, - retrieve: str = "all", - full: bool = False, - sanitized: bool = False, - format: str = "text", # This driver supports 'cli' for CLI, else default 'json' - ): - """ - :param retrieve: Which configuration type you want to populate, default is all of them. The rest will be set to “”. - :param full:Retrieve all the configuration. For instance, on ios, “sh run all”. - :param sanitized:Remove secret data. Default: False. - :return:Return the configuration of a device. - """ - try: - if retrieve not in ['all','running']: - # Only 'running' or 'all' is supported for get_config - return { - "running": "", - "candidate": "", - "startup": "" - } - - if self.running_format == 'cli' or format == 'cli': - if sanitized: - raise NotImplementedError( - "sanitized=True is not implemented with CLI format") - output = self.device._jsonrpcRunCli(["info flat"]) - running_config = self._return_result(output) - else: - running = self.device._gnmiGet("", {"/"}, "CONFIG") - if sanitized: - if "srl_nokia-system:system" in running: - _system = running["srl_nokia-system:system"] - if "srl_nokia-aaa:aaa" in _system: - del _system["srl_nokia-aaa:aaa"] - if "srl_nokia-tls:tls" in _system: - del _system["srl_nokia-tls:tls"] - running_config = json.dumps(running) # don't use sort_keys=True - - return { - "running": running_config, - "candidate": "", - "startup": "" - } - except NotImplementedError as e: - raise e - except Exception as e: - logging.error(f"Error occurred in get_config: {e}") - return { - "running": "", - "candidate": "", - "startup": "" - } - - def get_ntp_servers(self): - """ - :return:Returns the NTP servers configuration as dictionary. The keys of the dictionary represent the IP Addresses of the servers. Inner dictionaries do not have yet any available keys. - """ - try: - path = {"/system/ntp"} - path_type = "STATE" - output = self.device._gnmiGet("", path, path_type) - ntp_servers = self._getObj(output, *['srl_nokia-system:system','srl_nokia-ntp:ntp', "server"], default=[]) - server_data = {} - for s in ntp_servers: - if "address" in s: - server_data.update({ - s["address"]:{} - }) - return server_data - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - # # def get_ntp_peers(self): - # # """ - # # :return:Returns the NTP peers configuration as dictionary. The keys of the dictionary represent the IP Addresses of the peers. Inner dictionaries do not have yet any available keys. - # # """ - # # path = {"/system/ntp/server"} - # # path_type = "STATE" - # # output = self.device._gnmiGet("", path, path_type) - # # ntp_servers = self._getObj(output, *['srl_nokia-system:system/srl_nokia-ntp:ntp', "server"]) - # # peers_data = {} - # # for s in ntp_servers: - # # if "address" in s: - # # peers_data.update({ - # # s["address"]:{} - # # }) - # # return peers_data - # pass - - def get_ntp_stats(self): - """ - :return:Returns a list of NTP synchronization statistics. - """ - try: - path = {"/system/ntp"} - path_type = "STATE" - output = self.device._gnmiGet("", path, path_type) - ntp_servers = self._getObj(output, *['srl_nokia-system:system', 'srl_nokia-ntp:ntp', "server"], default=[]) - synchronized = self._getObj(output, *['srl_nokia-system:system', 'srl_nokia-ntp:ntp', 'synchronized']) - stats_data = [] - for s in ntp_servers: - prefer = s["prefer"] if "prefer" in s else None - if synchronized.lower() == "synchronized" or synchronized.lower() == "synchronised": - synced = True - else: - synced = False - if prefer: - offset = self._getObj(s, *["offset"], default=-1.0) - jitter = self._getObj(s, *["jitter"], default=-1.0) - stats_data.append({ - 'remote': self._getObj(s, *["address"]), - "referenceid": "", - 'synchronized': synced, - 'stratum': self._getObj(s, *["stratum"], default=-1), - "type": "", - "when": "", - 'hostpoll': self._getObj(s, *["poll-interval"], default=-1), - "reachability": -1, - "delay": -1.0, - 'offset': float(offset), - 'jitter': float(jitter) - }) - return stats_data - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_optics(self): - """ - :return:Fetches the power usage on the various transceivers installed on the switch (in dbm), and returns a view that conforms with the openconfig model openconfig-platform-transceiver.yang - """ - try: - path = {"/interface"} - path_type = "STATE" - output = self.device._gnmiGet("", path, path_type) - interfaces = self._getObj(output, *['srl_nokia-interfaces:interface'], default=[]) - channel_data = {} - for i in interfaces: - name = self._getObj(i, *["name"]) - channel = self._getObj(i, *["transceiver", "channel"], default={}) - channel_data.update({ - name: { - 'physical_channels': { - 'channel': [ - { - 'index': self._getObj(channel, *["index"], default=-1), - 'state': { - 'input_power': { - 'instant': self._getObj(channel, *["input-power", "latest_value"], - default=-1.0), - "avg": -1.0, - "min": -1.0, - "max": -1.0 - }, - 'output_power': { - 'instant': self._getObj(channel, *["output-power", "latest_value"], - default=-1.0), - "avg": -1.0, - "min": -1.0, - "max": -1.0 - }, - 'laser_bias_current': { - 'instant': self._getObj(channel, *["laser-bias-current", "latest_value"], - default=-1.0), - "avg": -1.0, - "min": -1.0, - "max": -1.0 - }, - } - } - ] - } - } - }) - return channel_data - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_mac_address_table(self): - """ - Returns a lists of dictionaries. Each dictionary represents an entry in the MAC Address Table, - having the following keys: - mac (string) - interface (string) - vlan (int) - active (boolean) - static (boolean) - moves (int) - last_move (float) - """ - try: - path = {"/network-instance/bridge-table/mac-table/mac"} - path_type = "STATE" - output = self.device._gnmiGet("", path, path_type) - mac_data = [] - instances = self._getObj(output, *['srl_nokia-network-instance:network-instance'], default=[]) - for i in instances: - mac_output = self._getObj(i, *['bridge-table', 'srl_nokia-bridge-table-mac-table:mac-table', 'mac'], - default=[]) - for m in mac_output: - dest_splits = str(m['destination']).split(".") - int = dest_splits[0] - subint = dest_splits[1] if len(dest_splits) >= 2 else "*" - vlan_path = { - "/interface[name={}]/subinterface[index={}]/vlan/encap/single-tagged/vlan-id".format(int, subint)} - vlan_output = self.device._gnmiGet("", vlan_path, "STATE") - vlanid = self._getObj(vlan_output, *['srl_nokia-interfaces:interface', 0, 'subinterface', 0, - 'srl_nokia-interfaces-vlans:vlan', 'encap', 'single-tagged', - 'vlan-id'], default=-1) - type = self._getObj(m, *["type"]) - static = False if not type else type != "learnt" - m_data = { - 'mac': self._getObj(m, *['address']), - 'interface': self._getObj(m, *['destination']), - 'vlan': vlanid, - "active": False, - 'static': static, - "moves": -1, - "last_move": -1.0 - } - mac_data.append(m_data) - return mac_data - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def get_route_to(self, destination='', protocol='', longer=False): - """ - :return:Returns a dictionary of dictionaries containing details of all available routes to a destination. - """ - try: - path = {"/network-instance"} - path_type = "STATE" - output = self.device._gnmiGet("", path, path_type) - dpath = {"/system/information/current-datetime"} - doutput = self.device._gnmiGet("", dpath, "STATE") - ctdatetime = self._getObj(doutput, - *['srl_nokia-system:system', 'srl_nokia-system-info:information', 'current-datetime'], - default=None) - interfaces = self._getObj(output, *['srl_nokia-network-instance:network-instance'], default=[]) - route_data = {} - for i in interfaces: - routes = self._getObj(i, *["route-table", "srl_nokia-ip-route-tables:ipv4-unicast", "route"], default=[]) - next_hop_groups = self._getObj(i, *["route-table", "srl_nokia-ip-route-tables:next-hop-group"], default=[]) - next_hops = self._getObj(i, *["route-table", "srl_nokia-ip-route-tables:next-hop"], default=[]) - name = self._getObj(i, *["name"]) - for r in routes: - if "next-hop-group" not in r: - continue - next_hop_group_id = r["next-hop-group"] - next_hop_group = [n for n in next_hop_groups if n["index"] == next_hop_group_id] - next_hop_group = next_hop_group[0] # definitely this will be present . list cannot be empty - next_hop_ids = [n["next-hop"] for n in next_hop_group["next-hop"]] - - ct_next_hops = [n for n in next_hops if n["index"] in next_hop_ids] - ct_next_hops_data = [] - for next_hop in ct_next_hops: - ip_address = self._getObj(next_hop, *["ip-address"]) - subinterface = self._getObj(next_hop, *["subinterface"]) - if ctdatetime and self._getObj(r, *["last-app-update"], default=None): - ctdatetime_obj = datetime.datetime.strptime(ctdatetime, "%Y-%m-%dT%H:%M:%S.%fZ") - last_app_date = datetime.datetime.strptime(r["last-app-update"], "%Y-%m-%dT%H:%M:%S.%fZ") - age = int((ctdatetime_obj - last_app_date).total_seconds()) - else: - age = -1 - ct_protocol = str(r["owner"]).split(":")[-1] - data = { - "protocol": ct_protocol, - "current_active": self._getObj(r, *["active"], default=False), - "last_active": False, - "age": age, - "next_hop": ip_address, - "outgoing_interface": subinterface, - "selected_next_hop": True if ip_address else False, - "preference": self._getObj(r, *["preference"], default=-1), - "inactive_reason": "", - "routing_table": name, - } - if "bgp" in r["owner"]: - bgp_protocol = self._getObj(i, *["protocols", "srl_nokia-bgp:bgp"], default={}) - bgp_rib_routes = self._getObj(i, *["srl_nokia-rib-bgp:bgp-rib", "ipv4-unicast", "local-rib", - "routes"], default=[]) - bgp_rib_attrsets = self._getObj(i, *["srl_nokia-rib-bgp:bgp-rib", "attr-sets", "attr-set"], - default=[]) - neighbor = [b for b in bgp_protocol["neighbor"] if b["peer-address"] == ip_address] - neighbor = neighbor[0] # exactly one neighbor will be present if it is bgp - rib_route = [rr for rr in bgp_rib_routes if - rr["prefix"] == r["ipv4-prefix"] and rr["neighbor"] == ip_address and rr[ - "origin-protocol"] == "bgp"] - rib_route = rib_route[0] - attr_id = rib_route["attr-id"] - att_set = [a for a in bgp_rib_attrsets if a["index"] == attr_id][0] - data.update({ - "protocol_attributes": { - "local_as": self._getObj(bgp_protocol, *["autonomous-system"], default=-1), - "remote_as": self._getObj(neighbor, *["peer-as"], default=-1), - "peer_id": self._getObj(neighbor, *["peer-address"]), - "as_path": str(self._getObj(att_set, *["as-path", "segment", 0, "member", 0])), - "communities": self._getObj(att_set, *["communities", "community"], default=[]), - "local_preference": self._getObj(att_set, *["local-pref"], default=-1), - "preference2": -1, - "metric": self._getObj(r, *["metric"], default=-1), - "metric2": -1 - } - }) - if "isis" in r["owner"]: - isis_protocol = self._getObj(i, *["protocols", "srl_nokia-isis:isis", "instance"])[0] - level = self._getObj(isis_protocol, *["level", 0, "level-number"], default=-1) - data.update({ - "protocol_attributes": { - "level": level - } - }) - ct_next_hops_data.append(data) - if destination and ( - destination == r["ipv4-prefix"] or destination == str(r["ipv4-prefix"]).split("/")[0]): - return { - r["ipv4-prefix"]: ct_next_hops_data - } - route_data.update({ - r["ipv4-prefix"]: ct_next_hops_data - }) - if protocol: - route_data_filtered = {} - for ipv4_prefix, nhs in route_data.items(): - next_hop_filtered = [n for n in nhs if n["protocol"] == protocol] - if next_hop_filtered: - route_data_filtered.update({ - ipv4_prefix: next_hop_filtered - }) - return route_data_filtered - if destination: # if destination was present , it should not reach here, rather returned earlier. - return {} - return route_data - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def is_alive(self): - alive = False - if self._channel: - try: - output = self.device._jsonrpcRunCli(["date"]) - if "result" in output: - alive = True - except Exception as e: - logging.error( f"Exception in is_alive: {e} -> returning False" ) - return { "is_alive": alive } - - def traceroute(self, destination, source="", ttl=255, timeout=2, vrf=""): - try: - if not vrf: - vrf = "default" - command = "traceroute {} {} {}".format( - destination, - "-m {}".format(ttl) if ttl else "", - "network-instance {}".format(vrf) if vrf else "", - ) - output = self.device._jsonrpcRunCli([command]) - if "error" in output: - return { - "error": output["error"] - } - if "result" not in output: - return { - "error": "No result in output: {}".format(output) - } - result = output["result"][0]['text'] - if "* * *" in result: - return { - 'error': 'unknown host {}'.format(destination) - } - hops = result.split("byte packets")[1] - hop_list = hops.split("\n") - probes = {} - for h in hop_list: - if h.strip(): - h_splits = re.split(r" |\(|\)", h.strip()) - splts = [s.strip() for s in h_splits if s.strip()] - ct_probe = { - 1: { - 'rtt': float(splts[3]), - 'ip_address': splts[2], - 'host_name': splts[1] - }, - 2: { - 'rtt': float(splts[5]), - 'ip_address': splts[2], - 'host_name': splts[1] - }, - 3: { - 'rtt': float(splts[7]), - 'ip_address': splts[2], - 'host_name': splts[1] - } - } - probes[int(splts[0])] = ct_probe - return {"success": probes} - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def _ping(self, destination, source="", ttl=255, timeout=2, size=100, count=5, vrf=""): - try: - if not vrf: - vrf = "default" - output = self.device._jsonrpcRunCli(["ping {} {} {} {} {} {} {}".format( - destination, - "-I {}".format(source) if source else "", - "-t {}".format(ttl) if ttl else "", - "-W {}".format(timeout) if timeout else "", - "-s {}".format(size) if size else "", - "-c {}".format(count) if count else "", - "network-instance {}".format(vrf) if vrf else "", - )]) - if "error" in output: - value = output["error"]["message"] - return {"error": value.strip()} - result = output["result"][0]['text'] - if "Destination Host Unreachable" in result: - return {"error": "unknown host {}".format(destination)} - pings = result.split("bytes of data.")[1] - ping_list = [p for p in pings.split("\n") if p.strip()] - rtt_line = ping_list[-1] - success_data = {} - r_splits = [r for r in re.split(" |/|=", rtt_line.strip()) if r.strip()] - stats_line = ping_list[-2] - s_splits = [s for s in stats_line.split(" ")] - loss = s_splits[5].strip("%") - sent = int(s_splits[0]) - lost_packets = int(sent * int(loss) / 100) - success_data.update({ - 'probes_sent': sent, - 'packet_loss': lost_packets, - 'rtt_min': float(r_splits[5]), - 'rtt_max': float(r_splits[7]), - 'rtt_avg': float(r_splits[6]), - 'rtt_stddev': float(r_splits[8]), - }) - ping_lines = [] - for line in ping_list: - if "ping statistics" in line: - break - if line.strip(): - ping_lines.append(line) - results = [] - for p in ping_lines: - p_splits = [s for s in re.split(" |:|=", p.strip()) if s.strip()] - results.append({ - 'ip_address': p_splits[3], - 'rtt': float(p_splits[9]) - }) - success_data.update({ - "results": results - }) - return {"success": success_data} - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def cli(self, commands, encoding="text"): - """ - Will execute a list of commands and return the output in a dictionary format. - """ - if encoding not in ("text",): - raise NotImplementedError("%s is not a supported encoding" % encoding) - - try: - output = {} - jsonrpc_output = self.device._jsonrpcRunCli(commands) - if "error" in jsonrpc_output: - return jsonrpc_output - result = jsonrpc_output["result"] - for (c, r) in zip(commands, result): - output[c] = r - return output - except Exception as e: - logging.error("Error occurred : {}".format(e)) - - def _clear_candidate(self): - if self.tmp_cfgfile is not None: - self.tmp_cfgfile.close() - self.tmp_cfgfile = None - return True - return False - - def _cli_commit(self, message='', revert_in=None): - """ - Commits the changes requested by the method load_replace_candidate or load_merge_candidate. - :return: - """ - try: - cmds = [ - "enter candidate private ", - "/", - f'commit { self.commit_mode }' + (f'comment "{message}"' if message else '') - ] - output = self.device._jsonrpcRunCli(cmds) - return self._return_result(output) - except Exception as e: - logging.error("Error occurred in _cli_commit: {}".format(e)) - raise CommitError(e) from e - - def compare_config(self): - try: - if not self._is_commit_pending(): #means to do compare for merge operation i.e onbox -remote diff - return self._compare_config_on_box() - else: #means to do compare for replace operation i.e offbox -local diff - if self.running_format == "cli": - raise NotImplementedError("compare_config for 'cli' format is not supported") - - running_config = self.get_config()["running"] - running_config_dict = json.loads(running_config) - cand_config = json.load(self.tmp_cfgfile) - if 'updates' in cand_config: - return cand_config # The update is the diff - cand_config = cand_config['replaces'][0]['value'] - return self._diff_json(cand_config, running_config_dict) - except Exception as e: - logging.error("Error occurred in compare_config: {}".format(e)) - raise CommandErrorException(e) from e - - def _compare_config_on_box(self): - """ - A string showing the difference between the running configuration and the candidate configuration. - The running_config is loaded automatically just before doing the comparison so there is no need for you to do it. - :return: - """ - cmds = [ - "enter candidate private", - "/", - "diff" - ] - output = self.device._jsonrpcRunCli(cmds) - if "result" in output: - result = output["result"] - return result[-1]["text"] if "text" in result[-1] else "" if result[-1] =={} else result[-1] - elif "error" in output: - return output["error"] - return output - - def load_replace_candidate(self, filename=None, config=None): - """ - Accepts either a native JSON formatted config, or a gNMI style JSON config - containing only 'replaces' - """ - try: - return self._load_candidate(filename,config,is_replace=True) - except Exception as e: - raise ReplaceConfigException("Error during load_replace_candidate operation") from e - - def load_merge_candidate(self, filename=None, config=None): - """ - Accepts either a native JSON formatted config (interpreted as 'update /') - or a gNMI style JSON config containing any number of 'deletes','replaces','updates' - """ - try: - return self._load_candidate(filename,config,is_replace=False) - except Exception as e: - raise MergeConfigException("Error during load_merge_candidate operation") from e - - def _return_result(self,output): - if "result" in output: - result = output["result"] - return result[-1]["text"] if "text" in result[-1] else result[-1] - elif "error" in output: - raise Exception(f"Error message from SRL : {output}") - raise Exception(f"result not found in output. Output : {output}") - - def _load_candidate(self,filename,config,is_replace): - if self._is_commit_pending(): - raise Exception("Candidate config is already loaded. Discard it to reload") - - if filename: - with open(filename,"r") as f: - config = f.read() - if not config: - raise Exception("Configuration is empty") - elif not config: - raise Exception("Either 'filename' or 'config' argument must be provided") - - cfg = None - try: - cfg = json.loads(config) # try to load it as json, could keep order of keys - if 'deletes' in cfg or 'updates' in cfg or 'replaces' in cfg: - if is_replace: - if ('deletes' in cfg or 'updates' in cfg): - raise Exception("'load_replace_candidate' cannot contain 'deletes' or 'updates'") - elif "path" not in cfg["replaces"] or cfg["replaces"]["path"] != "/": - raise Exception("'load_replace_candidate' must use 'replaces' with a single path of '/'") - else: - cfg = { 'replaces' if is_replace else 'updates': [ { 'path': '/', 'value': cfg } ] } - - self.tmp_cfgfile = tempfile.TemporaryFile(mode='w+') - json.dump(cfg, self.tmp_cfgfile, sort_keys=True) - self.tmp_cfgfile.seek(0) # Prepare for reading back - return "JSON candidate config loaded for " + ("replace" if is_replace else "merge") - except json.decoder.JSONDecodeError: # Upon error, assume it's CLI commands - cmds = [ - "enter candidate private", - "/", - ] - if is_replace: - cmds.append("delete /") - cmds.extend( config.split("\n") ) - output = self.device._jsonrpcRunCli(cmds) - return self._return_result(output) - - def commit_config(self, message='', revert_in=None): - """ - This method creates a system-wide checkpoint containing the current state before this configuration change. - """ - if revert_in: - raise NotImplementedError("'revert_in' not implemented") - - # Create named checkpoint - self.chkpoint_id = self.chkpoint_id + 1 - chkpt_cmds = [ - f"/tools system configuration generate-checkpoint name NAPALM-{self.chkpoint_id}" - ] - result = self.device._jsonrpcRunCli(chkpt_cmds) - logging.info( f"Checkpoint 'NAPALM-{self.chkpoint_id}' created: {result}" ) - - if self._is_commit_pending(): - try: - self.tmp_cfgfile.seek(0) - json_config = json.load(self.tmp_cfgfile) - if message: - raise NotImplementedError("'message' not supported with JSON config") - self.device._commit_json_config(json_config) - self._clear_candidate() - return "JSON config committed" - - # except grpc._channel._InactiveRpcError as e: - # Log but do not raise - # logging.error(e) - - except Exception as e: - logging.error(e) - raise CommitError(e) from e - else: - return self._cli_commit(message,revert_in) - - def discard_config(self): - if not self._clear_candidate(): - cmds = [ - "enter candidate private", - "/", - "discard now" - ] - output = self.device._jsonrpcRunCli(cmds) - return output["result"] if "result" in output else output - - return "Candidate config discarded" - - def _is_commit_pending(self): - return self.tmp_cfgfile is not None - - def rollback(self): - """ - Reverts changes made by the most recent commit_config call, by loading the named checkpoint that was created - - Caveat: Checkpoints contain the entire system configuration tree, and restore the system state to the point at which - the (named) checkpoint was created (i.e. most recent commit_config call) - If changes were made to the config after that checkpoint was created, those changes will be reverted too (!) - - In a highly concurrent environment in which multiple systems are provisioning nodes, it may be better to implement fine-grained - rollback consisting of only incremental changes, rather than the entire system state. In that case, 'rollback' would be implemented - by another call to commit_config, containing the original config subtree. - """ - try: - output = self.device._jsonrpcRunCli( - [ - "enter candidate private", - f"load checkpoint name NAPALM-{self.chkpoint_id}", # Use named checkpoint to avoid parallel overwrite - f"commit {self.commit_mode}" - ] - ) - return output - except Exception as e: - logging.error("Error occurred : {}".format(e)) - raise CommitError(e) from e - - def _getObj(self, obj, *keys, default=""): - try: - if len(keys) == 1: - output = obj[keys[0]] - return output if output else default - else: - output = self._getObj(obj[keys[0]], *keys[1:], default=default) - return output if output else default - except Exception as e: - logging.error(e) - # raise type(e)("{} occurred when trying to get path {}".format(e, keys)) - # return "##NOTFOUND##" - return default - - def _get_old(self, obj, *keys, default=""): - try: - if len(keys) == 1: - return obj[keys[0]] - else: - return self._get_old(obj[keys[0]], *keys[1:]) - except Exception as e: - raise type(e)("{} occurred when trying to get path {}".format(e, keys)) - # return "##NOTFOUND##" - # return default - - def _removeNotFound(self, obj): - """ - removes the ##NOTFOUND## from the obj - :param obj: shd be a dict or list. Can be nested - :return: - """ - if isinstance(obj, dict): - result = {} - for k, v in obj.items(): - if isinstance(v, dict) or isinstance(v, list): - result[k] = self._removeNotFound(v) - elif isinstance(v, str) and v == "##NOTFOUND##": - continue - else: - result[k] = v - return result - elif isinstance(obj, list): - result = [] - for r in obj: - if isinstance(r, dict) or isinstance(r, list): - result.append(self._removeNotFound(r)) - elif isinstance(r, str) and r == "##NOTFOUND##": - continue - else: - result.append(r) - return result - - def _diff_json(self, newjson, oldjson): - try: - j = jsondiff.jsondiff() - return j.cmp_dict(newjson, oldjson) - except Exception as e: - logging.error("Error occurred : {}".format(e)) - -class SRLAPI(object): - def __init__(self, hostname, username, password, timeout=60, optional_args=None): - """Constructor.""" - self.device = None - self._metadata = None - - self.hostname = hostname - self.username = username - self.password = password - self.timeout = timeout - - self._stub = None - self._channel = None - - if optional_args is None: - optional_args = {} - self.gnmi_port = optional_args.get("gnmi_port", 57400) - self.jsonrpc_port = optional_args.get("jsonrpc_port", 443) - if self.gnmi_port: - self.target = str(self.hostname) + ":" + str(self.gnmi_port) - self.target_name = optional_args.get("target_name", "") - self.skip_verify = optional_args.get("skip_verify", False) - self.insecure = optional_args.get("insecure", False) - self.encoding = optional_args.get("encoding", "JSON_IETF") - - self.tls_ca = optional_args.get("tls_ca", "") - self.tls_cert = optional_args.get("tls_cert", "") - self.tls_key = optional_args.get("tls_key", "") - - ciphers = optional_args.get("jsonrpc_ciphers", - "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES256-SHA") - - self.jsonrpc_session = requests.session() - self.jsonrpc_session.mount("https://", TLSHttpAdapter(ciphers=ciphers)) - - # Warn about incompatible/oddball settings - if self.jsonrpc_port == 80: - if not self.insecure: - logging.warning( "Secure JSON RPC uses port 443, not 80. " + - "Set 'insecure=True' flag to indicate this is ok" ) - elif self.jsonrpc_port != 443: - logging.warning( f"Non-default JSON RPC port configured ({self.jsonrpc_port}), typically only 443(default) or 80 are used" ) - - if not self.insecure: - if not self.tls_ca: - logging.warning( "Incompatible settings: insecure=False " + - "requires certificate parameter 'tls_ca' to be set " + - "when using self-signed certificates" ) - - def open(self): - """Implement the NAPALM method open (mandatory)""" - try: - # read the certificates - certs = {} - if self.tls_ca: - certs["root_certificates"] = self._readFile(self.tls_ca) - if self.tls_cert: - certs["certificate_chain"] = self._readFile(self.tls_cert) - if self.tls_key: - certs["private_key"] = self._readFile(self.tls_key) - - # If not provided and 'insecure' flag is set, fetch CA cert from server - if 'root_certificates' not in certs and self.insecure: - # Lazily import dependencies - from cryptography import x509 - import ssl - from cryptography.hazmat.backends import default_backend - - ssl_cert = ssl.get_server_certificate((self.hostname, self.gnmi_port)).encode("utf-8") - certs["root_certificates"] = ssl_cert - logging.warning("Using server certificate as root CA due to 'insecure' flag, not recommended for production use" ) - if not self.target_name: - ssl_cert_deserialized = x509.load_pem_x509_certificate(ssl_cert, default_backend()) - ssl_cert_common_names = ssl_cert_deserialized.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME) - self.target_name = ssl_cert_common_names[0].value - logging.warning(f'ssl_target_name_override(={self.target_name}) is auto-discovered, should be used for testing only!') - - credentials = grpc.ssl_channel_credentials(**certs) - self._metadata = [("username", self.username), ("password", self.password)] - - # open a secure channel, note that this does *not* send username/pwd yet... - self._channel = grpc.secure_channel( - target=self.target, - credentials=credentials, - options=(("grpc.ssl_target_name_override", self.target_name),), - ) - - if self._stub is None: - self._stub = gnmi_pb2.gNMIStub(self._channel) - # print("stub", self._stub) - except Exception as e: - logging.error("Error in Connection to SRL : {}".format(e)) - raise ConnectionException(e) from e - - def close(self): - """Implement the NAPALM method close (mandatory)""" - try: - if not self._channel: - logging.warning("No grpc channels created to close") - return - self._channel.close() - self._channel = None - self._stub = None - except Exception as e: - logging.error("Error occurred : {}".format(e)) - raise ConnectionException(e) from e - - @staticmethod - def _readFile(filename): - """ - Reads a binary certificate/key file - Parameters: - optionName(str): used to read filename from options - Returns: - File content - Raises: - ConnectionException: file does not exist or read excpetions - """ - path = "/etc/ssl:/etc/ssl/certs:/etc/ca-certificates" - - if filename: - if filename.startswith("~"): - filename = os.path.expanduser(filename) - if not filename.startswith("/"): - for entry in path.split(":"): - if os.path.isfile(os.path.join(entry, filename)): - filename = os.path.join(entry, filename) - break - if os.path.isfile(filename): - try: - with open(filename, "rb") as f: - return f.read() - except Exception as exc: - raise ConnectionException( - "Failed to read cert/keys file %s: %s" % (filename, exc) - ) - else: - raise ConnectionException( - "Cert/keys file %s does not exist" % filename - ) - return None - - def _jsonrpcRunCli(self, cmds): - data = { - "jsonrpc": "2.0", - "id": 0, - "method": "cli", - "params": { - "commands": cmds - } - } - return self._jsonrpcPost(data) - - def _jsonrpcSet(self, cmds, other_params=None): - data = { - "jsonrpc": "2.0", - "id": 0, - "method": "set", - "params": { - "commands": cmds - } - } - if other_params: - data["params"].update(other_params) - return self._jsonrpcPost(data) - - def _jsonrpcGet(self, cmds, other_params=None): - data = { - "jsonrpc": "2.0", - "id": 0, - "method": "get", - "params": { - "commands": cmds - } - } - if other_params: - data["params"].update(other_params) - return self._jsonrpcPost(data) - - def _jsonrpcPost(self, json_data, timeout=None): - headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } - proto = "https" if (self.jsonrpc_port==443 or (self.jsonrpc_port!=80 and not self.insecure)) else "http" - geturl = f"{proto}://{self.hostname}:{self.jsonrpc_port}/jsonrpc" - cert = ( self.tls_cert, self.tls_key ) if self.tls_cert and self.tls_key else None - resp = self.jsonrpc_session.post(geturl, headers=headers, json=json_data, - timeout=timeout if timeout else self.timeout, - auth=(self.username, self.password), cert=cert, - verify=False if self.skip_verify else self.tls_ca) - resp.raise_for_status() - return resp.json() if resp.text else "" - - def _gnmiGet(self, prefix, path, pathType): - """ - Executes a gNMI Get request - Encoding that is used for data serialization is automatically determined - based on the remote device capabilities. This gNMI plugin has implemented - suppport for JSON_IETF (preferred) and JSON (fallback). - Parameters: - type (str): Type of data that is requested: ALL, CONFIG, STATE - prefix (str): Path prefix that is added to all paths (XPATH syntax) - paths (list): List of paths (str) to be captured - Returns: - str: GetResponse message converted into JSON format - """ - # Remove all input parameters from kwargs that are not set - - input = {"path": path, "type": pathType, "encoding": self.encoding} - input["path"] = [self._encodeXpath(path) for path in input["path"]] - input["prefix"] = self._encodeXpath(prefix) - - try: - request = json_format.ParseDict(input, gnmi_pb2.GetRequest()) - response = self._stub.Get(request, metadata=self._metadata) - # print("response:", response) - except Exception as e: - if "StatusCode.INVALID_ARGUMENT" in str(e): - return "" - #logging.exception(e) - else: - for line in str(e).splitlines(False): - if "detail" in line: - raise Exception(line.strip()) from e - output = self._mergeToSingleDict( - json_format.MessageToDict(response)["notification"] - ) - return output - - # def _gnmiSet1(self, prefix=None, updates=None, replaces=None, deletes=None, extensions=None): - # request = gnmi_pb2.SetRequest() - # if prefix: - # request.prefix.CopyFrom(prefix) - # test_list = [updates, replaces, deletes] - # if not any(test_list): - # raise Exception("At least update, replace, or delete must be specified!") - # for item in test_list: - # if not item: - # continue - # if not isinstance(item, (list, set)): - # raise Exception("updates, replaces, and deletes must be iterables!") - # if updates: - # updates1 =[] - # for u in updates: - # update = gnmi_pb2.Update() - # update.val.json_ietf_val = gnmi_pb2.Path(u["val"]) - # update.path.CopyFrom(u["path"]) - # updates1.append(update) - # request.update.extend(updates1) - # if replaces: - # request.replace.extend(replaces) - # if deletes: - # request.delete.extend(deletes) - # if extensions: - # request.extension.extend(extensions) - # - # - # response = self._stub.Set(request, metadata={"username":""}) - # print(response) - - - # def _gnmi_update(self, update_path, update_json): - # update = gnmi_pb2.Update() - # path = json_format.ParseDict(self._encodeXpath(update_path), gnmi_pb2.Path()) - # update.path.CopyFrom(path) - # update.val.json_ietf_val = json.dumps(update_json).encode("utf-8") - # updates = [update] - # self._gnmiSet(update = updates) - - - # def gnmi_replace(self, replace_json): - # update = gnmi_pb2.Update() - # #path = json_format.ParseDict(self._encodeXpath(replace_path), gnmi_pb2.Path()) - # #update.path.CopyFrom(path) - # update.val.json_ietf_val = json.dumps(replace_json).encode("utf-8") - # updates = [update] - # self._gnmiSet(replace = updates) - - # def _gnmiSet(self, prefix=None, delete=None, replace=None, update=None): - # request = gnmi_pb2.SetRequest() - # if prefix: request.prefix.CopyFrom(prefix) - # if update: request.update.extend(update) - # if delete: request.delete.extend(delete) - # if replace: request.replace.extend(replace) - # self._stub.Set(request, metadata=self._metadata) - - def _commit_json_config(self,json_config): - request = gnmi_pb2.SetRequest() - if 'deletes' in json_config: - request.delete.extend( [ json_format.ParseDict(self._encodeXpath(p), gnmi_pb2.Path()) for p in json_config['deletes']] ) - for k in ('replaces','updates'): - if k in json_config: - items = [] - for u in json_config[k]: - update = gnmi_pb2.Update() - path = json_format.ParseDict(self._encodeXpath(u['path']), gnmi_pb2.Path()) - update.path.CopyFrom(path) - update.val.json_ietf_val = json.dumps(u['value']).encode("utf-8") - items.append(update) - if k=='replaces': - request.replace.extend( items ) - else: - request.update.extend( items ) - self._stub.Set(request, metadata=self._metadata) - - @staticmethod - def _encodeXpath(path): - """ - Encodes XPATH to dict representation that allows conversion to gnmi_pb.Path object - Parameters: - xpath (str): path string using XPATH syntax - Returns: - (dict): path dict using gnmi_pb2.Path structure for easy conversion - """ - mypath = [] - xpath = path.strip("\t\n\r /") - if xpath: - path_elements = re.split(r"""/(?=(?:[^\[\]]|\[[^\[\]]+\])*$)""", xpath) - for e in path_elements: - entry = {"name": e.split("[", 1)[0]} - eKeys = re.findall(r"\[(.*?)\]", e) - dKeys = dict(x.split("=", 1) for x in eKeys) - if dKeys: - entry["key"] = dKeys - mypath.append(entry) - return {"elem": mypath} - return {} - - def _mergeToSingleDict(self, rawData): - result = {} - - for entry in rawData: - if "syncResponse" in entry and entry["syncResponse"]: - # Ignore: SyncResponse is sent after initial update - break - elif "update" not in entry: - # Ignore: entry without updates - break - elif "timestamp" not in entry: - # Subscribe response, enter update context - entry = entry["update"] - else: - # Get response, keep context - pass - - prfx = result - if ("prefix" in entry) and ("elem" in entry["prefix"]): - prfx_elements = entry["prefix"]["elem"] - else: - prfx_elements = [] - - for elem in prfx_elements: - eleName = elem["name"] - if "key" in elem: - eleKey = json.dumps(elem["key"]) - eleName = "___" + eleName - # Path Element has key => must be list() - if eleName in prfx: - # Path Element exists => Change Context - prfx = prfx[eleName] - if eleKey not in prfx: - # List entry does not exist => Create - prfx[eleKey] = elem["key"] - prfx = prfx[eleKey] - else: - # Path Element does not exist => Create - prfx[eleName] = {} - prfx = prfx[eleName] - prfx[eleKey] = elem["key"] - prfx = prfx[eleKey] - else: - # Path Element hasn't key => must be dict() - if eleName in prfx: - # Path Element exists => Change Context - prfx = prfx[eleName] - else: - # Path Element does not exist => Create - prfx[eleName] = {} - prfx = prfx[eleName] - - for _upd in entry["update"]: - if "val" not in _upd: - # requested path without content (no value) => skip - continue - elif ("path" in _upd) and ("elem" in _upd["path"]): - path_elements = _upd["path"]["elem"] - cPath = prfx - elif prfx_elements: - path_elements = prfx_elements - cPath = result - else: - # No path at all, replace the objecttree with value - result = self._decodeVal(_upd["val"]) - prfx = result - continue - - # If path_elements has more than just a single entry, - # we need to create/navigate to the specified subcontext - for elem in path_elements[:-1]: - eleName = elem["name"] - if "key" in elem: - eleKey = json.dumps(elem["key"]) - eleName = "___" + eleName - # Path Element has key => must be list() - if eleName in cPath: - # Path Element exists => Change Context - cPath = cPath[eleName] - if eleKey not in cPath: - # List entry does not exist => Create - cPath[eleKey] = elem["key"] - cPath = cPath[eleKey] - else: - # Path Element does not exist => Create - cPath[eleName] = {} - cPath = cPath[eleName] - cPath[eleKey] = elem["key"] - cPath = cPath[eleKey] - else: - # Path Element hasn't key => must be dict() - if eleName in cPath: - # Path Element exists => Change Context - cPath = cPath[eleName] - else: - # Path Element does not exist => Create - cPath[eleName] = {} - cPath = cPath[eleName] - - # The last entry of path_elements is the leaf element - # that needs to be created/updated - leaf_elem = path_elements[-1] - if "key" in leaf_elem: - eleKey = json.dumps(leaf_elem["key"]) - eleName = "___" + leaf_elem["name"] - if eleName not in cPath: - cPath[eleName] = {} - cPath = cPath[eleName] - cPath[eleKey] = self._decodeVal(_upd["val"]) - else: - cPath[leaf_elem["name"]] = self._decodeVal(_upd["val"]) - - return self._dictToList(result) - - def _decodeVal(self, val): - """ - Decodes value from dict representation converted from gnmi_pb.TypedValue object - Parameters: - val (dict): decoded gnmi_pb.TypedValue object - Returns: - (ANY): extracted data - """ - if "jsonIetfVal" in val: - return json.loads(base64.b64decode(val["jsonIetfVal"])) - elif "jsonVal" in val: - return json.loads(base64.b64decode(val["jsonVal"])) - else: - raise ConnectionException( - "gNMI plugin does not support encoding for value: %s" % json.dumps(val) - ) - - def _dictToList(self, aDict): - for key in aDict.keys(): - if key.startswith("___"): - aDict[key[3:]] = [ - self._dictToList(val) if isinstance(val, dict) else val - for val in aDict[key].values() - ] - del aDict[key] - else: - if isinstance(aDict[key], dict): - aDict[key] = self._dictToList(aDict[key]) - return aDict - diff --git a/napalm_srl/__init__.py b/napalm_srlinux/__init__.py similarity index 83% rename from napalm_srl/__init__.py rename to napalm_srlinux/__init__.py index 9f67f51..2049e5a 100644 --- a/napalm_srl/__init__.py +++ b/napalm_srlinux/__init__.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations under # the License. -"""napalm-srl package.""" -from napalm_srl.srl import NokiaSRLDriver # noqa +"""napalm-srlinux package.""" +from napalm_srlinux.srlinux import NokiaSRLinuxDriver # noqa -__all__ = ('NokiaSRLDriver',) +__all__ = ('NokiaSRLinuxDriver',) diff --git a/napalm_srlinux/srlinux.py b/napalm_srlinux/srlinux.py new file mode 100644 index 0000000..641f6d2 --- /dev/null +++ b/napalm_srlinux/srlinux.py @@ -0,0 +1,951 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Nokia. All rights reserved. +# +# The contents of this file are licensed under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# SPDX-License-Identifier: Apache-2.0 + +""" +Napalm driver for SR Linux. + +Read https://napalm.readthedocs.io for more information. +""" + +import jsonpath_ng +import logging +import re +import datetime +import enum + +from napalm.base import NetworkDriver +from napalm.base.exceptions import ( + ConnectionException, + MergeConfigException, + ReplaceConfigException, + CommandErrorException, + CommitError, +) + +import httpx +from typing import Optional, AnyStr, Union + + +class NokiaSRLinuxDriver(NetworkDriver): + """Napalm driver for SRLinux.""" + + def __init__(self, hostname, username, password, timeout=60, optional_args=None): + """Constructor.""" + self.hostname = hostname + self.username = username + self.password = password + self.timeout = timeout + self.running_format = optional_args.get("running_format", "json") if optional_args else "json" + self.device = SRLinuxDevice(hostname, username, password, timeout=60, optional_args=optional_args) + + def open(self) -> None: + self.device.open() + + def close(self) -> None: + self.device.close() + + def get_arp_table(self, vrf: Optional[AnyStr] = "") -> list: + """ + Returns a list of dictionaries having the following set of keys: + interface (string) + mac (string) + ip (string) + age (float) + ‘vrf’ of null-string will default to all VRFs. + Specific ‘vrf’ will return the ARP table entries for that VRFs + (including potentially ‘default’ or ‘global’). + + In all cases the same data structure is returned and no reference to the VRF that was + used is included in the output. + """ + raise NotImplementedError + + def get_bgp_neighbors(self) -> dict: + """ + Returns a dictionary of dictionaries. The keys for the first dictionary will be the vrf + (global if no vrf). The inner dictionary will contain the following data for each vrf: + + router_id + peers - another dictionary of dictionaries. Outer keys are the IPs of the neighbors. + The inner keys are: + local_as (int) + remote_as (int) + remote_id - peer router id + is_up (True/False) + is_enabled (True/False) + description (string) + uptime (int in seconds) + address_family (dictionary) - A dictionary of address families available for + the neighbor. + So far it can be ‘ipv4’ or ‘ipv6’ + received_prefixes (int) + accepted_prefixes (int) + sent_prefixes (int) + Note, if is_up is False and uptime has a positive value then this indicates the + uptime of the last active BGP session. + """ + + return_data = { + "global": { + "router_id": "", + "peers": {} + } + } + + jrpc_output = self.device.get_paths( + ["/network-instance[name=*]/protocols/bgp", "/system/information"], + SRLinuxDevice.RPCDatastore.STATE + ) + + # TODO: handle exception + + bgp_data = jrpc_output[0] + system_data = jrpc_output[1] + + if not system_data.get("current-datetime"): + raise Exception("Missing 'current-datetime' key in /system/information") + + system_date_time = datetime.datetime.strptime( + system_data.get("current-datetime"), "%Y-%m-%dT%H:%M:%S.%fZ" + ) + + for network_instance in bgp_data["srl_nokia-network-instance:network-instance"]: + instance_name = network_instance.get("name") + + # SRLinux global route tables is called "default" + if instance_name == "default": + instance_name = "global" + + router_id = NokiaSRLinuxDriver._get_value_from_jsonpath( + "$.protocols.srl_nokia-bgp:bgp.router-id", network_instance) + global_asn = NokiaSRLinuxDriver._get_value_from_jsonpath( + "$.protocols.srl_nokia-bgp:bgp.autonomous-system", network_instance) + + return_data.update({instance_name: {"router_id": router_id, "peers": {}}}) + + # extract BGP Neighbours + bgp_neighbors = NokiaSRLinuxDriver._get_by_jsonpath( + "$.protocols.srl_nokia-bgp:bgp.neighbor[*]", + network_instance + ) + + for neighbor in bgp_neighbors: + cur_neighbor = { + "local_as": neighbor.get("local-as", {}).get("as-number", global_asn), + "remote_as": neighbor.get("peer-as", global_asn), + "remote_id": neighbor.get("peer-remote-id", neighbor.get("peer-address")), + "is_up": True if neighbor.get("session-state", "nil") == "established" else False, + "is_enabled": True if neighbor.get("admin-state", False) == "enable" else False, + "description": neighbor.get("description", ""), + "uptime": -1, + "address_family": {}, + } + + if cur_neighbor["is_up"]: + last_established = datetime.datetime.strptime( + neighbor.get("last-established"), + "%Y-%m-%dT%H:%M:%S.%fZ") + cur_neighbor["uptime"] = (system_date_time - last_established).seconds + + for afi_safi in neighbor.get("afi-safi", []): + # IPv4 + if afi_safi.get("afi-safi-name") == "srl_nokia-common:ipv4-unicast": + cur_neighbor["address_family"]["ipv4"] = { + "received_prefixes": afi_safi.get("received-routes", -1), + "sent_prefixes": afi_safi.get("sent-routes", -1), + "accepted_prefixes": afi_safi.get("accepted-routes", 0), + } + # IPv6 + if afi_safi.get("afi-safi-name") == "srl_nokia-common:ipv6-unicast": + cur_neighbor["address_family"]["ipv6"] = { + "received_prefixes": afi_safi.get("received-routes", -1), + "sent_prefixes": afi_safi.get("sent-routes", -1), + "accepted_prefixes": afi_safi.get("accepted-routes", 0), + } + + return_data[instance_name]["peers"][neighbor.get("peer-address")] = cur_neighbor + return return_data + + def get_bgp_neighbors_detail(self, neighbor_address: Optional[AnyStr] = "") -> dict: + """ + :param neighbor_address: + :return: + Returns a dictionary of dictionaries. The keys for the first dictionary will be the vrf (global if no vrf). + The keys of the inner dictionary represent the AS number of the neighbors. + Leaf dictionaries contain the following fields: + up (True/False) + local_as (int) + remote_as (int) + router_id (string) + local_address (string) + routing_table (string) + local_address_configured (True/False) + local_port (int) + remote_address (string) + remote_port (int) + multihop (True/False) + multipath (True/False) + remove_private_as (True/False) + import_policy (string) + export_policy (string) + input_messages (int) + output_messages (int) + input_updates (int) + output_updates (int) + messages_queued_out (int) + connection_state (string) + previous_connection_state (string) + last_event (string) + suppress_4byte_as (True/False) + local_as_prepend (True/False) + holdtime (int) + configured_holdtime (int) + keepalive (int) + configured_keepalive (int) + active_prefix_count (int) + received_prefix_count (int) + accepted_prefix_count (int) + suppressed_prefix_count (int) + advertised_prefix_count (int) + flap_count (int) + + """ + raise NotImplementedError + + def get_environment(self): + """ + Returns a dictionary where: + + fans is a dictionary of dictionaries where the key is the location and the values: + status (True/False) - True if it’s ok, false if it’s broken + temperature is a dict of dictionaries where the key is the location and the values: + temperature (float) - Temperature in celsius the sensor is reporting. + is_alert (True/False) - True if the temperature is above the alert threshold + is_critical (True/False) - True if the temp is above the critical threshold + power is a dictionary of dictionaries where the key is the PSU id and the values: + status (True/False) - True if it’s ok, false if it’s broken + capacity (float) - Capacity in W that the power supply can support + output (float) - Watts drawn by the system + cpu is a dictionary of dictionaries where the key is the ID and the values + %usage + memory is a dictionary with: + available_ram (int) - Total amount of RAM installed in the device + used_ram (int) - RAM in use in the device + """ + raise NotImplementedError + + def get_facts(self): + """ + Returns a dictionary containing the following information: + uptime - Uptime of the device in seconds. + vendor - Manufacturer of the device. + model - Device model. + hostname - Hostname of the device + fqdn - Fqdn of the device + os_version - String with the OS version running on the device. + serial_number - Serial number of the device + interface_list - List of the interfaces of the device + """ + + raise NotImplementedError + + def get_interfaces(self) -> dict: + """ + Returns a dictionary of dictionaries. + The keys for the first dictionary will be the interfaces in the devices. + The inner dictionary contains the following data for each interface: + is_up (True/False) + is_enabled (True/False) + description (string) + last_flapped (float in seconds) + speed (float in Mbit) + MTU (in Bytes) + mac_address (string) + """ + + interfaces = {} + + json_data = self.device.get_paths( + ["/interface[name=*]", + "/system/information", + ], + SRLinuxDevice.RPCDatastore.STATE + ) + + # TODO: handle exception + + interfaces_json = json_data[0] + system_data = json_data[1] + + for interface in interfaces_json.get("srl_nokia-interfaces:interface"): + interfaces[interface.get("name")] = { + "is_up": True if interface.get("oper-state") == "up" else False, + "is_enabled": True if interface.get("admin-state") == "enable" else False, + "description": interface.get("description"), + "last-flapped": NokiaSRLinuxDriver._calculate_time_since( + system_data.get("current-datetime"), + interface.get("last-change") + ), + "speed": NokiaSRLinuxDriver._port_speed_to_mbits(interface.get("ethernet", {}).get("port-speed")), + "mtu": interface.get("mtu"), + "mac_address": interface.get("ethernet", {}).get("hw-mac-address") + } + + return interfaces + + def get_interfaces_counters(self): + """ + Returns a dictionary of dictionaries where the first key is an interface name + and the inner dictionary contains the following keys: + tx_errors (int) + rx_errors (int) + tx_discards (int) + rx_discards (int) + tx_octets (int) + rx_octets (int) + tx_unicast_packets (int) + rx_unicast_packets (int) + tx_multicast_packets (int) + rx_multicast_packets (int) + tx_broadcast_packets (int) + rx_broadcast_packets (int) + """ + raise NotImplementedError + + def get_interfaces_ip(self): + """ + Returns all configured IP addresses on all interfaces as a dictionary of dictionaries. + of the main dictionary represent the name of the interface. + Values of the main dictionary represent are dictionaries that may consist of two keys + ‘ipv4’ and ‘ipv6’ (one, both or none) which are themselves dictionaries with the IP addresses as keys. + Each IP Address dictionary has the following keys: + prefix_length (int) + """ + raise NotImplementedError + + def get_ipv6_neighbors_table(self): + """ + Get IPv6 neighbors table information. + + Return a list of dictionaries having the following set of keys: + + interface (string) + mac (string) + ip (string) + age (float) in seconds + state (string) + """ + raise NotImplementedError + + def get_lldp_neighbors(self) -> dict: + """ + Returns a dictionary where the keys are local ports and the value is a list of dictionaries + with the following information: + hostname + port + """ + lldp_neighbors = {} + json_output = self.device.get_paths( + ["/system/lldp"], + SRLinuxDevice.RPCDatastore.STATE + ) + + #TODO: Handle exception + + if not json_output: + # no lldp interfaces + return {} + + lldp_interfaces = json_output[0].get("interface") + + for interface in lldp_interfaces: + if not interface.get("neighbor"): + continue + + neighbors = [] + + for neighor in interface.get("neighbor"): + neighbors.append( + { + "port": neighor.get("port-id"), + "hostname": neighor.get("system-name") + } + ) + lldp_neighbors[interface.get("name")] = neighbors + + return lldp_neighbors + + def get_lldp_neighbors_detail(self, interface: Optional[AnyStr] = "") -> dict: + """ + Returns a detailed view of the LLDP neighbors as a dictionary containing lists + of dictionaries for each interface. + + Empty entries are returned as an empty string (e.g. ‘’) or list where applicable. + + Inner dictionaries contain fields: + parent_interface (string) + remote_port (string) + remote_port_description (string) + remote_chassis_id (string) + remote_system_name (string) + remote_system_description (string) + remote_system_capab (list) with any of these values + other + repeater + bridge + wlan-access-point + router + telephone + docsis-cable-device + station + remote_system_enabled_capab (list) + """ + lldp_neighbors = {} + json_output = self.device.get_paths( + ["/system/lldp"], + SRLinuxDevice.RPCDatastore.STATE + ) + + # TODO: handle exception + + if not json_output: + # no lldp interfaces + return {} + + lldp_interfaces = json_output[0].get("interface") + + # TODO: respect the `interface` param + for interface in lldp_interfaces: + if not interface.get("neighbor"): + continue + + neighbors = [] + + for neighor in interface.get("neighbor"): + neighbors.append( + { + "parent_interface": interface.get("name"), + "remote_port": neighor.get("port-id"), + "remote_port_description": neighor.get("port-description", ""), + "remote_chassis_id": neighor.get("chassis-id", ""), + "remote_system_name": neighor.get("system-name", ""), + "remote_system_description": neighor.get("system-description", ""), + "remote_system_capab": [cap.get("name").split(":")[1].lower() for cap in + neighor.get("capability", [])] + } + ) + lldp_neighbors[interface.get("name")] = neighbors + + return lldp_neighbors + + def get_network_instances(self, name=""): + """ + Return a dictionary of network instances (VRFs) configured, including default/global + Parameters: name (string) – + Returns: + name (dict) + name (unicode) + type (unicode) + state (dict) + route_distinguisher (unicode) + interfaces (dict) + interface (dict) + interface name: (dict) + """ + raise NotImplementedError + + def get_users(self) -> dict: + """ + Returns a dictionary with the configured users. + The keys of the main dictionary represents the username. + The values represent the details of the user, represented by the following keys: + level (int) + password (str) + sshkeys (list) + The level is an integer between 0 and 15, where 0 is the lowest access + and 15 represents full access to the device. + """ + + users_dict = {} + + paths_data = self.device.get_paths( + ["/system/aaa/authentication/admin-user", + "/system/aaa/authentication/user[username=*]"], + SRLinuxDevice.RPCDatastore.STATE + ) + + # TODO: handle thrown exception + + # first result is the admin user + admin_user = paths_data[0] + users_dict.update({ + "admin": { + "level": 0, # Not supported by SRLinux + "password": admin_user["password"], + "ssh-keys": [k for k in admin_user["ssh-key"]] + } + }) + + # all other users + for user in paths_data[1]["user"]: + users_dict.update({ + user["username"]: { + "level": 0, + "password": user["password"], + "ssh-keys": [k for k in user.get("ssh-key", [])] + } + }) + + return users_dict + + def get_bgp_config(self, group="", neighbor=""): + """ + Returns a dictionary containing the BGP configuration. Can return either the whole config, either the config only for a group or neighbor. + + :param neighbor: specific BGP neighbor. + :param group: specific BGP group. + :return: Returns the configuration of a specific BGP neighbor /BGP group + """ + raise NotImplementedError + + def get_snmp_information(self): + """ + :return: + Returns a dict of dicts containing SNMP configuration. Each inner dictionary contains these fields + + chassis_id (string) + community (dictionary) + contact (string) + location (string) + """ + raise NotImplementedError + + def get_config(self, retrieve: str = "all", full: bool = False, sanitized: bool = False, format: str = "text"): + """ + :param retrieve: Which configuration type you want to populate, default is all of them. The rest will be set to “”. + :param full:Retrieve all the configuration. For instance, on ios, “sh run all”. + :param sanitized:Remove secret data. Default: False. + :return:Return the configuration of a device. + """ + raise NotImplementedError + + def get_ntp_servers(self): + """ + :return:Returns the NTP servers configuration as dictionary. The keys of the dictionary represent the IP Addresses of the servers. Inner dictionaries do not have yet any available keys. + """ + raise NotImplementedError + + def get_ntp_stats(self): + """ + :return:Returns a list of NTP synchronization statistics. + """ + raise NotImplementedError + + def get_optics(self): + """ + :return:Fetches the power usage on the various transceivers installed on the switch (in dbm), and returns a view that conforms with the openconfig model openconfig-platform-transceiver.yang + """ + raise NotImplementedError + + def get_mac_address_table(self): + """ + Returns a lists of dictionaries. Each dictionary represents an entry in the MAC Address Table, + having the following keys: + mac (string) + interface (string) + vlan (int) + active (boolean) + static (boolean) + moves (int) + last_move (float) + """ + raise NotImplementedError + + def get_route_to(self, destination='', protocol='', longer=False): + """ + Returns a dictionary of dictionaries containing details of all available routes to a destination. + """ + raise NotImplementedError + + def is_alive(self) -> dict: + """ + Tests if the device is reachable. Returns a dict with a single key: 'is_alive', value is a bool. + """ + try: + alive = self.device.open() + return {'is_alive': alive} + except Exception: + return {'is_alive': False} + + def traceroute(self, destination, source="", ttl=255, timeout=2, vrf=""): + raise NotImplementedError + + def ping(self, destination: str, source: Optional[str] = "", ttl: Optional[int] = 255, + timeout: Optional[int] = 2, + size: Optional[int] = 100, + count: Optional[int] = 5, + vrf: Optional[AnyStr] = "default", + source_interface: Optional[AnyStr] = "") -> dict: + """ + Executes a ping against the provided destination + Returns a dictionary in the required NAPALM format. + TODO: update with format + """ + # prefer source_interface if given + ping_src = "" + if source_interface: + ping_src = source_interface + elif source: + ping_src = source + + ping_cmd = [f"ping {destination}", + f"-I {ping_src}" if ping_src else "", + f"-t {ttl}" if ttl else "", + f"-W {timeout}" if timeout else "", + f"-s {size}" if size else "", + f"-c {count}" if count else "", + f"network-instance {vrf}"] + try: + result = self.device.run_cli_commands( + [" ".join(ping_cmd)] + ) + except Exception as e: + return { + 'error': str(e) + } + ping_text = result[0].get("text") + re_pattern = "(\d+) packets transmitted, (\d+) received, (\d*\.?\d*)% packet loss, time (\w+)ms(\nrtt min/avg/max/mdev = (\d*\.?\d*)/(\d*\.?\d*)/(\d*\.?\d*)/(\d*\.?\d*))?" + re_match = re.search(re_pattern, ping_text) + + # If DNS doesn't resolve or a host isn't in the route-table, fail the request. + if not re_match: + return {'error': 'Unable to complete request'} + + groups = re_match.groups() + pings_pattern = '(\d+\.\d+\.\d+\.\d+)\)?: icmp_seq=\d+ ttl=\d+ time=(\d+\.\d+) ms' + pings = re.findall(pings_pattern, ping_text) + + # SRL doesn't print stats if at least one ping isn't successful. + has_stats = len(groups) == 9 + + ping_results = [{'ip_address': p[0], 'rtt': p[1]} for p in pings] + return { + 'success': { + 'probes_sent': groups[0], + 'packet_loss': int(groups[0]) - int(groups[1]), + 'rtt_min': groups[5] if has_stats else -1.0, + 'rtt_max': groups[7] if has_stats else -1.0, + 'rtt_avg': groups[6] if has_stats else -1.0, + 'rtt_stddev': groups[8] if has_stats else -1.0, + 'results': ping_results + } + } + + def cli(self, commands, encoding="text"): + """ + Will execute a list of commands and return the output in a dictionary format. + """ + raise NotImplementedError + + def compare_config(self) -> AnyStr: + """ + Compares the current running configuration with the loaded candidate configuration. + Returns a string that represents the diff between these config datastore's. + """ + raise NotImplementedError + + def load_replace_candidate(self, filename=None, config=None): + """ + Accepts either a native JSON formatted config, or a gNMI style JSON config + containing only 'replaces' + """ + raise NotImplementedError + + def load_merge_candidate(self, filename=None, config=None): + """ + Accepts either a native JSON formatted config (interpreted as 'update /') + or a gNMI style JSON config containing any number of 'deletes','replaces','updates' + """ + raise NotImplementedError + + def commit_config(self, message='', revert_in=None): + """ + This method creates a system-wide checkpoint containing the current state before this configuration change. + """ + raise NotImplementedError + + def discard_config(self): + """ + Discards the current candidate configuration changes. + """ + raise NotImplementedError + + def rollback(self): + """ + Reverts changes made by the most recent commit_config call, by loading the named checkpoint that was created + + Caveat: Checkpoints contain the entire system configuration tree, and restore the system state to the point at which + the (named) checkpoint was created (i.e. most recent commit_config call) + If changes were made to the config after that checkpoint was created, those changes will be reverted too (!) + + In a highly concurrent environment in which multiple systems are provisioning nodes, it may be better to implement fine-grained + rollback consisting of only incremental changes, rather than the entire system state. In that case, 'rollback' would be implemented + by another call to commit_config, containing the original config subtree. + """ + return NotImplementedError + + @staticmethod + def _jsonpath_expr(jsonpath: AnyStr) -> jsonpath_ng.jsonpath.Child: + """ + Builds and escapes a provided JSONPath string and returns a jsonpath.Child object. + """ + # need to single-quote keys with ':' in them + keys = jsonpath.split(".") + + quoted_keys = [] + for k in keys: + # find keys with [...] at the end + if '[' in k and ':' in k: + x = re.findall("(.*)(\[.*])", k) + # quote the key portion, leaving the slice indicator unquoted + quoted_key = f"['{x[0][0]}']{x[0][1]}" + quoted_keys.append(quoted_key) + elif ':' in k: + # quote keys with : in them + quoted_keys.append(f"['{k}']") + else: + # don't touch otherwise + quoted_keys.append(k) + + return jsonpath_ng.parse('.'.join(quoted_keys)) + + @staticmethod + def _get_by_jsonpath(jsonpath: AnyStr, data: dict) -> list: + """ + Fetch a subtree from a jsonpath string and return a list of results. + """ + expr = NokiaSRLinuxDriver._jsonpath_expr(jsonpath) + matches = expr.find(data) + return [m.value for m in matches] + + @staticmethod + def _get_value_from_jsonpath(jsonpath: AnyStr, data: dict) -> Union[None, AnyStr]: + """ + Get a single value from a jsonpath string and return it. + """ + expr = NokiaSRLinuxDriver._jsonpath_expr(jsonpath) + matches = expr.find(data) + + return matches[0].value if matches else None + + @staticmethod + def _calculate_time_since(system_time: AnyStr, reference_time: AnyStr) -> int: + """ + Calculate the difference between a timestamp and the system's time. + SRL timestamps are in the format "%Y-%m-%dT%H:%M:%S.%fZ" + """ + system_datetime = datetime.datetime.strptime(system_time, "%Y-%m-%dT%H:%M:%S.%fZ") + ref_datetime = datetime.datetime.strptime(reference_time, "%Y-%m-%dT%H:%M:%S.%fZ") + return (ref_datetime - system_datetime).seconds + + @staticmethod + def _port_speed_to_mbits(port_speed: str) -> float: + """ + Convert a string port-speed to a floating point of Megabits + """ + port_speeds = { + "10M": 10000.0, + "100M": 100000.0, + "1G": 1000000.0, + "10G": 10000000.0, + "25G": 25000000.0, + "40G": 40000000.0, + "50G": 50000000.0, + "100G": 100000000.0, + "200G": 200000000.0, + "400G": 400000000.0, + "800G": 800000000.0, + "1T": 1000000000.0, + } + return port_speeds.get(port_speed) + + +class SRLinuxDevice(object): + """ + Represents an SRLinux device and abstracts the connection Protocol + used to talk to the device. + """ + + class RPCMethod(str, enum.Enum): + """ + Enum class used to represent RPC Methods + """ + GET = "get" + SET = "set" + VALIDATE = "validate" + CLI = "cli" + + class RPCAction(str, enum.Enum): + """ + Enum class used to represent RPC Actions + """ + REPLACE = "replace" + UPDATE = "update" + DELETE = "delete" + + class RPCDatastore(str, enum.Enum): + """ + Enum class used to represent SRL Data stores for RPC calls. + """ + CANDIDATE = "candidate" + RUNNING = "running" + STATE = "state" + TOOLS = "tools" + + def __init__(self, hostname: str, username: str, password: str, timeout: Optional[int] = 60, + optional_args: Optional[dict] = None): + """Constructor.""" + self.device = None + self.hostname = hostname + self.username = username + self.password = password + self.timeout = timeout + + if optional_args is None: + optional_args = {} + + # Optional Arguments + self.jsonrpc_port = optional_args.get("jsonrpc_port", 443) + self.skip_verify = optional_args.get("skip_verify", False) + self.insecure = optional_args.get("insecure", False) + self.tls_ca = optional_args.get("tls_ca", "") + self.tls_cert_path = optional_args.get("tls_cert_path", "") + self.tls_key_path = optional_args.get("tls_key_path", "") + self.tls_key_password = optional_args.get("tls_key_password", "") + + self.jsonrpc_session = self._new_jsonrpc_client() + + # Warn about incompatible/oddball settings + if self.jsonrpc_port == 80: + if not self.insecure: + logging.warning("Secure JSON RPC uses port 443, not 80." + + "Set 'insecure=True' flag to indicate this is ok") + elif self.jsonrpc_port != 443: + logging.warning( + f"Non-default JSON RPC port configured ({self.jsonrpc_port}), typically only 443(default) or 80 are used") + + if not self.insecure: + if not self.tls_ca: + logging.warning("Incompatible settings: insecure=False " + + "requires certificate parameter 'tls_ca' to be set " + + "when using self-signed certificates") + + def open(self): + """ Check the supplied init params actually work, throw an exception if not.""" + # Set up a JSON RPC Client and test connectivity to the endpoint. + path = "/system/information/version" + ok, data = self.get_paths([path], SRLinuxDevice.RPCDatastore.STATE) + + if ok: + return True + else: + raise Exception("Error opening connection. Error: " + data.get("error").get("message")) + + def close(self): + """Cleanup the HTTP Client """ + self.jsonrpc_session.close() + + def get_paths(self, paths: list, datastore: RPCDatastore) -> list: + """ + Get the subtrees from a list of YANG paths from the specified datastore. + Returns a list of results. + """ + commands = [{"path": p, "datastore": datastore} for p in paths] + + ok, result = self._jsonrpc_request( + SRLinuxDevice.RPCMethod.GET, + {"commands": commands} + ) + + if ok: + return result.get("result") + else: + raise Exception("Error getting subtrees from YANG path. Error: " + result.get("error")) + + def run_cli_commands(self, commands: list) -> list: + """ + Runs a list of CLI commands on the device, returns the result or raises an exception if + a command isn't valid or fails. Returns a list of results. + """ + ok, response = self._jsonrpc_request( + SRLinuxDevice.RPCMethod.CLI, + {"commands": commands} + ) + + if ok: + return response.get("result") + else: + raise Exception(response.get("error", {}).get("message")) + + def _jsonrpc_request(self, method: RPCMethod, params: dict) -> (bool, dict): + """ + Make a JSON RPC request, raise an exception if the HTTP request returns anything other than 2xx + Return a boolean success value (if HTTP 2xx) and the result. + """ + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + request_data = { + "jsonrpc": "2.0", + "id": datetime.datetime.now().strftime('%s'), + "method": method, + "params": params, + } + + proto = "https" if (self.jsonrpc_port == 443 or (self.jsonrpc_port != 80 and not self.insecure)) else "http" + url = f"{proto}://{self.hostname}:{self.jsonrpc_port}/jsonrpc" + + result = self.jsonrpc_session.post(url, headers=headers, json=request_data, timeout=self.timeout) + + if result.status_code == httpx.codes.OK and result.json().get("error"): + return False, result.json() + elif result.status_code == httpx.codes.OK: + return True, result.json() + elif result.status_code == httpx.codes.BAD_REQUEST: + raise Exception("Request raised HTTP/400 BAD REQUEST") + else: + raise Exception(f"Request raised unknown status code {result.status_code}") + + def _new_jsonrpc_client(self): + """ + Create a JSON RPC Client, preconfigured with TLS if required. + """ + cert = None + if not self.insecure: + cert = (self.tls_cert_path, self.tls_key_path, self.tls_key_password) \ + if self.tls_key_password else (self.tls_cert_path, self.tls_key_path) + + opts = { + "verify": (not self.insecure), + "auth": (self.username, self.password) + } + + if cert: + opts["cert"] = cert + + return httpx.Client(**opts) diff --git a/off_box_diff.png b/off_box_diff.png deleted file mode 100755 index ff438493573f87f53c4a1c27b5ce453249e519b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156946 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

=St5h#|BNeNs5B?*n0iCpV8F& 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 dqP#N!MYq0Hy!B=56.0.0", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "napalm-srl" +name = "napalm-srlinux" version="1.0.5" description="Network Automation and Programmability Abstraction Layer driver for Nokia SR Linux" readme = "README.md" diff --git a/requirements.txt b/requirements.txt index 75c446b..99591ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,12 +2,6 @@ napalm>=2.0.0 pip>=20.1.1 pytest>=5.4.3 setuptools>=47.3.1 -paramiko>=2.7.1 -grpcio -protobuf==3.20.3 -cryptography - -# The new 2.0.2 version contains security enhancements -# however botocore 1.31.14 depends on urllib3<1.27 -# urllib3>=2.0.2 -urllib3 +httpx>=0.27.0 +jsonpath-ng>=1.6.0 +pydantic>=2.8.2 diff --git a/setup.cfg b/setup.cfg index e1016ed..cfd8fc5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,8 +30,8 @@ jsonapi = true [coverage:run] include = - napalm_srl/* + napalm_srlinux/* [coverage:report] omit = - napalm_srl/test/* + napalm_srlinux/test/* diff --git a/setup.py b/setup.py index b165f18..f19c12f 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ __author__ = 'Jose Valente ' setup( - name="napalm-srl", + name="napalm-srlinux", version="1.0.5", packages=find_packages(), author="Jose Valente", diff --git a/test/unit/TestNokiaSRLDriver.py b/test/unit/TestNokiaSRLDriver.py index 183b062..e2f0bd4 100644 --- a/test/unit/TestNokiaSRLDriver.py +++ b/test/unit/TestNokiaSRLDriver.py @@ -15,7 +15,7 @@ import unittest import pytest -from napalm_srl import srl +from napalm_srlinux import srlinux from napalm.base.test.base import TestConfigNetworkDriver from napalm.base.exceptions import ( MergeConfigException, @@ -38,7 +38,7 @@ def setUpClass(cls): # "insecure": False "encoding": "JSON_IETF" } - cls.device = srl.NokiaSRLDriver( + cls.device = srl.NokiaSRLinuxDriver( hostname, username, password, timeout=60, optional_args=optional_args ) cls.device.open() diff --git a/test/unit/conftest.py b/test/unit/conftest.py index dc9fd6b..51d865a 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -11,7 +11,7 @@ from napalm.base.test.double import BaseTestDouble -from napalm_srl import srl +from napalm_srlinux import srlinux import json @pytest.fixture(scope="session", autouse=True) @@ -26,7 +26,7 @@ def fin(): request.cls.device.close() request.addfinalizer(fin) - request.cls.driver = srl.NokiaSRLDriver + request.cls.driver = srl.NokiaSRLinuxDriver request.cls.patched_driver = PatchedsrlDriver request.cls.vendor = 'srl' parent_conftest.set_device_parameters(request) @@ -37,7 +37,7 @@ def pytest_generate_tests(metafunc): parent_conftest.pytest_generate_tests(metafunc, __file__) -class PatchedsrlDriver(srl.NokiaSRLDriver): +class PatchedsrlDriver(srl.NokiaSRLinuxDriver): """Patched Skeleton Driver.""" def __init__(self, hostname, username, password, timeout=60, optional_args=None): diff --git a/tox.ini b/tox.ini index 746e406..bd1419d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36 +envlist = py27,py34,py35,py36,py39,py310 [testenv] deps =