Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 123 additions & 38 deletions gnmi_cli_py/py_gnmicli.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,11 @@ def _create_parser():
'file (prepend filename with "@")', default='get')
parser.add_argument('-val', '--value', type=str, help='Value for SetRequest.'
'\nCan be Leaf value or JSON file. If JSON file, prepend'
' with "@"; eg "@interfaces.json".',
required=False)
' with "@"; eg "@interfaces.json".'
'\n If empty value for delete operation, use "".',
nargs="+", required=False)
Copy link

@qiluo-msft qiluo-msft Sep 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"+"

You assume at least one file. If the query is expected to fail, do you allow gnmicli to have 0 args? #Closed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to use *

parser.add_argument('--proto', type=str, help='Output files for proto bytes',
nargs="*", required=False)
parser.add_argument('-pkey', '--private_key', type=str, help='Fully'
'quallified path to Private key to use when establishing'
'a gNMI Channel to the Target', required=False)
Expand All @@ -127,10 +130,13 @@ def _create_parser():
'Target when establishing secure gRPC channel.',
required=False, action='store_true')
parser.add_argument('-x', '--xpath', type=str, help='The gNMI path utilized'
'in the GetRequest or Subscirbe', required=True)
'in the GetRequest or Subscirbe', nargs="+", required=True)
parser.add_argument('-xt', '--xpath_target', type=str, help='The gNMI prefix'
'target in the GetRequest or Subscirbe', default=None,
required=False)
parser.add_argument('-xo', '--xpath_origin', type=str, help='The gNMI prefix'
'origin in the GetRequest, SetRequest or Subscirbe', default=None,
required=False)
parser.add_argument('-o', '--host_override', type=str, help='Use this as '
'Targets hostname/peername when checking it\'s'
'certificate CN. You can check the cert with:\nopenssl '
Expand Down Expand Up @@ -181,7 +187,53 @@ def _path_names(xpath):
"""
if not xpath or xpath == '/': # A blank xpath was provided at CLI.
return []
return xpath.strip().strip('/').split('/') # Remove leading and trailing '/'.
xpath = xpath.strip().strip('/')
path = []
xpath = xpath + '/'
# insideBrackets is true when at least one '[' has been found and no
# ']' has been found. It is false when a closing ']' has been found.
insideBrackets = False
# begin marks the beginning of a path element, which is separated by
# '/' unclosed between '[' and ']'.
begin = 0
# end marks the end of a path element, which is separated by '/'
# unclosed between '[' and ']'.
end = 0

# Split the given string using unescaped '/'.
while end < len(xpath):
if xpath[end] == '/':
if not insideBrackets:
# Current '/' is a valid path element
# separator.
if end > begin:
path.append(xpath[begin:end])
end += 1
begin = end
else:
# Current '/' must be part of a List key value
# string.
end += 1
elif xpath[end] == '[':
if (end == 0 or xpath[end-1] != '\\') and not insideBrackets:
# Current '[' is unescacped, and is the
# beginning of List key-value pair(s) string.
insideBrackets = True
end += 1
elif xpath[end] == ']':
if (end == 0 or xpath[end-1] != '\\') and insideBrackets:
# Current ']' is unescacped, and is the end of
# List key-value pair(s) string.
insideBrackets = False
end += 1
else:
end += 1

if insideBrackets:
print("missing ] in path string: %s" % xpath)
return []

return path


def _parse_path(p_names):
Expand Down Expand Up @@ -275,6 +327,16 @@ def _get_val(json_value):
raise JsonReadError('Error while loading JSON: %s' % str(e))
val.json_ietf_val = json.dumps(set_json).encode()
return val
elif '$' in json_value:
try:
proto_bytes = six.moves.builtins.open(json_value.strip('$'), 'rb').read()
except (IOError, ValueError) as e:
raise ValueError('Error while loading %s: %s' % (json_value.strip('$'), str(e)))
val.proto_bytes = proto_bytes
return val
elif json_value == '':
# GNMI client should use delete operation for empty string
return None
coerced_val = _format_type(json_value)
type_to_value = {bool: 'bool_val', int: 'int_val', float: 'float_val',
str: 'string_val'}
Expand All @@ -283,52 +345,53 @@ def _get_val(json_value):
return val


def _get(stub, paths, username, password, prefix):
def _get(stub, paths, username, password, prefix, encoding):
"""Create a gNMI GetRequest.

Args:
stub: (class) gNMI Stub used to build the secure channel.
paths: gNMI Path
paths: (list) gNMI Path
username: (str) Username used when building the channel.
password: (str) Password used when building the channel.
prefix: gNMI Path

encoding: (int) Encoding
Returns:
a gnmi_pb2.GetResponse object representing a gNMI GetResponse.
"""
kwargs = {}
if username: # User/pass supplied for Authentication.
kwargs = {'metadata': [('username', username), ('password', password)]}
return stub.Get(
gnmi_pb2.GetRequest(prefix=prefix, path=[paths], encoding='JSON_IETF'),
gnmi_pb2.GetRequest(prefix=prefix, path=paths, encoding=encoding),
**kwargs)

def _set(stub, paths, set_type, username, password, json_value):
def _set(stub, prefix, paths, set_type, username, password, value_list):
"""Create a gNMI SetRequest.

Args:
stub: (class) gNMI Stub used to build the secure channel.
paths: gNMI Path
paths: (list) gNMI Path
set_type: (str) Type of gNMI SetRequest.
username: (str) Username used when building the channel.
password: (str) Password used when building the channel.
json_value: (str) JSON_IETF or file.
value_list: (list) JSON_IETF or file.

Returns:
a gnmi_pb2.SetResponse object representing a gNMI SetResponse.
"""
if json_value: # Specifying ONLY a path is possible (eg delete).
val = _get_val(json_value)
path_val = gnmi_pb2.Update(path=paths, val=val,)

delete_list = []
update_list = []
for path, value in zip(paths, value_list):
val = _get_val(value)
if val is None:
delete_list.append(path)
else:
path_val = gnmi_pb2.Update(path=path, val=val,)
update_list.append(path_val)
kwargs = {}
if username:
kwargs = {'metadata': [('username', username), ('password', password)]}
if set_type == 'delete':
return stub.Set(gnmi_pb2.SetRequest(delete=[paths]), **kwargs)
elif set_type == 'update':
return stub.Set(gnmi_pb2.SetRequest(update=[path_val]), **kwargs)
return stub.Set(gnmi_pb2.SetRequest(replace=[path_val]), **kwargs)
return stub.Set(gnmi_pb2.SetRequest(prefix=prefix, delete=delete_list, update=update_list), **kwargs)


def _build_creds(target, port, get_cert, certs, notls):
Expand Down Expand Up @@ -378,17 +441,18 @@ def _open_certs(**kwargs):
def gen_request(paths, opt, prefix):
"""Create subscribe request for passed xpath.
Args:
paths: (str) gNMI path.
paths: (list) 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"]*1000000,
heartbeat_interval=opt['heartbeat']*1000000,
suppress_redundant=opt['suppress'])
mysubs.append(mysub)
for path in paths:
mysub = gnmi_pb2.Subscription(path=path, mode=opt["submode"],
sample_interval=opt["interval"]*1000000,
heartbeat_interval=opt['heartbeat']*1000000,
suppress_redundant=opt['suppress'])
mysubs.append(mysub)

