#!/usr/bin/env python # ultimately, this should provide an interactive cli for navigating confluent # tree and doing console with a socket. ]0; can be used to # present info such as whether it is in a console or other mode and, if in # console, how many other connections are live and looking # this means 'wcons' simply needs to make a terminal run and we'll take care of # the title while providing more info # this also means the socket interface needs to have ways to convey more # interesting pieces of data (like concurrent connection count) # socket will probably switch to a TLV scheme: # 32 bit TL, 8 bits of type code and 24 bit size # type codes: # 0: string data # 1: json data # 24 bit size allows the peer to avoid having to do any particular parsing to # understand message boundaries (which is a significant burden on the xCAT # protocol) # When in a console client mode, will recognize two escape sequences by # default: # Ctrl-E, c, ?: mimick conserver behavior # ctrl-]: go to interactive prompt (telnet escape, but not telnet prompt) # esc-( would interfere with normal esc use too much # ~ I will not use for now... import fcntl import getpass import optparse import os import readline import select import shlex import socket import ssl import sys import termios import tty exitcode = 0 consoleonly = False target = "/" path = os.path.dirname(os.path.realpath(__file__)) path = os.path.realpath(os.path.join(path, '..')) sys.path.append(path) import confluent.common.tlvdata as tlvdata SO_PASSCRED = 16 conserversequence = '\x05c' # ctrl-e, c oldtcattr = termios.tcgetattr(sys.stdin.fileno()) server = None def prompt(): try: return raw_input(target + ' -> ') except KeyboardInterrupt: print "" return "" except EOFError: # ctrl-d print("exit") return "exit" # sys.stdout.write(target + ' -> ') # sys.stdout.flush() # username = raw_input("Name: ") valid_commands = [ 'start', 'cd', 'show', 'set', 'create', ] candidates = None def completer(text, state): try: return rcompleter(text, state) except: import traceback traceback.print_exc() def rcompleter(text, state): global candidates global valid_commands cline = readline.get_line_buffer() if len(text): cline = cline[:-len(text)] args = shlex.split(cline, posix=True) currpos = len(args) if currpos and cline[-1] == ' ': lastarg = '' currpos += 1 elif currpos: lastarg = args[-1] if currpos <= 1: foundcount = 0 for cmd in valid_commands: if cmd.startswith(text): if foundcount == state: return cmd else: foundcount += 1 candidates = None return None cmd = args[0] if candidates is None: candidates = [] targpath = fullpath_target(lastarg) for res in send_request('retrieve', targpath, server): if 'item' in res: # a link relation if type(res['item']) == dict: candidates.append(res['item']["href"]) else: for item in res['item']: candidates.append(item["href"]) foundcount = 0 for elem in candidates: if cmd == 'cd' and elem[-1] != '/': continue if elem.startswith(text): if foundcount == state: return elem else: foundcount += 1 candidates = None return None def parseservervalue(serverstring): if serverstring.find(']:') != -1: server, port = serverstring[1:].split(']:') elif serverstring[0] == '[': server = serverstring[1:-1] port = 4001 elif -1 != opts.server.find(':'): server, port = opts.server.split(":") else: server = serverstring port = 4001 return (server, port) def parse_command(command): args = shlex.split(command, posix=True) return args currchildren = None def send_request(operation, path, server, parameters=None): payload = {'operation': operation, 'path': path} if parameters is not None: payload['parameters'] = parameters tlvdata.send_tlvdata(server, payload) result = tlvdata.recv_tlvdata(server) while '_requestdone' not in result: yield result result = tlvdata.recv_tlvdata(server) def do_command(command, server): global exitcode global target global currconsole global currchildren exitcode = 0 command = command.lower() argv = parse_command(command) if len(argv) == 0: return if argv[0] == 'exit': server.close() sys.exit(0) elif argv[0] == 'cd': otarget = target if len(argv) > 1: target = fullpath_target(argv[1], forcepath=True) else: # cd by itself, go 'home' target = '/' for res in send_request('retrieve', target, server): if 'errorcode' in res: exitcode = res['errorcode'] if 'error' in res: sys.stderr.write(target + ': ' + res['error'] + '\n') target = otarget elif argv[0] in ('cat', 'show', 'ls', 'dir'): if len(argv) > 1: targpath = fullpath_target(argv[1]) else: targpath = target for res in send_request('retrieve', targpath, server): if 'item' in res: # a link relation if type(res['item']) == dict: print res['item']["href"] else: for item in res['item']: print item["href"] else: # generic attributes to list for key in res.iterkeys(): notes = [] if res[key] is None: attrstr = '%s=""' % key elif type(res[key]) == list: attrstr = '%s=["%s"]' % (key, '","'.join(res[key])) elif 'value' in res[key] and res[key]['value'] is not None: attrstr = '%s="%s"' % (key, res[key]['value']) elif 'value' in res[key] and res[key]['value'] is None: attrstr = '%s=""' % key else: if 'isset' in res[key] and res[key]['isset']: attrstr = '%s="********"' % key else: attrstr = '%s=""' % key if res[key] is not None and 'inheritedfrom' in res[key]: notes.append('Inherited from %s' % res[key]['inheritedfrom']) if res[key] is not None and 'expression' in res[key]: notes.append('Derived from expression "%s"' % res[key]['expression']) notestr = '(' + ', '.join(notes) + ')' output = '{0:<40} {1:>39}'.format(attrstr, notestr) print output elif argv[0] == 'start': targpath = fullpath_target(argv[1]) currconsole = targpath tlvdata.send_tlvdata(server, {'operation': 'start', 'path': targpath}) status = tlvdata.recv_tlvdata(server) if 'error' in status: if 'errorcode' in status: exitcode = status['errorcode'] sys.stderr.write('Error: ' + status['error'] + '\n') return print '[console session started]' startconsole() return elif argv[0] == 'set': setvalues(argv[1:]) else: sys.stderr.write("%s: command not found...\n" % argv[0]) def setvalues(attribs): if '=' in attribs[0]: # going straight to attribute resource = attribs[0][:attribs[0].index("=")] if '/' in resource: lastslash = resource.rindex('/') attribs[0] = attribs[0][lastslash + 1:] else: # an actual resource resource = attribs[0] attribs = attribs[1:] keydata = {} for attrib in attribs: if '=' not in attrib: sys.stderr.write("Invalid syntax %s" % attrib) return key = attrib[:attrib.index("=")] value = attrib[attrib.index("=") + 1:] keydata[key] = value targpath = fullpath_target(resource) for res in send_request('update', targpath, server, keydata): if 'error' in res: if 'errorcode' in res: exitcode = res['errorcode'] sys.stderr.write('Error: ' + res['error'] + '\n') def fullpath_target(path, forcepath=False): global target if path == '': return target pathcomponents = path.split("/") if pathcomponents[-1] == "": # preserve path forcepath=True if pathcomponents[0] == "": # absolute path ntarget = path else: targparts = target.split("/")[:-1] for component in pathcomponents: if component in ('.',''): # ignore these continue elif component == '..': if len(targparts) > 0: del targparts[-1] else: targparts.append(component) if forcepath and targparts[-1] != "": targparts.append('') ntarget = '/'.join(targparts) if forcepath and (len(ntarget) == 0 or ntarget[-1] != '/'): ntarget += '/' return ntarget def startconsole(): global inconsole tty.setraw(sys.stdin.fileno()) currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL) fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl | os.O_NONBLOCK) inconsole = True def exit(code=0): global consoleonly global inconsole global currconsole currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL) fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl ^ os.O_NONBLOCK) termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr) if consoleonly: server.shutdown(socket.SHUT_RDWR) server.close() sys.exit(code) else: tlvdata.send_tlvdata(server, {'operation': 'stop', 'path': currconsole}) inconsole = False def conserver_command(fh, command): while not command: ready, _, _ = select.select((fh,), (), (), 1) if ready: command += fh.read() if command[0] == '.': print("disconnect]\r") exit() elif command[0] == '?': print("help]\r") print(". disconnect\r") print(" abort command\r") elif command[0] == '\x0d': print("ignored]\r") else: #not a command at all.. print("unknown -- use '?']\r") def check_escape_seq(input, fh): while conserversequence.startswith(input): if input.startswith(conserversequence): # We have full sequence sys.stdout.write("[") sys.stdout.flush() return conserver_command(fh, input[len(conserversequence):]) ready, _, _ = select.select((fh,), (), (), 3) if not ready: # 3 seconds of no typing break input += fh.read() return input def connect_unix_server(sockpath): server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, SO_PASSCRED, 1) server.connect(sockpath) return server def connect_tls_server(serverstring): host, port = parseservervalue(serverstring) for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM): af, socktype, proto, cononname, sa = res try: server = socket.socket(af, socktype, proto) server.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) except: server = None continue try: server.settimeout(5) server.connect(sa) except: server.close() server = None continue break if server is None: sys.stderr.write("Failed to connect to %s\n" % serverstring) sys.exit(1) secserver = ssl.wrap_socket(server) return secserver parser = optparse.OptionParser() parser.add_option("-s", "--server", dest="server", help="TLS server to connect to", metavar="SERVER:PORT") parser.add_option("-u", "--unixsocket", dest="unixsock", help="TLS server to connect to", metavar="UNIXDOMAINSOCKET") opts, args = parser.parse_args() if opts.server: # going over a TLS network server = connect_tls_server(opts.server) elif opts.unixsock: server = connect_unix_server(opts.unixsock) elif os.path.exists("/var/run/confluent/api.sock"): server = connect_unix_server("/var/run/confluent/api.sock") #Next stop, reading and writing from whichever of stdin and server goes first. #see pyghmi code for solconnect.py banner = tlvdata.recv_tlvdata(server) authinfo = tlvdata.recv_tlvdata(server) while authinfo['authpassed'] != 1: username = raw_input("Name: ") readline.clear_history() passphrase = getpass.getpass("Passphrase: ") tlvdata.send_tlvdata(server, {'username': username, 'passphrase': passphrase}) authinfo = tlvdata.recv_tlvdata(server) # clear on start can help with readable of TUI, but it # can be annoying, so for now don't do it. # sys.stdout.write('\x1b[H\x1b[J') # sys.stdout.flush() readline.parse_and_bind("tab: complete") readline.parse_and_bind("set bell-style none") readline.set_completer(completer) doexit = False inconsole = False pendingcommand = "" if len(args) == 1: # a node name, go straight to trying to console consoleonly = True do_command("start /node/%s/console/session" % args[0], server) while not doexit: if inconsole: rdylist, _, _ = select.select((sys.stdin, server), (), (), 60) for fh in rdylist: if fh == server: # this only should get called in the # case of a console session # each command should slurp up all relevant # recv_tlvdata potential #fh.read() try: data = tlvdata.recv_tlvdata(fh) except Exception: data = None if type(data) == dict: print repr(data) continue if data is not None: sys.stdout.write(data) sys.stdout.flush() else: doexit = True sys.stdout.write("\r\n[remote disconnected]\r\n") break else: input = fh.read() input = check_escape_seq(input, fh) if input: tlvdata.send_tlvdata(server, input) else: command = prompt() do_command(command, server) consoleonly = True exit()