diff --git a/README.md b/README.md index 0482a13..c0f6dfb 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ cisco-gnmi --help This library covers the gNMI defined `Capabilities`, `Get`, `Set`, and `Subscribe` RPCs, and helper clients provide OS-specific recommendations. A CLI (`cisco-gnmi`) is also available upon installation. As commonalities and differences are identified between OS functionality this library will be refactored as necessary. +Several examples of library usage are available in [`examples/`](examples/). The `cisco-gnmi` CLI script found at [`src/cisco_gnmi/cli.py`](src/cisco_gnmi/cli.py) may also be useful. + It is *highly* recommended that users of the library learn [Google Protocol Buffers](https://developers.google.com/protocol-buffers/) syntax to significantly ease usage. Understanding how to read Protocol Buffers, and reference [`gnmi.proto`](https://github.com/openconfig/gnmi/blob/master/proto/gnmi/gnmi.proto), will be immensely useful for utilizing gNMI and any other gRPC interface. ### cisco-gnmi CLI diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..d30c83a --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1 @@ +gnmi_sub.json diff --git a/examples/custom.py b/examples/custom.py new file mode 100644 index 0000000..988e057 --- /dev/null +++ b/examples/custom.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +"""Copyright 2020 Cisco Systems +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +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. +""" + +"""Custom usage, no wrapper. +Because we're not using a wrapper, we are going to need to build our own protos. +""" + +import json +from getpass import getpass +from cisco_gnmi import ClientBuilder, proto + +"""First let's build a Client. We are not going to specify an OS +name here resulting in just the base Client returned without any OS +convenience methods. Client does have some level of "convenience" built-in +insofar as it doesn't take direct Requests (SubscribeRequest) etc. +To directly use the gNMI RPCs access via client.service.(). +So - either: + * Pass args to the client.() methods. + * Pass full Request protos to client.service.() +This code passes args to the client.() methods. +""" +target = input("Host/Port: ") +username = input("Username: ") +password = getpass() +client = ( + ClientBuilder(target) + .set_secure_from_target() + .set_ssl_target_override() + .set_call_authentication(username, password) + .construct() +) +"""Capabilities is an easy RPC to test.""" +input("Press Enter for Capabilities...") +capabilities = client.capabilities() +print(capabilities) +"""Let's build a Get! +client.get() expects a list of Paths as the primary method of interaction. +client.parse_xpath_to_gnmi_path is a convenience method to..parse an XPath to a Path. +Generally OS wrappers will override this function to specialize on origins, etc. +But we are not using a wrapper, and if using OpenConfig pathing we don't need an origin. +""" +input("Press Enter for Get...") +get_path = client.parse_xpath_to_gnmi_path("/interfaces/interface/state/counters") +get_response = client.get([get_path], data_type="STATE", encoding="JSON_IETF") +print(get_response) +"""Let's build a sampled Subscribe! +client.subscribe() accepts an iterable of SubscriptionLists +""" +input("Press Enter for Subscribe SAMPLE...") +subscription_list = proto.gnmi_pb2.SubscriptionList() +subscription_list.mode = proto.gnmi_pb2.SubscriptionList.Mode.Value("STREAM") +subscription_list.encoding = proto.gnmi_pb2.Encoding.Value("PROTO") +sampled_subscription = proto.gnmi_pb2.Subscription() +sampled_subscription.path.CopyFrom( + client.parse_xpath_to_gnmi_path("/interfaces/interface/state/counters") +) +sampled_subscription.mode = proto.gnmi_pb2.SubscriptionMode.Value("SAMPLE") +sampled_subscription.sample_interval = 10 * int(1e9) +subscription_list.subscription.extend([sampled_subscription]) +for subscribe_response in client.subscribe([subscription_list]): + print(subscribe_response) + break +"""Now let's do ON_CHANGE. Just have to put SubscriptionMode to ON_CHANGE.""" +input("Press Enter for Subscribe ON_CHANGE...") +subscription_list = proto.gnmi_pb2.SubscriptionList() +subscription_list.mode = proto.gnmi_pb2.SubscriptionList.Mode.Value("STREAM") +subscription_list.encoding = proto.gnmi_pb2.Encoding.Value("PROTO") +onchange_subscription = proto.gnmi_pb2.Subscription() +onchange_subscription.path.CopyFrom( + client.parse_xpath_to_gnmi_path( + "/syslog/messages/message", origin="Cisco-IOS-XR-infra-syslog-oper" + ) +) +onchange_subscription.mode = proto.gnmi_pb2.SubscriptionMode.Value("ON_CHANGE") +subscription_list.subscription.extend([onchange_subscription]) +synced = False +for subscribe_response in client.subscribe([subscription_list]): + if subscribe_response.sync_response: + synced = True + print("Synced. Now perform action that will create a changed value.") + print("If using XR syslog as written, just try SSH'ing to device.") + continue + if not synced: + continue + print(subscribe_response) + break +"""Let's build a Set! +client.set() expects updates, replaces, and/or deletes to be provided. +updates is a list of Updates +replaces is a list of Updates +deletes is a list of Paths +Let's do an update. +""" +input("Press Enter for Set update...") +set_update = proto.gnmi_pb2.Update() +# This is the fully modeled JSON we want to update with +update_json = json.loads( + """ +{ + "openconfig-interfaces:interfaces": { + "interface": [ + { + "name": "Loopback9339" + } + ] + } +} +""" +) +# Let's just do an update from the very top element +top_element = next(iter(update_json.keys())) +set_update.path.CopyFrom(client.parse_xpath_to_gnmi_path(top_element)) +# Remove the top element from the config since it's now in Path +update_json = update_json.pop(top_element) +# Set our update payload +set_update.val.json_ietf_val = json.dumps(update_json).encode("utf-8") +set_result = client.set(updates=[set_update]) +print(set_result) +# This may all seem somewhat obtuse, and that's what the client wrappers are for. diff --git a/examples/load_subscribe_dump.py b/examples/load_subscribe_dump.py new file mode 100644 index 0000000..5c4cdfb --- /dev/null +++ b/examples/load_subscribe_dump.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +"""Copyright 2020 Cisco Systems +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +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. +""" + +"""This is effectively just demo code to load the output of subscribe_dump.py +""" +import argparse +import os +import logging +import json +import cisco_gnmi +from google.protobuf import json_format, text_format + + +def main(): + logging.basicConfig(level=logging.INFO) + logging.info("Demo of loading protobufs from files.") + args = setup_args() + src_proto_array = load_proto_file(args.protos_file) + parsed_proto_array = [] + for proto_msg in src_proto_array: + parsed_proto = None + if args.text_format is True: + parsed_proto = text_format.Parse( + proto_msg, cisco_gnmi.proto.gnmi_pb2.SubscribeResponse() + ) + else: + if args.raw_json: + parsed_proto = json_format.Parse( + proto_msg, cisco_gnmi.proto.gnmi_pb2.SubscribeResponse() + ) + else: + parsed_proto = json_format.ParseDict( + proto_msg, cisco_gnmi.proto.gnmi_pb2.SubscribeResponse() + ) + parsed_proto_array.append(parsed_proto) + logging.info("Parsed %i formatted messages into objects!", len(parsed_proto_array)) + + +def load_proto_file(filename): + if not filename.endswith(".json"): + raise Exception("Expected JSON file (array of messages) from proto_dump.py") + proto_array = None + with open(filename, "r") as protos_fd: + proto_array = json.load(protos_fd) + if not isinstance(proto_array, (list)): + raise Exception("Expected array of messages from file!") + return proto_array + + +def setup_args(): + parser = argparse.ArgumentParser(description="Proto Load Example") + parser.add_argument("protos_file", help="File containing protos.", type=str) + parser.add_argument( + "-text_format", + help="Protos are in text format instead of JSON.", + action="store_true", + ) + parser.add_argument( + "-raw_json", + help="Do not serialize to dict, but directly to JSON.", + action="store_true", + ) + return parser.parse_args() + + +if __name__ == "__main__": + main() diff --git a/examples/requirements.txt b/examples/requirements.txt new file mode 100644 index 0000000..34dc4c7 --- /dev/null +++ b/examples/requirements.txt @@ -0,0 +1 @@ +cisco_gnmi \ No newline at end of file diff --git a/examples/subscribe_dump.py b/examples/subscribe_dump.py new file mode 100644 index 0000000..2eaaa2e --- /dev/null +++ b/examples/subscribe_dump.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +"""Copyright 2020 Cisco Systems +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +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. +""" + +"""This demoes a gNMI subscription and dumping messages to a file.""" +import json +import logging +import argparse +from getpass import getpass +from google.protobuf import json_format, text_format +from cisco_gnmi import ClientBuilder + + +def main(): + logging.basicConfig(level=logging.INFO) + args = setup_args() + username = input("Username: ") + password = getpass() + logging.info("Connecting to %s as %s ...", args.netloc, args.os) + client = ( + ClientBuilder(args.netloc) + .set_os(args.os) + .set_secure_from_target() + .set_ssl_target_override() + .set_call_authentication(username, password) + .construct() + ) + formatted_messages = [] + try: + logging.info("Subscribing to %s ...", args.xpath) + sub_args = {"xpath_subscriptions": args.xpath} + if args.encoding: + sub_args["encoding"] = args.encoding + for message in client.subscribe_xpaths(**sub_args): + if message.sync_response and not args.no_stop: + logging.warning("Stopping on sync_response.") + break + formatted_message = None + if args.text_format is True: + formatted_message = text_format.MessageToString(message) + else: + if args.raw_json: + formatted_message = json_format.MessageToJson(message) + else: + formatted_message = json_format.MessageToDict(message) + logging.info(formatted_message) + formatted_messages.append(formatted_message) + except KeyboardInterrupt: + logging.warning("Stopping on interrupt.") + except Exception: + logging.exception("Stopping due to exception!") + finally: + logging.info("Writing to %s ...", args.protos_file) + with open(args.protos_file, "w") as protos_fd: + json.dump( + formatted_messages, + protos_fd, + sort_keys=True, + indent=4, + separators=(",", ": "), + ) + + +def setup_args(): + parser = argparse.ArgumentParser(description="gNMI Proto Dump Example") + parser.add_argument("netloc", help=":", type=str) + parser.add_argument( + "-os", + help="OS to use.", + type=str, + default="IOS XR", + choices=list(ClientBuilder.os_class_map.keys()), + ) + parser.add_argument( + "-xpath", + help="XPath to subscribe to.", + type=str, + default="/interfaces/interface/state/counters", + ) + parser.add_argument( + "-protos_file", help="File to write protos.", type=str, default="gnmi_sub.json" + ) + parser.add_argument( + "-no_stop", help="Do not stop on sync_response.", action="store_true" + ) + parser.add_argument( + "-encoding", help="gNMI subscription encoding.", type=str, nargs="?" + ) + parser.add_argument( + "-text_format", + help="Protos are in text format instead of JSON.", + action="store_true", + ) + parser.add_argument( + "-raw_json", + help="Do not serialize to dict, but directly to JSON.", + action="store_true", + ) + return parser.parse_args() + + +if __name__ == "__main__": + main() diff --git a/examples/subscribe_onchange.py b/examples/subscribe_onchange.py new file mode 100644 index 0000000..4bcd0c9 --- /dev/null +++ b/examples/subscribe_onchange.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +"""Copyright 2020 Cisco Systems +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +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. +""" + +"""This demoes a gNMI subscription and dumping messages to a file. +Targets IOS XR syslog as demo. +TODO: Refactor library so ON_CHANGE is functionally simpler. +""" +import json +import logging +import argparse +from getpass import getpass +from google.protobuf import json_format, text_format +from cisco_gnmi import ClientBuilder, proto + + +def main(): + logging.basicConfig(level=logging.INFO) + args = setup_args() + username = input("Username: ") + password = getpass() + logging.info("Connecting to %s as %s ...", args.netloc, args.os) + client = ( + ClientBuilder(args.netloc) + .set_os(args.os) + .set_secure_from_target() + .set_ssl_target_override() + .set_call_authentication(username, password) + .construct() + ) + formatted_messages = [] + try: + logging.info("Subscribing to %s ...", args.xpath) + sub_args = {"xpath_subscriptions": args.xpath, "sub_mode": "ON_CHANGE"} + if args.encoding: + sub_args["encoding"] = args.encoding + if not args.process_all: + logging.info("Ignoring messages before sync_response.") + synced = False + for message in client.subscribe_xpaths(**sub_args): + if message.sync_response: + synced = True + logging.info("Synced with latest state.") + continue + if not synced and not args.process_all: + continue + formatted_message = None + if args.text_format is True: + formatted_message = text_format.MessageToString(message) + else: + if args.raw_json: + formatted_message = json_format.MessageToJson(message) + else: + formatted_message = json_format.MessageToDict(message) + logging.info(formatted_message) + formatted_messages.append(formatted_message) + except KeyboardInterrupt: + logging.warning("Stopping on interrupt.") + except Exception: + logging.exception("Stopping due to exception!") + finally: + logging.info("Writing to %s ...", args.protos_file) + with open(args.protos_file, "w") as protos_fd: + json.dump( + formatted_messages, + protos_fd, + sort_keys=True, + indent=4, + separators=(",", ": "), + ) + + +def setup_args(): + parser = argparse.ArgumentParser(description="gNMI Subscribe Dump Example") + parser.add_argument("netloc", help=":", type=str) + parser.add_argument( + "-os", + help="OS to use.", + type=str, + default="IOS XR", + choices=list(ClientBuilder.os_class_map.keys()), + ) + parser.add_argument( + "-xpath", + help="XPath to subscribe to.", + type=str, + default="Cisco-IOS-XR-infra-syslog-oper:syslog/messages/message", + ) + parser.add_argument( + "-protos_file", help="File to write protos.", type=str, default="gnmi_sub.json" + ) + parser.add_argument( + "-process_all", + help="Process all the way through sync_response.", + action="store_true", + ) + parser.add_argument( + "-encoding", help="gNMI subscription encoding.", type=str, default="PROTO" + ) + parser.add_argument( + "-text_format", + help="Protos are in text format instead of JSON.", + action="store_true", + ) + parser.add_argument( + "-raw_json", + help="Do not serialize to dict, but directly to JSON.", + action="store_true", + ) + return parser.parse_args() + + +if __name__ == "__main__": + main() diff --git a/src/cisco_gnmi/__init__.py b/src/cisco_gnmi/__init__.py index edd58b4..60f494f 100644 --- a/src/cisco_gnmi/__init__.py +++ b/src/cisco_gnmi/__init__.py @@ -30,4 +30,4 @@ from .xe import XEClient from .builder import ClientBuilder -__version__ = "1.0.6" +__version__ = "1.0.7" diff --git a/src/cisco_gnmi/builder.py b/src/cisco_gnmi/builder.py index db1d0d6..a74a219 100644 --- a/src/cisco_gnmi/builder.py +++ b/src/cisco_gnmi/builder.py @@ -76,9 +76,13 @@ class ClientBuilder(object): os_class_map = { None: Client, + "None": Client, "IOS XR": XRClient, + "XR": XRClient, "NX-OS": NXClient, + "NX": NXClient, "IOS XE": XEClient, + "XE": XEClient, } def __init__(self, target): diff --git a/src/cisco_gnmi/cli.py b/src/cisco_gnmi/cli.py index 623d380..ac2bf46 100644 --- a/src/cisco_gnmi/cli.py +++ b/src/cisco_gnmi/cli.py @@ -64,8 +64,7 @@ def main(): See --help for RPC options. """.format( - version=__version__, - supported_rpcs="\n".join(sorted(list(rpc_map.keys()))) + version=__version__, supported_rpcs="\n".join(sorted(list(rpc_map.keys()))) ), ) parser.add_argument("rpc", help="gNMI RPC to perform against network element.")