if prefix:
myprefix = prefix
Expand Down Expand Up @@ -482,16 +546,22 @@ def main():
get_cert = args['get_cert']
root_cert = args['root_cert']
cert_chain = args['cert_chain']
json_value = args['value']
value_list = args['value']
private_key = args['private_key']
xpath = args['xpath']
prefix = gnmi_pb2.Path(target=args['xpath_target'])
xpath_list = args['xpath']
proto_list = args['proto']
# In the case that a prefix is specified, it MUST specify any required origin
prefix = gnmi_pb2.Path(origin=args['xpath_origin'], target=args['xpath_target'])
host_override = args['host_override']
user = args['username']
password = args['password']
form = args['format']
create_connections = args['create_connections']
paths = _parse_path(_path_names(xpath))
encoding = args['encoding']
paths = []
if xpath_list:
for xpath in xpath_list:
paths.append(_parse_path(_path_names(xpath)))
kwargs = {'root_cert': root_cert, 'cert_chain': cert_chain,
'private_key': private_key}
certs = _open_certs(**kwargs)
Expand All @@ -517,32 +587,44 @@ def main():
if mode == 'get':
print('Performing GetRequest, encoding=JSON_IETF', 'to', target,
' with the following gNMI Path\n', '-'*25, '\n', paths)
response = _get(stub, paths, user, password, prefix)
response = _get(stub, paths, user, password, prefix, encoding)
print('The GetResponse is below\n' + '-'*25 + '\n')
if form == 'protobuff':
if encoding == 2:
i = 0
for notification in response.notification:
for update in notification.update:
if i >= len(proto_list):
print("Not enough files: %s" % str(proto_list))
sys.exit(1)
with open(proto_list[i], 'wb') as fp:
Copy link

