Skip to content

Commit bd877d4

Browse files
committed
Refactor subscribe_xpaths into Client
1 parent fb94dd2 commit bd877d4

File tree

7 files changed

+336
-206
lines changed

7 files changed

+336
-206
lines changed

README.md

Lines changed: 52 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -257,12 +257,12 @@ cisco-gnmi capabilities 127.0.0.1:57500 -auto_ssl_target_override
257257
```
258258
cisco-gnmi capabilities --help
259259
usage: cisco-gnmi [-h] [-os {None,IOS XR,NX-OS,IOS XE}]
260-
[-root_certificates ROOT_CERTIFICATES]
261-
[-private_key PRIVATE_KEY]
262-
[-certificate_chain CERTIFICATE_CHAIN]
263-
[-ssl_target_override SSL_TARGET_OVERRIDE]
264-
[-auto_ssl_target_override] [-debug]
265-
netloc
260+
[-root_certificates ROOT_CERTIFICATES]
261+
[-private_key PRIVATE_KEY]
262+
[-certificate_chain CERTIFICATE_CHAIN]
263+
[-ssl_target_override SSL_TARGET_OVERRIDE]
264+
[-auto_ssl_target_override] [-debug]
265+
netloc
266266
267267
Performs Capabilities RPC against network element.
268268
@@ -309,16 +309,17 @@ cisco-gnmi get 127.0.0.1:57500 -os "IOS XR" -xpath /interfaces/interface/state/c
309309

310310
#### Usage
311311
```
312+
cisco-gnmi get --help
312313
usage: cisco-gnmi [-h] [-xpath XPATH]
313-
[-encoding [{JSON,BYTES,PROTO,ASCII,JSON_IETF}]]
314-
[-data_type [{ALL,CONFIG,STATE,OPERATIONAL}]] [-dump_json]
315-
[-os {None,IOS XR,NX-OS,IOS XE}]
316-
[-root_certificates ROOT_CERTIFICATES]
317-
[-private_key PRIVATE_KEY]
318-
[-certificate_chain CERTIFICATE_CHAIN]
319-
[-ssl_target_override SSL_TARGET_OVERRIDE]
320-
[-auto_ssl_target_override] [-debug]
321-
netloc
314+
[-encoding {JSON,BYTES,PROTO,ASCII,JSON_IETF}]
315+
[-data_type {ALL,CONFIG,STATE,OPERATIONAL}] [-dump_json]
316+
[-os {None,IOS XR,NX-OS,IOS XE}]
317+
[-root_certificates ROOT_CERTIFICATES]
318+
[-private_key PRIVATE_KEY]
319+
[-certificate_chain CERTIFICATE_CHAIN]
320+
[-ssl_target_override SSL_TARGET_OVERRIDE]
321+
[-auto_ssl_target_override] [-debug]
322+
netloc
322323
323324
Performs Get RPC against network element.
324325
@@ -328,9 +329,9 @@ positional arguments:
328329
optional arguments:
329330
-h, --help show this help message and exit
330331
-xpath XPATH XPaths to Get.
331-
-encoding [{JSON,BYTES,PROTO,ASCII,JSON_IETF}]
332+
-encoding {JSON,BYTES,PROTO,ASCII,JSON_IETF}
332333
gNMI Encoding.
333-
-data_type [{ALL,CONFIG,STATE,OPERATIONAL}]
334+
-data_type {ALL,CONFIG,STATE,OPERATIONAL}
334335
gNMI GetRequest DataType
335336
-dump_json Dump as JSON instead of textual protos.
336337
-os {None,IOS XR,NX-OS,IOS XE}
@@ -381,16 +382,17 @@ Please note that `Set` operations may be destructive to operations and should be
381382

