mirror of https://github.com/ntop/n2n.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
334 lines
9.2 KiB
334 lines
9.2 KiB
#!/usr/bin/env python3
|
|
# Licensed under GPLv3
|
|
#
|
|
# Simple script to query the management interface of a running n2n edge node
|
|
|
|
import argparse
|
|
import socket
|
|
import json
|
|
import collections
|
|
import time
|
|
|
|
|
|
class JsonUDP():
|
|
"""encapsulate communication with the edge"""
|
|
|
|
def __init__(self, port):
|
|
self.address = "127.0.0.1"
|
|
self.port = port
|
|
self.tag = 0
|
|
self.key = None
|
|
self.debug = False
|
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
self.sock.settimeout(1)
|
|
|
|
def _next_tag(self):
|
|
tagstr = str(self.tag)
|
|
self.tag = (self.tag + 1) % 1000
|
|
return tagstr
|
|
|
|
def _cmdstr(self, msgtype, cmdline):
|
|
"""Create the full command string to send"""
|
|
tagstr = self._next_tag()
|
|
|
|
options = [tagstr]
|
|
if self.key is not None:
|
|
options += ['1'] # Flags set for auth key field
|
|
options += [self.key]
|
|
optionsstr = ':'.join(options)
|
|
|
|
return tagstr, ' '.join((msgtype, optionsstr, cmdline))
|
|
|
|
def _rx(self, tagstr):
|
|
"""Wait for rx packets"""
|
|
|
|
seen_begin = False
|
|
while not seen_begin:
|
|
# TODO: there are no timeouts with any of the recv calls
|
|
data, _ = self.sock.recvfrom(1024)
|
|
data = json.loads(data.decode('utf8'))
|
|
|
|
# TODO: We assume the first packet we get will be tagged for us
|
|
assert (data['_tag'] == tagstr)
|
|
|
|
if data['_type'] == 'error':
|
|
raise ValueError('Error: {}'.format(data['error']))
|
|
|
|
if data['_type'] == 'replacing':
|
|
# a signal that we have evicted an earlier subscribe
|
|
continue
|
|
|
|
if data['_type'] == 'subscribe':
|
|
return True
|
|
|
|
if data['_type'] == 'begin':
|
|
seen_begin = True
|
|
|
|
# Ideally, we would confirm that this is our "begin", but that
|
|
# would need the cmd passed into this method, and that would
|
|
# probably require parsing the cmdline passed to us :-(
|
|
# assert(data['cmd'] == cmd)
|
|
|
|
continue
|
|
|
|
raise ValueError('Unknown data type {} from '
|
|
'edge'.format(data['_type']))
|
|
|
|
result = list()
|
|
error = None
|
|
|
|
while True:
|
|
data, _ = self.sock.recvfrom(1024)
|
|
data = json.loads(data.decode('utf8'))
|
|
|
|
if data['_tag'] != tagstr:
|
|
# this packet is not for us, ignore it
|
|
continue
|
|
|
|
if data['_type'] == 'error':
|
|
# we still expect an end packet, so save the error
|
|
error = ValueError('Error: {}'.format(data['error']))
|
|
continue
|
|
|
|
if data['_type'] == 'end':
|
|
if error:
|
|
raise error
|
|
return result
|
|
|
|
if data['_type'] != 'row':
|
|
raise ValueError('Unknown data type {} from '
|
|
'edge'.format(data['_type']))
|
|
|
|
# remove our boring metadata
|
|
del data['_tag']
|
|
del data['_type']
|
|
|
|
if self.debug:
|
|
print(data)
|
|
|
|
result.append(data)
|
|
|
|
def _call(self, msgtype, cmdline):
|
|
"""Perform a rpc call"""
|
|
tagstr, msgstr = self._cmdstr(msgtype, cmdline)
|
|
self.sock.sendto(msgstr.encode('utf8'), (self.address, self.port))
|
|
return self._rx(tagstr)
|
|
|
|
def read(self, cmdline):
|
|
return self._call('r', cmdline)
|
|
|
|
def write(self, cmdline):
|
|
return self._call('w', cmdline)
|
|
|
|
def sub(self, cmdline):
|
|
return self._call('s', cmdline)
|
|
|
|
def readevent(self):
|
|
self.sock.settimeout(3600)
|
|
|
|
data, _ = self.sock.recvfrom(1024)
|
|
data = json.loads(data.decode('utf8'))
|
|
# assert(data['_tag'] == tagstr)
|
|
assert (data['_type'] == 'event')
|
|
|
|
del data['_tag']
|
|
del data['_type']
|
|
return data
|
|
|
|
|
|
def str_table(rows, columns, orderby):
|
|
"""Given an array of dicts, do a simple table print"""
|
|
result = list()
|
|
widths = collections.defaultdict(lambda: 0)
|
|
|
|
if len(rows) == 0:
|
|
# No data to show, be sure not to truncate the column headings
|
|
for col in columns:
|
|
widths[col] = len(col)
|
|
else:
|
|
for row in rows:
|
|
for col in columns:
|
|
if col in row:
|
|
widths[col] = max(widths[col], len(str(row[col])))
|
|
|
|
for col in columns:
|
|
if widths[col] == 0:
|
|
widths[col] = 1
|
|
result += "{:{}.{}} ".format(col, widths[col], widths[col])
|
|
result += "\n"
|
|
|
|
if orderby is not None:
|
|
rows = sorted(rows, key=lambda row: row.get(orderby, 0))
|
|
|
|
for row in rows:
|
|
for col in columns:
|
|
if col in row:
|
|
data = row[col]
|
|
else:
|
|
data = ''
|
|
result += "{:{}} ".format(data, widths[col])
|
|
result += "\n"
|
|
|
|
return ''.join(result)
|
|
|
|
|
|
def num2timestr(seconds):
|
|
"""Convert a number of seconds into a human time"""
|
|
|
|
if seconds == 0:
|
|
return "now"
|
|
|
|
days, seconds = divmod(seconds, (60*60*24))
|
|
hours, seconds = divmod(seconds, (60*60))
|
|
minutes, seconds = divmod(seconds, 60)
|
|
|
|
r = []
|
|
if days:
|
|
r += [f"{days}d"]
|
|
if hours:
|
|
r += [f"{hours}h"]
|
|
if minutes:
|
|
r += [f"{minutes}m"]
|
|
if seconds:
|
|
r += [f"{seconds}s"]
|
|
return "".join(r)
|
|
|
|
|
|
def subcmd_show_supernodes(rpc, args):
|
|
rows = rpc.read('supernodes')
|
|
columns = [
|
|
'version',
|
|
'current',
|
|
'macaddr',
|
|
'sockaddr',
|
|
'uptime',
|
|
'last_seen',
|
|
]
|
|
|
|
now = int(time.time())
|
|
for row in rows:
|
|
row["last_seen"] = num2timestr(now - row["last_seen"])
|
|
|
|
return str_table(rows, columns, args.orderby)
|
|
|
|
|
|
def subcmd_show_edges(rpc, args):
|
|
rows = rpc.read('edges')
|
|
columns = [
|
|
'mode',
|
|
'ip4addr',
|
|
'macaddr',
|
|
'sockaddr',
|
|
'desc',
|
|
'last_seen',
|
|
]
|
|
|
|
now = int(time.time())
|
|
for row in rows:
|
|
row["last_seen"] = num2timestr(now - row["last_seen"])
|
|
|
|
return str_table(rows, columns, args.orderby)
|
|
|
|
|
|
def subcmd_show_help(rpc, args):
|
|
result = 'Commands with pretty-printed output:\n\n'
|
|
for name, cmd in subcmds.items():
|
|
result += "{:12} {}\n".format(name, cmd['help'])
|
|
|
|
result += "\n"
|
|
result += "Possble remote commands:\n"
|
|
result += "(those without a pretty-printer will pass-through)\n\n"
|
|
rows = rpc.read('help')
|
|
for row in rows:
|
|
result += "{:12} {}\n".format(row['cmd'], row['help'])
|
|
return result
|
|
|
|
|
|
subcmds = {
|
|
'help': {
|
|
'func': subcmd_show_help,
|
|
'help': 'Show available commands',
|
|
},
|
|
'supernodes': {
|
|
'func': subcmd_show_supernodes,
|
|
'help': 'Show the list of supernodes',
|
|
},
|
|
'edges': {
|
|
'func': subcmd_show_edges,
|
|
'help': 'Show the list of edges/peers',
|
|
},
|
|
}
|
|
|
|
|
|
def subcmd_default(rpc, args):
|
|
"""Just pass command through to edge"""
|
|
cmdline = ' '.join([args.cmd] + args.args)
|
|
if args.write:
|
|
rows = rpc.write(cmdline)
|
|
elif args.read:
|
|
rows = rpc.read(cmdline)
|
|
elif args.sub:
|
|
if not rpc.sub(cmdline):
|
|
raise ValueError('Could not subscribe')
|
|
while True:
|
|
event = rpc.readevent()
|
|
# FIXME: violates layering..
|
|
print(json.dumps(event, sort_keys=True, indent=4))
|
|
else:
|
|
raise ValueError('Unknown request type')
|
|
return json.dumps(rows, sort_keys=True, indent=4)
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(
|
|
description='Query the running local n2n edge')
|
|
ap.add_argument('-t', '--mgmtport', action='store', default=5644,
|
|
help='Management Port (default=5644)', type=int)
|
|
ap.add_argument('-k', '--key', action='store',
|
|
help='Password for mgmt commands')
|
|
ap.add_argument('-d', '--debug', action='store_true',
|
|
help='Also show raw internal data')
|
|
ap.add_argument('--raw', action='store_true',
|
|
help='Force cmd to avoid any pretty printing')
|
|
ap.add_argument('--orderby', action='store',
|
|
help='Hint to a pretty printer on how to sort')
|
|
|
|
group = ap.add_mutually_exclusive_group()
|
|
group.add_argument('--read', action='store_true',
|
|
help='Make a read request (default)')
|
|
group.add_argument('--write', action='store_true',
|
|
help='Make a write request (only to non pretty'
|
|
'printed cmds)')
|
|
group.add_argument('--sub', action='store_true',
|
|
help='Make a subscribe request')
|
|
|
|
ap.add_argument('cmd', action='store',
|
|
help='Command to run (try "help" for list)')
|
|
ap.add_argument('args', action='store', nargs="*",
|
|
help='Optional args for the command')
|
|
|
|
args = ap.parse_args()
|
|
|
|
if not args.read and not args.write and not args.sub:
|
|
args.read = True
|
|
|
|
if args.raw or (args.cmd not in subcmds):
|
|
func = subcmd_default
|
|
else:
|
|
func = subcmds[args.cmd]['func']
|
|
|
|
rpc = JsonUDP(args.mgmtport)
|
|
rpc.debug = args.debug
|
|
rpc.key = args.key
|
|
|
|
try:
|
|
result = func(rpc, args)
|
|
except socket.timeout as e:
|
|
print(e)
|
|
exit(1)
|
|
|
|
print(result)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|