Skip to content

Commit 6051075

Browse files
authored
Merge pull request #15 from cisco-ie/nx-client
NX-OS Client Wrapper
2 parents bba96ae + 389823d commit 6051075

File tree

4 files changed

+209
-7
lines changed

4 files changed

+209
-7
lines changed

src/cisco_gnmi/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from .client import Client
2828
from .xr import XRClient
29+
from .nx import NXClient
2930
from .builder import ClientBuilder
3031

31-
__version__ = "1.0.0"
32+
__version__ = "1.0.1"

src/cisco_gnmi/builder.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import logging
2727

2828
import grpc
29-
from . import Client, XRClient
29+
from . import Client, XRClient, NXClient
3030
from .auth import CiscoAuthPlugin
3131
from .util import gen_target_netloc, get_cert_from_target, get_cn_from_cert
3232

@@ -74,6 +74,8 @@ class ClientBuilder(object):
7474
>>> print(capabilities)
7575
"""
7676

77+
os_class_map = {None: Client, "IOS XR": XRClient, "NX-OS": NXClient}
78+
7779
def __init__(self, target):
7880
"""Initializes the builder, most initialization is done via set_* methods.
7981
A target is always required, thus a member of the constructor.
@@ -111,18 +113,18 @@ def set_os(self, name=None):
111113
----------
112114
name : str
113115
"IOS XR" maps to the XRClient class.
116+
"NX-OS" maps to the NXClient class.
114117
None maps to the base Client class which simply wraps the gNMI stub.
115-
["IOS XR", None]
118+
["IOS XR", "NX-OS", None]
116119
117120
Returns
118121
-------
119122
self
120123
"""
121-
os_class_map = {None: Client, "IOS XR": XRClient}
122-
if name not in os_class_map.keys():
124+
if name not in self.os_class_map.keys():
123125
raise Exception("OS not supported!")
124126
else:
125-
self.__client_class = os_class_map[name]
127+
self.__client_class = self.os_class_map[name]
126128
logging.debug("Using %s wrapper.", name or "Client")
127129
return self
128130

@@ -183,6 +185,8 @@ def set_secure_from_target(self):
183185
"""Wraps set_secure(...) but loads root certificates from target.
184186
In effect, simply uses the target's certificate to create an encrypted channel.
185187
188+
TODO: This may not work with IOS XE and NX-OS, uncertain.
189+
186190
Returns
187191
-------
188192
self

