#!/usr/bin/env python # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2014 IBM Corporation # Copyright 2015-2016 Lenovo # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # 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, ?: mimic 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 math import getpass import optparse import os import select import shlex import signal import socket import sys import time try: import fcntl import termios import tty except ImportError: pass try: signal.signal(signal.SIGPIPE, signal.SIG_DFL) except AttributeError: pass exitcode = 0 consoleonly = False consolename = "" didconsole = False target = "/" path = os.path.dirname(os.path.realpath(__file__)) path = os.path.realpath(os.path.join(path, '..', 'lib', 'python')) if path.startswith('/opt'): sys.path.append(path) import confluent.termhandler as termhandler import confluent.tlvdata as tlvdata import confluent.client as client conserversequence = '\x05c' # ctrl-e, c oldtcattr = None fd = sys.stdin try: if fd.isatty(): oldtcattr = termios.tcgetattr(fd.fileno()) except NameError: pass netserver = None laststate = {} def print_help(): print("confetty provides a filesystem like interface to confluent. " "Navigation is done using the same commands as would be used in a " "filesystem. Tab completion is supported to aid in navigation," "as is up arrow to recall previous commands and control-r to search" "previous command history, similar to using bash\n\n" "The supported commands are:\n" "cd [location] - Set the current command context, similar to a " "working directory.\n" "show [resource] - Present the information about the specified " "resource, or current context if omitted.\n" "create [resource] attributename=value attributename=value - Create " "a new instance of a resource.\n" "remove [resource] - Remove a resource from a list\n" "set [resource] attributename=value attributename=value - Change " "the specified attributes value for the given resource name\n" "unset [resource] attributename - Clear any value for the given " "attribute names on a resource.\n" "start [resource] - When used on a text session resource, it " "enters remote terminal mode. In this mode, use 'ctrl-e, c, ?' for " "help" ) #TODO(jjohnson2): lookup context help for 'target' variable, perhaps #common with the api document def updatestatus(stateinfo={}): status = consolename info = [] for statekey in stateinfo: laststate[statekey] = stateinfo[statekey] if ('connectstate' in laststate and laststate['connectstate'] != 'connected'): info.append(laststate['connectstate']) if laststate['connectstate'] == 'closed': quitconfetty(fullexit=consoleonly) if 'error' in laststate: info.append(laststate['error']) # error will be repeated if relevant # avoid keeping it around as stale del laststate['error'] if 'clientcount' in laststate and laststate['clientcount'] != 1: info.append('clients: %d' % laststate['clientcount']) if 'bufferage' in stateinfo and stateinfo['bufferage'] is not None: laststate['showtime'] = time.time() - stateinfo['bufferage'] if 'showtime' in laststate: showtime = laststate['showtime'] age = time.time() - laststate['showtime'] if age > 86400: # older than one day # disambiguate by putting date in and time info.append(time.strftime('%m-%dT%H:%M', time.localtime(showtime))) else: info.append(time.strftime('%H:%M', time.localtime(showtime))) if info: status += ' [' + ','.join(info) + ']' if os.environ.get('TERM', '') not in ('linux'): sys.stdout.write('\x1b]0;console: %s\x07' % status) sys.stdout.flush() def recurse_format(datum, levels=0): ret = '' import json return json.dumps(datum, ensure_ascii=False, indent=1) if isinstance(datum, dict): for key in datum.iterkeys(): if datum[key] is None: continue ret += key + ':' if type(datum[key]) in (str, unicode): ret += datum[key] + '\n' else: ret += recurse_format(datum[key], levels + 1) elif isinstance(datum, list): if type(datum[0]) in (str, unicode): ret += '[' + ",".join(datum) + ']\n' else: ret += '[' elems = [] for elem in datum: elems.append('{' + recurse_format(elem, levels + 1) + '}') ret += ','.join(elems) ret += (' ' * levels) + ']\n' return ret def prompt(): if os.environ.get('TERM', '') not in ('linux'): sys.stdout.write('\x1b]0;confetty: %s\x07' % target) 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', 'unset', 'create', 'remove', 'rm', 'delete', 'help', ] candidates = None session = None def completer(text, state): try: return rcompleter(text, state) except: pass #import traceback #traceback.print_exc() def rcompleter(text, state): global candidates global valid_commands cline = readline.get_line_buffer() cline = cline[:readline.get_endidx()] 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] else: lastarg = '' 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 session.read(targpath): 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 parse_command(command): try: args = shlex.split(command, posix=True) except ValueError as ve: print('Error: ' + str(ve)) return [] return args currchildren = None def print_result(res): if 'errorcode' in res or 'error' in res: print res['error'] return if 'databynode' in res: print_result(res['databynode']) return for key in sorted(res): notes = [] if res[key] is None: attrstr = '%s=""' % key elif type(res[key]) == list: attrstr = '%s=%s' % (key, recurse_format(res[key])) elif not isinstance(res[key], dict): try: print '{0}: {1}'.format(key, res[key]) except UnicodeEncodeError: print '{0}: {1}'.format(key, repr(res[key])) continue 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 elif 'isset' in res[key] and res[key]['isset']: attrstr = '%s="********"' % key elif 'isset' in res[key] or not res[key]: attrstr = '%s=""' % key else: sys.stdout.write('{0}: '.format(key)) if isinstance(res[key], str) or isinstance(res[key], unicode): print res[key] else: print_result(res[key]) continue 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'])) if notes: notestr = '(' + ', '.join(notes) + ')' output = '{0:<40} {1:>39}'.format(attrstr, notestr) else: output = attrstr try: print(output) except (UnicodeDecodeError, UnicodeEncodeError): print(output.encode('utf-8')) def do_command(command, server): global exitcode global target global currconsole global currchildren exitcode = 0 argv = parse_command(command) if len(argv) == 0: return argv[0] = argv[0].lower() if argv[0] == 'exit': if os.environ.get('TERM', '') not in ('linux'): sys.stdout.write('\x1b]0;\x07') sys.exit(0) elif argv[0] in ('help', '?'): return print_help() elif argv[0] == 'cd': otarget = target if len(argv) > 1: target = fullpath_target(argv[1], forcepath=True) else: # cd by itself, go 'home' target = '/' if target[-1] == '/': parentpath = target[:-1] else: parentpath = target if parentpath: childname = '{0}/'.format(parentpath[parentpath.rindex('/') + 1:]) parentpath = parentpath[:parentpath.rindex('/') + 1] if parentpath == '/noderange/': for res in session.read(target, server): if 'errorcode' in res: exitcode = res['errorcode'] target = otarget if 'error' in res: sys.stderr.write(target + ': ' + res['error'] + '\n') target = otarget else: foundchild = False for res in session.read(parentpath, server): try: if res['item']['href'] == childname: foundchild = True except KeyError: pass if 'errorcode' in res: exitcode = res['errorcode'] target = otarget if 'error' in res: sys.stderr.write(target + ': ' + res['error'] + '\n') target = otarget if not foundchild: sys.stderr.write(target + ': Target not found - \n') target = otarget elif argv[0] in ('cat', 'show', 'ls', 'dir'): if len(argv) > 1: targpath = fullpath_target(argv[1]) if argv[0] in ('ls', 'dir'): if targpath[-1] != '/': # could still be a directory, fetch the parent.. childname = targpath[targpath.rindex('/') + 1:] parentpath = targpath[:targpath.rindex('/') + 1] if parentpath != '/noderange/': # if it were /noderange/, then it's a directory # even though parent won't tell us that for res in session.read(parentpath, server): try: if res['item']['href'] == childname: print(childname) return except KeyError: pass else: targpath = target for res in session.read(targpath): 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 if 'error' in res: sys.stderr.write(res['error'] + '\n') if 'errorcode' in res: exitcode = res['errorcode'] continue print_result(res) elif argv[0] == 'start': targpath = fullpath_target(argv[1]) nodename = targpath.split('/')[-3] currconsole = targpath startrequest = {'operation': 'start', 'path': targpath, 'parameters': {}} for param in argv[2:]: (parmkey, parmval) = param.split("=") startrequest['parameters'][parmkey] = parmval tlvdata.send( session.connection, startrequest) status = tlvdata.recv(session.connection) if 'error' in status: if 'errorcode' in status: exitcode = status['errorcode'] sys.stderr.write('Error: ' + status['error'] + '\n') while '_requestdone' not in status: status = tlvdata.recv(session.connection) return startconsole(nodename) return elif argv[0] == 'set': setvalues(argv[1:]) elif argv[0] == 'create': createresource(argv[1:]) elif argv[0] in ('rm', 'delete', 'remove'): delresource(argv[1]) elif argv[0] in ('unset', 'clear'): clearvalues(argv[1], argv[2:]) elif argv[0] == 'shutdown': shutdown() else: sys.stderr.write("%s: command not found...\n" % argv[0]) def shutdown(): tlvdata.send(session.connection, {'operation': 'shutdown', 'path': '/'}) def createresource(args): resname = args[0] attribs = args[1:] keydata = parameterize_attribs(attribs) if keydata is None: return targpath = fullpath_target(resname) if targpath.startswith('/noderange//'): collection = targpath else: collection, _, resname = targpath.rpartition('/') keydata['name'] = resname makecall(session.create, (collection, keydata)) def makecall(callout, args): global exitcode for response in callout(*args): if 'deleted' in response: print("Deleted: " + response['deleted']) if 'created' in response: print("Created: " + response['created']) if 'error' in response: if 'errorcode' in response: exitcode = response['errorcode'] sys.stderr.write('Error: ' + response['error'] + '\n') def clearvalues(resource, attribs): global exitcode targpath = fullpath_target(resource) keydata = {} for attrib in attribs: keydata[attrib] = None for res in session.update(targpath, keydata): if 'error' in res: if 'errorcode' in res: exitcode = res['errorcode'] sys.stderr.write('Error: ' + res['error'] + '\n') def delresource(resname): resname = fullpath_target(resname) makecall(session.delete, (resname,)) def setvalues(attribs): global exitcode 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 = parameterize_attribs(attribs) if not keydata: return targpath = fullpath_target(resource) for res in session.update(targpath, keydata): if 'error' in res: if 'errorcode' in res: exitcode = res['errorcode'] sys.stderr.write('Error: ' + res['error'] + '\n') print_result(res) def parameterize_attribs(attribs): keydata = {} for attrib in attribs: if '=' not in attrib: sys.stderr.write("Invalid syntax %s\n" % attrib) return None key = attrib[:attrib.index("=")] value = attrib[attrib.index("=") + 1:] if key == 'groups': value = value.split(',') keydata[key] = value return keydata def fullpath_target(currpath, forcepath=False): global target if currpath == '': return target pathcomponents = currpath.split("/") if pathcomponents[-1] == "": # preserve path forcepath = True if pathcomponents[0] == "": # absolute path ntarget = currpath 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 (len(targparts) == 0 or targparts[-1] != ""): targparts.append('') ntarget = '/'.join(targparts) if forcepath and (len(ntarget) == 0 or ntarget[-1] != '/'): ntarget += '/' return ntarget def startconsole(nodename): global inconsole global consolename global didconsole didconsole = True consolename = nodename 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 quitconfetty(code=0, fullexit=False, fixterm=True): global inconsole global currconsole global didconsole if fixterm or didconsole: currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL) fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl ^ os.O_NONBLOCK) if oldtcattr is not None: termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr) # Request default color scheme, to undo potential weirdness of terminal sys.stdout.write('\x1b[m') if fullexit: if os.environ.get('TERM', '') not in ('linux'): sys.stdout.write('\x1b]0;\x07') sys.exit(code) else: tlvdata.send(session.connection, {'operation': 'stop', 'path': currconsole}) inconsole = False def get_session_node(shellargs): # straight to node console if len(shellargs) == 1 and ' ' not in shellargs[0]: return shellargs[0] if len(shellargs) == 2 and shellargs[0] == 'start': args = [s for s in shellargs[1].split('/') if s] if len(args) == 4 and args[0] == 'nodes' and args[2] == 'console' and \ args[3] == 'session': return args[1] return None def conserver_command(filehandle, localcommand): # x - conserver has that as 'show baud', I am inclined to replace that with # 'request exclusive' # b - conserver has that as 'broadcast message', I'm tempted to use that # for break # r - replay # p - replay (this is the one I've always used) # f - force attach read/write # a - attach read/write # s - spy mode # l[n] - send a particular break, tempted to do l0 for compatibility # o - reopen tty, this should mean reconnect console # d - down a console... never used this... # L - toggle logging # w - who is on console cmdlen = 1 localcommand = get_command_bytes(filehandle, localcommand, cmdlen) if localcommand[0] == '.': print("disconnect]\r") quitconfetty(fullexit=consoleonly) elif localcommand[0] == 'o': tlvdata.send(session.connection, {'operation': 'reopen', 'path': currconsole}) print('reopen]\r') elif localcommand[0] == 'b': tlvdata.send(session.connection, {'operation': 'break', 'path': currconsole}) print("break sent]\r") elif localcommand[0] == 'p': # power cmdlen += 1 localcommand = get_command_bytes(filehandle, localcommand, cmdlen) if localcommand[1] == 'o': # off print("powering off...") session.simple_noderange_command(consolename, '/power/state', 'off') print("complete]\r") elif localcommand[1] == 's': # shutdown print("shutting down...") session.simple_noderange_command(consolename, '/power/state', 'shutdown') print("complete]\r") elif localcommand[1] == 'b': # boot cmdlen += 1 localcommand = get_command_bytes(filehandle, localcommand, cmdlen) if localcommand[2] == 's': # boot to setup print("booting to setup...") bootmode = 'uefi' bootdev = 'setup' rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode) if rc: print("Error]\r") else: rc = session.simple_noderange_command(consolename, '/power/state', 'boot') if rc: print("Error]\r") else: print("complete]\r") elif localcommand[2] == 'n': # boot to network print("booting to network...") bootmode = 'uefi' bootdev = 'network' rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode) if rc: print("Error]\r") else: rc = session.simple_noderange_command(consolename, '/power/state', 'boot') if rc: print("Error]\r") else: print("complete]\r") elif localcommand[2] == '\x0d': # boot to default print("booting to default...") bootmode = 'uefi' bootdev = 'default' rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode) if rc: print("Error]\r") else: rc = session.simple_noderange_command(consolename, '/power/state', 'boot') if rc: print("Error]\r") else: print("complete]\r") else: print("Unknown boot state.]\r") else: print("Unknown power state.]\r") #check_power_state() elif localcommand[0] == '?': print("help]\r") print(". exit console\r") print("b break\r") print("o reopen\r") print("po power off\r") print("ps shutdown\r") print("pbs boot to setup\r") print("pbn boot to network\r") print("pb boot to default\r") print(" abort command\r") elif localcommand[0] == '\x0d': print("ignored]\r") else: # not a command at all.. print("unknown -- use '?']\r") def get_command_bytes(filehandle, localcommand, cmdlen): while len(localcommand) < cmdlen: ready, _, _ = select.select((filehandle,), (), (), 1) if ready: localcommand += filehandle.read() return localcommand def check_escape_seq(currinput, filehandle): while conserversequence.startswith(currinput): if currinput.startswith(conserversequence): # We have full sequence sys.stdout.write("[") sys.stdout.flush() return conserver_command( filehandle, currinput[len(conserversequence):]) ready, _, _ = select.select((filehandle,), (), (), 3) if not ready: # 3 seconds of no typing break currinput += filehandle.read() return currinput parser = optparse.OptionParser() parser.add_option("-s", "--server", dest="netserver", help="Confluent instance to connect to", metavar="SERVER:PORT") parser.add_option("-c", "--control", dest="controlpath", help="Path to offer terminal control", metavar="PATH") opts, shellargs = parser.parse_args() username = None passphrase = None def server_connect(): global session, username, passphrase if opts.controlpath: termhandler.TermHandler(opts.controlpath) if opts.netserver: # going over a TLS network session = client.Command(opts.netserver) elif 'CONFLUENT_HOST' in os.environ: session = client.Command(os.environ['CONFLUENT_HOST']) else: # unix domain session = client.Command() # Next stop, reading and writing from whichever of stdin and server goes first. #see pyghmi code for solconnect.py if not session.authenticated and username is not None: session.authenticate(username, passphrase) if not session.authenticated and 'CONFLUENT_USER' in os.environ: username = os.environ['CONFLUENT_USER'] passphrase = os.environ['CONFLUENT_PASSPHRASE'] session.authenticate(username, passphrase) while not session.authenticated: username = raw_input("Name: ") passphrase = getpass.getpass("Passphrase: ") session.authenticate(username, passphrase) try: server_connect() except EOFError, KeyboardInterrupt: sys.exit(0) except socket.gaierror: sys.stderr.write('Could not connect to confluent\n') sys.exit(1) # 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() if sys.stdout.isatty(): import readline readline.parse_and_bind("tab: complete") readline.parse_and_bind("set bell-style none") dl = readline.get_completer_delims().replace('-', '') readline.set_completer_delims(dl) readline.set_completer(completer) doexit = False inconsole = False pendingcommand = "" session_node = get_session_node(shellargs) if session_node is not None: consoleonly = True do_command("start /nodes/%s/console/session" % session_node, netserver) doexit = True elif shellargs: command = " ".join(shellargs) do_command(command, netserver) quitconfetty(fullexit=True, fixterm=False) powerstate = None powertime = None def check_power_state(): global powerstate, powertime for rsp in session.read('/nodes/' + consolename + '/power/state'): if type(rsp) == dict and 'state' in rsp: newpowerstate = rsp['state']['value'] powertime = time.time() if newpowerstate != powerstate and newpowerstate == 'off': sys.stdout.write("\x1b[2J\x1b[;H[powered off]\r\n") powerstate = newpowerstate elif type(rsp) == dict and '_requestdone' in rsp: break elif type(rsp) == dict: updatestatus(rsp) else: sys.stdout.write(rsp) sys.stdout.flush() while inconsole or not doexit: if inconsole: rdylist, _, _ = select.select( (sys.stdin, session.connection), (), (), 60) for fh in rdylist: if fh == session.connection: # this only should get called in the # case of a console session # each command should slurp up all relevant # recv potential #fh.read() try: data = tlvdata.recv(fh) except Exception: data = None if type(data) == dict: updatestatus(data) continue if data is not None: try: sys.stdout.write(data) except IOError: # Some times circumstances are bad # resort to byte at a time... for d in data: sys.stdout.write(d) now = time.time() if ('showtime' not in laststate or (now // 60) != laststate['showtime'] // 60): # don't bother churning if minute does not change laststate['showtime'] = now updatestatus() sys.stdout.flush() else: deadline = 5 connected = False while not connected and deadline > 0: try: server_connect() connected = True except (socket.gaierror, socket.error): pass if not connected: time.sleep(1) deadline -=1 if connected: do_command( "start /nodes/%s/console/session skipreplay=True" % consolename, netserver) else: doexit = True inconsole = False sys.stdout.write("\r\n[remote disconnected]\r\n") break else: try: myinput = fh.read() myinput = check_escape_seq(myinput, fh) if myinput: tlvdata.send(session.connection, myinput) except IOError: pass #if powerstate is None or powertime < time.time() - 60: # Check powerstate every 60 seconds # check_power_state() else: currcommand = prompt() try: do_command(currcommand, netserver) except socket.error: try: server_connect() do_command(currcommand, netserver) except socket.error: doexit = True sys.stdout.write('Lost connection to server') quitconfetty(fullexit=True)