@qiluo-msft qiluo-msft Sep 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[i]

Possible to index out of range? #Closed

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

fp.write(update.val.proto_bytes)
i += 1
elif form == 'protobuff':
print(response)
elif response.notification[0].update[0].val.json_ietf_val:
print(json.dumps(json.loads(response.notification[0].update[0].val.
json_ietf_val), indent=2))
for notification in response.notification:
for update in notification.update:
print(json.dumps(json.loads(update.val.json_ietf_val), indent=2))
print('-'*25 + '\n')
elif response.notification[0].update[0].val.string_val:
print(response.notification[0].update[0].val.string_val)
else:
print('JSON Format specified, but gNMI Response was not json_ietf_val')
print(response)
elif mode == 'set-update':
print('Performing SetRequest Update, encoding=JSON_IETF', ' to ', target,
' with the following gNMI Path\n', '-'*25, '\n', paths, json_value)
response = _set(stub, paths, 'update', user, password, json_value)
' with the following gNMI Path\n', '-'*25, '\n', paths, value_list)
response = _set(stub, prefix, paths, 'update', user, password, value_list)
print('The SetRequest response is below\n' + '-'*25 + '\n', response)
elif mode == 'set-replace':
print('Performing SetRequest Replace, encoding=JSON_IETF', ' to ', target,
' with the following gNMI Path\n', '-'*25, '\n', paths)
response = _set(stub, paths, 'replace', user, password, json_value)
response = _set(stub, prefix, paths, 'replace', user, password, value_list)
print('The SetRequest response is below\n' + '-'*25 + '\n', response)
elif mode == 'set-delete':
print('Performing SetRequest Delete, encoding=JSON_IETF', ' to ', target,
' with the following gNMI Path\n', '-'*25, '\n', paths)
response = _set(stub, paths, 'delete', user, password, json_value)
response = _set(stub, prefix, paths, 'delete', user, password, value_list)
print('The SetRequest response is below\n' + '-'*25 + '\n', response)
elif mode == 'subscribe':
request_iterator = gen_request(paths, args, prefix)
Expand All @@ -552,6 +634,9 @@ def main():
print("Client receives an exception '{}' indicating gNMI server is shut down and Exiting ..."
.format(err.details()))
sys.exit(GNMI_SERVER_UNAVAILABLE)
else:
print("GRPC error\n {}".format(err.details()))
sys.exit(1)


if __name__ == '__main__':
Expand Down
4 changes: 2 additions & 2 deletions gnmi_cli_py/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
enum34==1.1.6
futures==3.2.0
grpcio==1.18.0
grpcio-tools==1.15.0
grpcio==1.41.1
grpcio-tools==1.41.1
protobuf==3.6.1 --no-binary=protobuf
six==1.12.0