src/cisco_gnmi/nx.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""Copyright 2019 Cisco Systems
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions are
6+
met:
7+
8+
* Redistributions of source code must retain the above copyright
9+
notice, this list of conditions and the following disclaimer.
10+
11+
The contents of this file are licensed under the Apache License, Version 2.0
12+
(the "License"); you may not use this file except in compliance with the
13+
License. You may obtain a copy of the License at
14+
15+
http://www.apache.org/licenses/LICENSE-2.0
16+
17+
Unless required by applicable law or agreed to in writing, software
18+
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
19+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
20+
License for the specific language governing permissions and limitations under
21+
the License.
22+
"""
23+
24+
"""Wrapper for NX-OS to simplify usage of gNMI implementation."""
25+
26+
27+
import logging
28+
29+
from six import string_types
30+
from .client import Client, proto, util
31+
32+
33+
class NXClient(Client):
34+
"""NX-OS-specific wrapper for gNMI functionality.
35+
36+
Returns direct responses from base Client methods.
37+
38+
Methods
39+
-------
40+
subscribe_xpaths(...)
41+
Convenience wrapper for subscribe() which helps construct subscriptions for specified xpaths.
42+
43+
Examples
44+
--------
45+
>>> from cisco_gnmi import ClientBuilder
46+
>>> client = ClientBuilder('127.0.0.1:9339').set_os(
47+
... 'NX-OS'
48+
... ).set_secure_from_file().set_ssl_target_override().set_call_authentication(
49+
... 'admin',
50+
... 'its_a_secret'
51+
... ).construct()
52+
>>> capabilities = client.capabilities()
53+
>>> print(capabilities)
54+
"""
55+
56+
def get(self, *args, **kwargs):
57+
raise NotImplementedError("Get not yet supported on NX-OS!")
58+
59+
def set(self, *args, **kwargs):
60+
raise NotImplementedError("Set not yet supported on NX-OS!")
61+
62+
def subscribe_xpaths(
63+
self,
64+
xpath_subscriptions,
65+
request_mode="STREAM",
66+
sub_mode="SAMPLE",
67+
encoding="PROTO",
68+
sample_interval=Client._NS_IN_S * 10,
69+
):
70+
"""A convenience wrapper of subscribe() which aids in building of SubscriptionRequest
71+
with request as subscribe SubscriptionList. This method accepts an iterable of simply xpath strings,
72+
dictionaries with Subscription attributes for more granularity, or already built Subscription
73+
objects and builds the SubscriptionList. Fields not supplied will be defaulted with the default arguments
74+
to the method.
75+
76+
Generates a single SubscribeRequest.
77+
78+
Parameters
79+
----------
80+
xpath_subscriptions : str or iterable of str, dict, Subscription
81+
An iterable which is parsed to form the Subscriptions in the SubscriptionList to be passed
82+
to SubscriptionRequest. Strings are parsed as XPaths and defaulted with the default arguments,
83+
dictionaries are treated as dicts of args to pass to the Subscribe init, and Subscription is
84+
treated as simply a pre-made Subscription.
85+
request_mode : proto.gnmi_pb2.SubscriptionList.Mode, optional
86+
Indicates whether STREAM to stream from target,
87+
ONCE to stream once (like a get),
88+
POLL to respond to POLL.
89+
[STREAM, ONCE, POLL]
90+
sub_mode : proto.gnmi_pb2.SubscriptionMode, optional
91+
The default SubscriptionMode on a per Subscription basis in the SubscriptionList.
92+
ON_CHANGE only streams updates when changes occur.
93+
SAMPLE will stream the subscription at a regular cadence/interval.
94+
[ON_CHANGE, SAMPLE]
95+
encoding : proto.gnmi_pb2.Encoding, optional
96+
A member of the proto.gnmi_pb2.Encoding enum specifying desired encoding of returned data
97+
[JSON, PROTO]
98+
sample_interval : int, optional
99+
Default nanoseconds for sample to occur.
100+
Defaults to 10 seconds.
101+
102+
Returns
103+
-------
104+
subscribe()
105+
"""
106+
supported_request_modes = ["STREAM", "ONCE", "POLL"]
107+
supported_encodings = ["JSON", "PROTO"]
108+
supported_sub_modes = ["ON_CHANGE", "SAMPLE"]
109+
subscription_list = proto.gnmi_pb2.SubscriptionList()
110+
subscription_list.mode = util.validate_proto_enum(
111+
"mode",
112+
request_mode,
113+
"SubscriptionList.Mode",
114+
proto.gnmi_pb2.SubscriptionList.Mode,
115+
supported_request_modes,
116+
)
117+
subscription_list.encoding = util.validate_proto_enum(
118+
"encoding",
119+
encoding,
120+
"Encoding",
121+
proto.gnmi_pb2.Encoding,
122+
supported_encodings,
123+
)
124+
if isinstance(xpath_subscriptions, string_types):
125+
xpath_subscriptions = [xpath_subscriptions]
126+
for xpath_subscription in xpath_subscriptions:
127+
subscription = None
128+
if isinstance(xpath_subscription, string_types):
129+
subscription = proto.gnmi_pb2.Subscription()
130+
subscription.path.CopyFrom(
131+
self.parse_xpath_to_gnmi_path(xpath_subscription)
132+
)
133+
subscription.mode = util.validate_proto_enum(
134+
"sub_mode",
135+
sub_mode,
136+
"SubscriptionMode",
137+
proto.gnmi_pb2.SubscriptionMode,
138+
supported_sub_modes,
139+
)
140+
subscription.sample_interval = sample_interval
141+
elif isinstance(xpath_subscription, dict):
142+
path = self.parse_xpath_to_gnmi_path(xpath_subscription["path"])
143+
arg_dict = {
144+
"path": path,
145+
"mode": sub_mode,
146+
"sample_interval": sample_interval,
147+
}
148+
arg_dict.update(xpath_subscription)
149+
if "mode" in arg_dict:
150+
arg_dict["mode"] = util.validate_proto_enum(
151+
"sub_mode",
152+
arg_dict["mode"],
153+
"SubscriptionMode",
154+
proto.gnmi_pb2.SubscriptionMode,
155+
supported_sub_modes,
156+
)
157+
subscription = proto.gnmi_pb2.Subscription(**arg_dict)
158+
elif isinstance(xpath_subscription, proto.gnmi_pb2.Subscription):
159+
subscription = xpath_subscription
160+
else:
161+
raise Exception("xpath in list must be xpath or dict/Path!")
162+
subscription_list.subscription.append(subscription)
163+
return self.subscribe([subscription_list])
164+
165+
def parse_xpath_to_gnmi_path(self, xpath, origin=None):
166+
"""Origin defaults to YANG (device) paths
167+
Otherwise specify "DME" as origin
168+
"""
169+
if xpath.startswith("openconfig"):
170+
raise NotImplementedError(
171+
"OpenConfig data models not yet supported on NX-OS!"
172+
)
173+
if origin is None:
174+
if any(map(xpath.startswith, ["Cisco-NX-OS-device", "ietf-interfaces"])):
175+
origin = "device"
176+
else:
177+
origin = "DME"
178+
return super(NXClient, self).parse_xpath_to_gnmi_path(xpath, origin)

src/cisco_gnmi/util.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def gen_target_netloc(target, netloc_prefix="//", default_port=9339):
6060
return target_netloc
6161

6262

63-
def validate_proto_enum(value_name, value, enum_name, enum):
63+
def validate_proto_enum(value_name, value, enum_name, enum, subset=None):
6464
"""Helper function to validate an enum against the proto enum wrapper."""
6565
enum_value = None
6666
if value not in enum.keys() and value not in enum.values():
@@ -76,6 +76,25 @@ def validate_proto_enum(value_name, value, enum_name, enum):
7676
enum_value = enum.Value(value)
7777
else:
7878
enum_value = value
79+
if subset:
80+
resolved_subset = []
81+
for element in subset:
82+
if element in enum.keys():
83+
resolved_subset.append(enum.Value(element))
84+
elif element in enum.values():
85+
resolved_subset.append(element)
86+
else:
87+
raise Exception(
88+
"Subset element {element} not in {enum_name}!".format(
89+
element=element, enum_name=enum_name
90+
)
91+
)
92+
if enum_value not in resolved_subset:
93+
raise Exception(
94+
"{name}={value} not in subset {subset}!".format(
95+
name=value_name, value=enum_value, subset=resolved_subset
96+
)
97+
)
7998
return enum_value
8099

81100

0 commit comments

Comments
 (0)