#!/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 clearpowermessage = False 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={}): global powerstate, powertime, clearpowermessage 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 'state' in stateinfo: # currently only read power means anything newpowerstate = stateinfo['state']['value'] if newpowerstate != powerstate and newpowerstate == 'off': sys.stdout.write("\x1b[2J\x1b[;H[powered off]\r\n") clearpowermessage = True if newpowerstate == 'on' and clearpowermessage: sys.stdout.write("\x1b[2J\x1b[;H") clearpowermessage = False powerstate = newpowerstate 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] == 'r': sys.stdout.write('\x1b7\x1b[999;999H\x1b[6n') sys.stdout.flush() reply = '' while 'R' not in reply: try: reply += sys.stdin.read(1) except IOError: pass reply = reply.replace('\x1b[', '') reply = reply.replace('R', '') height, width = reply.split(';') sys.stdout.write('\x1b8') sys.stdout.flush() print('sending stty commands]') return 'stty columns {0}\rstty rows {1}\r'.format(width, height) 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("r send stty command to resize terminal\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: try: ready, _, _ = select.select((filehandle,), (), (), 1) except select.error: ready = () 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):]) try: ready, _, _ = select.select((filehandle,), (), (), 3) except select.error: ready = () 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(): tlvdata.send( session.connection, {'operation': 'retrieve', 'path': '/nodes/' + consolename + '/power/state'}) return while inconsole or not doexit: if inconsole: try: rdylist, _, _ = select.select( (sys.stdin, session.connection), (), (), 10) except select.error: rdylist = () 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: if clearpowermessage: sys.stdout.write("\x1b[2J\x1b[;H") clearpowermessage = False 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() - 10: # Check powerstate every 10 seconds powertime = time.time() powerstate = True 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)