382383
#### Usage
383384
```
385+
cisco-gnmi set --help
384386
usage: cisco-gnmi [-h] [-update_json_config UPDATE_JSON_CONFIG]
385-
[-replace_json_config REPLACE_JSON_CONFIG]
386-
[-delete_xpath DELETE_XPATH] [-no_ietf] [-dump_json]
387-
[-os {None,IOS XR,NX-OS,IOS XE}]
388-
[-root_certificates ROOT_CERTIFICATES]
389-
[-private_key PRIVATE_KEY]
390-
[-certificate_chain CERTIFICATE_CHAIN]
391-
[-ssl_target_override SSL_TARGET_OVERRIDE]
392-
[-auto_ssl_target_override] [-debug]
393-
netloc
387+
[-replace_json_config REPLACE_JSON_CONFIG]
388+
[-delete_xpath DELETE_XPATH] [-no_ietf] [-dump_json]
389+
[-os {None,IOS XR,NX-OS,IOS XE}]
390+
[-root_certificates ROOT_CERTIFICATES]
391+
[-private_key PRIVATE_KEY]
392+
[-certificate_chain CERTIFICATE_CHAIN]
393+
[-ssl_target_override SSL_TARGET_OVERRIDE]
394+
[-auto_ssl_target_override] [-debug]
395+
netloc
394396
395397
Performs Set RPC against network element.
396398
@@ -469,24 +471,28 @@ interface Loopback9339
469471
```
470472

471473
### Subscribe
472-
This command will output the `SubscribeResponse` to `stdout` or `-dump_file`. `-xpath` may be specified multiple times to specify multiple `Path`s for the `GetRequest`. Subscribe currently only supports a sampled stream. `ON_CHANGE` is possible but not implemented in the CLI, yet. :)
474+
This command will output the `SubscribeResponse` to `stdout` or `-dump_file`. `-xpath` may be specified multiple times to specify multiple `Path`s for the `GetRequest`.
475+
473476
```
474477
cisco-gnmi subscribe 127.0.0.1:57500 -os "IOS XR" -xpath /interfaces/interface/state/counters -auto_ssl_target_override
475478
```
476479

