From 61a6dda57b779c47a2085d6b7f3c5f1572892821 Mon Sep 17 00:00:00 2001 From: Tejaskumar Kasundra Date: Thu, 28 Feb 2019 19:44:00 +0530 Subject: [PATCH 1/6] Update py_gnmicli.py for gNMI Subscribe support Adding support for gNMI subscribe. --- gnmi_cli_py/py_gnmicli.py | 85 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 4 deletions(-) diff --git a/gnmi_cli_py/py_gnmicli.py b/gnmi_cli_py/py_gnmicli.py index 90c92dad..b1595547 100644 --- a/gnmi_cli_py/py_gnmicli.py +++ b/gnmi_cli_py/py_gnmicli.py @@ -46,13 +46,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.3' _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 +141,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 @@ -248,7 +263,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) + 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 +364,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'): + dt = {} + for x in range (len(response.update.update)): + dt['timestamp'] = response.update.timestamp + dt[response.update.update[x].path.elem[-1].name] = response.update.update[x].val.uint_val + # print("Timestamp: %s Val: %s Element Name: %s" % (response.update.timestamp, response.update.update[x].val, response.update.update[x].path.elem[-1].name)) + print(dt) + 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()) @@ -407,8 +484,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__': From cbeb57aa149ff2be85df4ccd2893cbcb735543ec Mon Sep 17 00:00:00 2001 From: Tejaskumar Kasundra Date: Fri, 1 Mar 2019 14:15:34 +0530 Subject: [PATCH 2/6] Update py_gnmicli.py Discarded redundant print response. --- gnmi_cli_py/py_gnmicli.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gnmi_cli_py/py_gnmicli.py b/gnmi_cli_py/py_gnmicli.py index b1595547..c7254550 100644 --- a/gnmi_cli_py/py_gnmicli.py +++ b/gnmi_cli_py/py_gnmicli.py @@ -409,12 +409,6 @@ def subscribe_start(stub, options, req_iterator): elif response.HasField('error'): print('gNMI Error '+str(response.error.code)+' received\n'+str(response.error.message) + str(response.error)) elif response.HasField('update'): - dt = {} - for x in range (len(response.update.update)): - dt['timestamp'] = response.update.timestamp - dt[response.update.update[x].path.elem[-1].name] = response.update.update[x].val.uint_val - # print("Timestamp: %s Val: %s Element Name: %s" % (response.update.timestamp, response.update.update[x].val, response.update.update[x].path.elem[-1].name)) - print(dt) print(response) else: print('Unknown response received:\n'+str(response)) From 161c56badbaaa428dee717895b913fa4d8e328ce Mon Sep 17 00:00:00 2001 From: Tejaskumar Kasundra Date: Fri, 1 Mar 2019 14:21:33 +0530 Subject: [PATCH 3/6] Update README.md Added subscribe request example. --- gnmi_cli_py/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gnmi_cli_py/README.md b/gnmi_cli_py/README.md index e6311f0b..2e6f8e0f 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 From cdd72b50d0d77c7d7ed9c0001cb9b07c2838f171 Mon Sep 17 00:00:00 2001 From: Tejaskumar Kasundra Date: Mon, 4 Mar 2019 14:36:31 +0530 Subject: [PATCH 4/6] Update py_gnmicli.py Fixing issue - https://github.com/google/gnxi/issues/67 --- gnmi_cli_py/py_gnmicli.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/gnmi_cli_py/py_gnmicli.py b/gnmi_cli_py/py_gnmicli.py index c7254550..34f156cd 100644 --- a/gnmi_cli_py/py_gnmicli.py +++ b/gnmi_cli_py/py_gnmicli.py @@ -169,9 +169,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): From 8917d0f2bc70b16b33dca86cde16a6cea176fec3 Mon Sep 17 00:00:00 2001 From: Tejaskumar Kasundra Date: Mon, 15 Jun 2020 00:17:20 +0530 Subject: [PATCH 5/6] Fixed Line length --- gnmi_cli_py/py_gnmicli.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/gnmi_cli_py/py_gnmicli.py b/gnmi_cli_py/py_gnmicli.py index 5165cc05..11cf6127 100644 --- a/gnmi_cli_py/py_gnmicli.py +++ b/gnmi_cli_py/py_gnmicli.py @@ -395,7 +395,10 @@ def gen_request(paths, opt): 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']) + 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"])) @@ -406,7 +409,9 @@ def gen_request(paths, opt): 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) + 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)) @@ -429,7 +434,8 @@ def subscribe_start(stub, options, req_iterator): 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)) + print('gNMI Error '+str(response.error.code)+\ + ' received\n'+str(response.error.message) + str(response.error)) elif response.HasField('update'): print(response) else: From 965d41def8472d556ae3f239826471b14f7e4475 Mon Sep 17 00:00:00 2001 From: Tejaskumar Kasundra Date: Tue, 7 Jul 2020 11:18:29 +0530 Subject: [PATCH 6/6] Update py_gnmicli.py - Version updated. - Updated info section. --- gnmi_cli_py/py_gnmicli.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gnmi_cli_py/py_gnmicli.py b/gnmi_cli_py/py_gnmicli.py index 11cf6127..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 @@ -48,7 +46,7 @@ import gnmi_pb2_grpc import grpc -__version__ = '0.4' +__version__ = '0.5' _RE_PATH_COMPONENT = re.compile(r''' ^