2
0
mirror of https://github.com/xcat2/confluent.git synced 2024-11-26 03:19:48 +00:00
confluent/bin/confetty
Jarrod Johnson 11138aa3de Fix confetty interactive behavior after set
After set, confetty client would fail to follow protocol and
end up leaving followup commands in an off by one state.  One
TODO out of this is to refactor send_request into a library function
2014-02-22 20:22:23 -05:00

448 lines
14 KiB
Python
Executable File

#!/usr/bin/env python
# ultimately, this should provide an interactive cli for navigating confluent
# tree and doing console with a socket. <ESC>]0;<string><BELL> 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("=")]
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')
return
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("<cr> 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()