diff --git a/gnmi_cli_py/README.md b/gnmi_cli_py/README.md index 898ead4c..fe5d4db9 100644 --- a/gnmi_cli_py/README.md +++ b/gnmi_cli_py/README.md @@ -40,6 +40,10 @@ gNMI SetRequest Replace for an Access Point target, who's hostname is "ap-1", wi ``` python py_gnmicli.py -t example.net -p 443 -m set-replace -x /access-points/access-point[hostname=test-ap1]/radios/radio[id=0]/config/channel -user admin -pass admin -rcert ca.cert.pem -val 165 ``` +gNMI Subscribe request with Subscribe Mode STREAM and Submode SAMPLE with sample interval 5 seconds. +``` +python py_gnmicli.py -t example.net -p 443 -m subscribe -x /access-points/access-point[hostname=test-ap1]/radios/radio[id=0]/config/channel -user admin -pass admin -g -o openconfig.com --interval 5 --subscribe_mode 0 --submode 2 +``` The above SetRequest Replace would output the following to stdout: ``` Performing SetRequest Replace, encoding=JSON_IETF to openconfig.example.com with the following gNMI Path diff --git a/gnmi_cli_py/py_gnmicli.py b/gnmi_cli_py/py_gnmicli.py index 7c9e92b6..f0d9c491 100644 --- a/gnmi_cli_py/py_gnmicli.py +++ b/gnmi_cli_py/py_gnmicli.py @@ -24,9 +24,7 @@ - Auto-loads Target cert from Target if not specified - User/password based authentication - Certifificate based authentication - -Current unsupported gNMI features: -- Subscribe +- Subscribe request """ from __future__ import absolute_import @@ -46,13 +44,14 @@ print('ERROR: Ensure you\'ve installed dependencies from requirements.txt\n' 'eg, pip install -r requirements.txt') import gnmi_pb2_grpc +import grpc -__version__ = '0.4' +__version__ = '0.5' _RE_PATH_COMPONENT = re.compile(r''' ^ (?P[^[]+) # gNMI path name -(\[(?P\w+) # gNMI path key +(\[(?P[a-zA-Z0-9\-]+) # gNMI path key = (?P.*) # gNMI path value \])?$ @@ -140,6 +139,20 @@ def _create_parser(): required=False, action='store_true') parser.add_argument('-n', '--notls', help='gRPC insecure mode', required=False, action='store_true') + parser.add_argument('--interval', default=10, type=int, + help='sample interval (default: 10s)') + parser.add_argument('--timeout', type=int, help='subscription' + 'duration in seconds (default: none)') + parser.add_argument('--heartbeat', default=0, type=int, help='heartbeat interval (default: None)') + parser.add_argument('--aggregate', action='store_true', help='allow aggregation') + parser.add_argument('--suppress', action='store_true', help='suppress redundant') + parser.add_argument('--submode', default=2, type=int, + help='subscription mode [0=TARGET_DEFINED, 1=ON_CHANGE, 2=SAMPLE]') + parser.add_argument('--subscribe_mode', default=0, type=int, help='[0=STREAM, 1=ONCE, 2=POLL]') + parser.add_argument('--encoding', default=0, type=int, help='[0=JSON, 1=BYTES, 2=PROTO, 3=ASCII, 4=JSON_IETF]') + parser.add_argument('--qos', default=0, type=int, help='') + parser.add_argument('--use_alias', action='store_true', help='use alias') + parser.add_argument('--prefix', default='', help='gRPC path prefix (default: none)') return parser @@ -154,9 +167,31 @@ def _path_names(xpath): Returns: list of gNMI path names. """ - if not xpath or xpath == '/': # A blank xpath was provided at CLI. - return [] - return xpath.strip().strip('/').split('/') # Remove leading and trailing '/'. + path = [] + insidebracket = False + begin = 0 + end = 0 + xpath=xpath+'/' + while end < len(xpath): + if xpath[end] == "/": + if insidebracket == False: + if end > begin: + path.append(xpath[begin:end]) + end = end + 1 + begin = end + else: + end = end + 1 + elif xpath[end] == "[": + if (end==0 or xpath[end-1]!='\\') and insidebracket == False: + insidebracket = True + end = end + 1 + elif xpath[end] == "]": + if (end==0 or xpath[end-1]!='\\') and insidebracket == True: + insidebracket = False + end = end + 1 + else: + end = end + 1 + return path def _parse_path(p_names): @@ -248,7 +283,7 @@ def _get_val(json_value): json_value.strip('@'), 'rb').read()) except (IOError, ValueError) as e: raise JsonReadError('Error while loading JSON: %s' % str(e)) - val.json_ietf_val = json.dumps(set_json).encode() + val.json_ietf_val = json.dumps(set_json).encode('utf-8') return val coerced_val = _format_type(json_value) type_to_value = {bool: 'bool_val', int: 'int_val', float: 'float_val', @@ -349,6 +384,68 @@ def _open_certs(**kwargs): return kwargs +def gen_request(paths, opt): + """Create subscribe request for passed xpath. + Args: + paths: (str) gNMI path. + opt: (dict) Command line argument passed for subscribe reqeust. + Returns: + gNMI SubscribeRequest object. + """ + mysubs = [] + mysub = gnmi_pb2.Subscription(path=paths, mode=opt["submode"], + sample_interval=opt["interval"]*1000000000, + heartbeat_interval=opt['heartbeat']*1000000000, + suppress_redundant=opt['suppress']) + mysubs.append(mysub) + if opt["prefix"]: + myprefix = _parse_path(_path_names(opt["prefix"])) + else: + myprefix = None + + if opt["qos"]: + myqos = gnmi_pb2.QOSMarking(marking=opt["qos"]) + else: + myqos = None + mysblist = gnmi_pb2.SubscriptionList(prefix=myprefix, mode=opt['subscribe_mode'], + allow_aggregation=opt['aggregate'], encoding=opt['encoding'], + subscription=mysubs, use_aliases=opt['use_alias'], qos=myqos) + mysubreq = gnmi_pb2.SubscribeRequest(subscribe=mysblist) + + print('Sending SubscribeRequest\n'+str(mysubreq)) + yield mysubreq + + +def subscribe_start(stub, options, req_iterator): + """ RPC Start for Subscribe reqeust + Args: + stub: (class) gNMI Stub used to build the secure channel. + options: (dict) Command line argument passed for subscribe reqeust. + req_iterator: gNMI Subscribe Request from gen_request. + Returns: + Start Subscribe and printing response of gNMI Subscribe Response. + """ + metadata = [('username', options['username']), ('password', options['password'])] + try: + responses = stub.Subscribe(req_iterator, options['timeout'], metadata=metadata) + for response in responses: + if response.HasField('sync_response'): + print('Sync Response received\n'+str(response)) + elif response.HasField('error'): + print('gNMI Error '+str(response.error.code)+\ + ' received\n'+str(response.error.message) + str(response.error)) + elif response.HasField('update'): + print(response) + else: + print('Unknown response received:\n'+str(response)) + except KeyboardInterrupt: + print("Subscribe Session stopped by user.") + except grpc.RpcError as x: + print("grpc.RpcError received:\n%s" %x) + except Exception as err: + print(err) + + def main(): argparser = _create_parser() args = vars(argparser.parse_args()) @@ -409,8 +506,8 @@ def main(): response = _set(stub, paths, 'delete', user, password, json_value) print('The SetRequest response is below\n' + '-'*25 + '\n', response) elif mode == 'subscribe': - print('This mode not available in this version') - sys.exit() + request_iterator = gen_request(paths, args) + subscribe_start(stub, args, request_iterator) if __name__ == '__main__':