477480
#### Usage
478481
```
479482
cisco-gnmi subscribe --help
480-
usage: cisco-gnmi [-h] [-xpath XPATH] [-interval INTERVAL] [-dump_file DUMP_FILE]
481-
[-dump_json] [-sync_stop]
482-
[-encoding [{JSON,BYTES,PROTO,ASCII,JSON_IETF}]]
483-
[-os {None,IOS XR,NX-OS,IOS XE}]
484-
[-root_certificates ROOT_CERTIFICATES]
485-
[-private_key PRIVATE_KEY]
486-
[-certificate_chain CERTIFICATE_CHAIN]
487-
[-ssl_target_override SSL_TARGET_OVERRIDE]
488-
[-auto_ssl_target_override] [-debug]
489-
netloc
483+
usage: cisco-gnmi [-h] [-xpath XPATH] [-interval INTERVAL]
484+
[-mode {TARGET_DEFINED,ON_CHANGE,SAMPLE}]
485+
[-suppress_redundant]
486+
[-heartbeat_interval HEARTBEAT_INTERVAL]
487+
[-dump_file DUMP_FILE] [-dump_json] [-sync_stop]
488+
[-sync_start] [-encoding {JSON,BYTES,PROTO,ASCII,JSON_IETF}]
489+
[-os {None,IOS XR,NX-OS,IOS XE}]
490+
[-root_certificates ROOT_CERTIFICATES]
491+
[-private_key PRIVATE_KEY]
492+
[-certificate_chain CERTIFICATE_CHAIN]
493+
[-ssl_target_override SSL_TARGET_OVERRIDE]
494+
[-auto_ssl_target_override] [-debug]
495+
netloc
490496
491497
Performs Subscribe RPC against network element.
492498
@@ -498,11 +504,18 @@ optional arguments:
498504
-xpath XPATH XPath to subscribe to.
499505
-interval INTERVAL Sample interval in seconds for Subscription. Defaults
500506
to 10.
507+
-mode {TARGET_DEFINED,ON_CHANGE,SAMPLE}
508+
SubscriptionMode for Subscription. Defaults to SAMPLE.
509+
-suppress_redundant Suppress redundant information in Subscription.
510+
-heartbeat_interval HEARTBEAT_INTERVAL
511+
Heartbeat interval in seconds.
501512
-dump_file DUMP_FILE Filename to dump to. Defaults to stdout.
502513
-dump_json Dump as JSON instead of textual protos.
503514
-sync_stop Stop on sync_response.
504-
-encoding [{JSON,BYTES,PROTO,ASCII,JSON_IETF}]
505-
gNMI Encoding.
515+
-sync_start Start processing messages after sync_response.
516+
-encoding {JSON,BYTES,PROTO,ASCII,JSON_IETF}
517+
gNMI Encoding. Defaults to whatever Client wrapper
518+
prefers.
506519
-os {None,IOS XR,NX-OS,IOS XE}
507520
OS wrapper to utilize. Defaults to IOS XR.
508521
-root_certificates ROOT_CERTIFICATES

src/cisco_gnmi/cli.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,7 @@ def gnmi_capabilities():
9191

9292

9393
def gnmi_subscribe():
94-
"""Performs a sampled Subscribe against network element.
95-
TODO: ON_CHANGE
94+
"""Performs a streaming Subscribe against network element.
9695
"""
9796
parser = argparse.ArgumentParser(
9897
description="Performs Subscribe RPC against network element."
@@ -106,6 +105,20 @@ def gnmi_subscribe():
106105
type=int,
107106
default=10,
108107
)
108+
parser.add_argument(
109+
"-mode",
110+
help="SubscriptionMode for Subscription. Defaults to SAMPLE.",
111+
default="SAMPLE",
112+
choices=proto.gnmi_pb2.SubscriptionMode.keys(),
113+
)
114+
parser.add_argument(
115+
"-suppress_redundant",
116+
help="Suppress redundant information in Subscription.",
117+
action="store_true",
118+
)
119+
parser.add_argument(
120+
"-heartbeat_interval", help="Heartbeat interval in seconds.", type=int
121+
)
109122
parser.add_argument(
110123
"-dump_file",
111124
help="Filename to dump to. Defaults to stdout.",
@@ -120,11 +133,15 @@ def gnmi_subscribe():
120133
parser.add_argument(
121134
"-sync_stop", help="Stop on sync_response.", action="store_true"
122135
)
136+
parser.add_argument(
137+
"-sync_start",
138+
help="Start processing messages after sync_response.",
139+
action="store_true",
140+
)
123141
parser.add_argument(
124142
"-encoding",
125-
help="gNMI Encoding.",
143+
help="gNMI Encoding. Defaults to whatever Client wrapper prefers.",
126144
type=str,
127-
nargs="?",
128145
choices=proto.gnmi_pb2.Encoding.keys(),
129146
)
130147
args = __common_args_handler(parser)
@@ -138,20 +155,30 @@ def gnmi_subscribe():
138155
kwargs["encoding"] = args.encoding
139156
if args.interval:
140157
kwargs["sample_interval"] = args.interval * int(1e9)
158+
if args.mode:
159+
kwargs["sub_mode"] = args.mode
160+
if args.suppress_redundant:
161+
kwargs["suppress_redundant"] = args.suppress_redundant
162+
if args.heartbeat_interval:
163+
kwargs["heartbeat_interval"] = args.heartbeat_interval * int(1e9)
141164
try:
142-
logging.info(
165+
logging.debug(
143166
"Dumping responses to %s as %s ...",
144167
args.dump_file,
145168
"JSON" if args.dump_json else "textual proto",
146169
)
147-
logging.info("Subscribing to:\n%s", "\n".join(args.xpath))
170+
logging.debug("Subscribing to:\n%s", "\n".join(args.xpath))
171+
synced = False
148172
for subscribe_response in client.subscribe_xpaths(args.xpath, **kwargs):
149173
logging.debug("SubscribeResponse received.")
150174
if subscribe_response.sync_response:
151175
logging.debug("sync_response received.")
152176
if args.sync_stop:
153177
logging.warning("Stopping on sync_response.")
154178
break
179+
synced = True
180+
if not synced and args.sync_start:
181+
continue
155182
formatted_message = __format_message(subscribe_response)
156183
if args.dump_file == "stdout":
157184
logging.info(formatted_message)
@@ -175,14 +202,12 @@ def gnmi_get():
175202
"-encoding",
176203
help="gNMI Encoding.",
177204
type=str,
178-
nargs="?",
179205
choices=proto.gnmi_pb2.Encoding.keys(),
180206
)
181207
parser.add_argument(
182208
"-data_type",
183209
help="gNMI GetRequest DataType",
184210
type=str,
185-
nargs="?",
186211
choices=enum_type_wrapper.EnumTypeWrapper(
187212
proto.gnmi_pb2._GETREQUEST_DATATYPE
188213
).keys(),

src/cisco_gnmi/client.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,140 @@ def validate_request(request):
251251
)
252252
return response_stream
253253

254+
def subscribe_xpaths(
255+
self,
256+
xpath_subscriptions,
257+
request_mode="STREAM",
258+
sub_mode="SAMPLE",
259+
encoding="JSON",
260+
sample_interval=_NS_IN_S * 10,
261+
suppress_redundant=False,
262+
heartbeat_interval=None,
263+
):
264+
"""A convenience wrapper of subscribe() which aids in building of SubscriptionRequest
265+
with request as subscribe SubscriptionList. This method accepts an iterable of simply xpath strings,
266+
dictionaries with Subscription attributes for more granularity, or already built Subscription
267+
objects and builds the SubscriptionList. Fields not supplied will be defaulted with the default arguments
268+
to the method.
269+
270+
Generates a single SubscribeRequest.
271+
272+
Parameters
273+
----------
274+
xpath_subscriptions : str or iterable of str, dict, Subscription
275+
An iterable which is parsed to form the Subscriptions in the SubscriptionList to be passed
276+
to SubscriptionRequest. Strings are parsed as XPaths and defaulted with the default arguments,
277+
dictionaries are treated as dicts of args to pass to the Subscribe init, and Subscription is
278+
treated as simply a pre-made Subscription.
279+
request_mode : proto.gnmi_pb2.SubscriptionList.Mode, optional
280+
Indicates whether STREAM to stream from target,
281+
ONCE to stream once (like a get),
282+
POLL to respond to POLL.
283+
[STREAM, ONCE, POLL]
284+
sub_mode : proto.gnmi_pb2.SubscriptionMode, optional
285+
The default SubscriptionMode on a per Subscription basis in the SubscriptionList.
286+
TARGET_DEFINED indicates that the target (like device/destination) should stream
287+
information however it knows best. This instructs the target to decide between ON_CHANGE
288+
or SAMPLE - e.g. the device gNMI server may understand that we only need RIB updates
289+
as an ON_CHANGE basis as opposed to SAMPLE, and we don't have to explicitly state our
290+
desired behavior.
291+
ON_CHANGE only streams updates when changes occur.
292+
SAMPLE will stream the subscription at a regular cadence/interval.
293+
[TARGET_DEFINED, ON_CHANGE, SAMPLE]
294+
encoding : proto.gnmi_pb2.Encoding, optional
295+
A member of the proto.gnmi_pb2.Encoding enum specifying desired encoding of returned data
296+
[JSON, BYTES, PROTO, ASCII, JSON_IETF]
297+
sample_interval : int, optional
298+
Default nanoseconds for SAMPLE to occur.
299+
Defaults to 10 seconds.
300+
suppress_redundant : bool, optional
301+
Indicates whether values that have not changed should be sent in a SAMPLE subscription.
302+
heartbeat_interval : int, optional
303+
Specifies the maximum allowable silent period in nanoseconds when
304+
suppress_redundant is in use. The target should send a value at least once
305+
in the period specified. Also applies in ON_CHANGE.
306+
307+
Returns
308+
-------
309+
subscribe()
310+
"""
311+
subscription_list = proto.gnmi_pb2.SubscriptionList()
312+
subscription_list.mode = util.validate_proto_enum(
313+
"mode",
314+
request_mode,
315+
"SubscriptionList.Mode",
316+
proto.gnmi_pb2.SubscriptionList.Mode,
317+
)
318+
subscription_list.encoding = util.validate_proto_enum(
319+
"encoding", encoding, "Encoding", proto.gnmi_pb2.Encoding
320+
)
321+
if isinstance(
322+
xpath_subscriptions, (string_types, dict, proto.gnmi_pb2.Subscription)
323+
):
324+
xpath_subscriptions = [xpath_subscriptions]
325+
subscriptions = []
326+
for xpath_subscription in xpath_subscriptions:
327+
subscription = None
328+
if isinstance(xpath_subscription, proto.gnmi_pb2.Subscription):
329+
subscription = xpath_subscription
330+
elif isinstance(xpath_subscription, string_types):
331+
subscription = proto.gnmi_pb2.Subscription()
332+
subscription.path.CopyFrom(
333+
self.parse_xpath_to_gnmi_path(xpath_subscription)
334+
)
335+
subscription.mode = util.validate_proto_enum(
336+
"sub_mode",
337+
sub_mode,
338+
"SubscriptionMode",
339+
proto.gnmi_pb2.SubscriptionMode,
340+
)
341+
if sub_mode == "SAMPLE":
342+
subscription.sample_interval = sample_interval
343+
elif isinstance(xpath_subscription, dict):
344+
subscription_dict = {}
345+
if "path" not in xpath_subscription.keys():
346+
raise Exception("path must be specified in dict!")
347+
if isinstance(xpath_subscription["path"], proto.gnmi_pb2.Path):
348+
subscription_dict["path"] = xpath_subscription["path"]
349+
elif isinstance(xpath_subscription["path"], string_types):
350+
subscription_dict["path"] = self.parse_xpath_to_gnmi_path(
351+
xpath_subscription["path"]
352+
)
353+
else:
354+
raise Exception("path must be string or Path proto!")
355+
sub_mode_name = (
356+
sub_mode
357+
if "mode" not in xpath_subscription.keys()
358+
else xpath_subscription["mode"]
359+
)
360+
subscription_dict["mode"] = util.validate_proto_enum(
361+
"sub_mode",
362+
sub_mode,
363+
"SubscriptionMode",
364+
proto.gnmi_pb2.SubscriptionMode,
365+
)
366+
if sub_mode_name == "SAMPLE":
367+
subscription_dict["sample_interval"] = (
368+
sample_interval
369+
if "sample_interval" not in xpath_subscription.keys()
370+
else xpath_subscription["sample_interval"]
371+
)
372+
if "suppress_redundant" in xpath_subscription.keys():
373+
subscription_dict["suppress_redundant"] = xpath_subscription[
374+
"suppress_redundant"
375+
]
376+
if sub_mode_name != "TARGET_DEFINED":
377+
if "heartbeat_interval" in xpath_subscription.keys():
378+
subscription_dict["heartbeat_interval"] = xpath_subscription[
379+
"heartbeat_interval"
380+
]
381+
subscription = proto.gnmi_pb2.Subscription(**subscription_dict)
382+
else:
383+
raise Exception("path must be string, dict, or Subscription proto!")
384+
subscriptions.append(subscription)
385+
subscription_list.subscription.extend(subscriptions)
386+
return self.subscribe([subscription_list])
387+
254388
def parse_xpath_to_gnmi_path(self, xpath, origin=None):
255389
"""Parses an XPath to proto.gnmi_pb2.Path.
256390
This function should be overridden by any child classes for origin logic.

0 commit comments

Comments
 (0)