2013-09-12 16:01:39 -04:00
|
|
|
# Copyright 2013 IBM Corporation
|
|
|
|
# All rights reserved
|
|
|
|
|
|
|
|
# This is the common console support for confluent. It takes over
|
|
|
|
# whatever filehandle is conversing with the client and starts
|
|
|
|
# relaying data. It uses Ctrl-] like telnet for escape back to prompt
|
|
|
|
|
|
|
|
#we track nodes that are actively being logged, watched, or have attached
|
|
|
|
#there should be no more than one handler per node
|
2014-02-01 18:49:36 -05:00
|
|
|
import confluent.interface.console as conapi
|
2014-03-09 20:34:39 -04:00
|
|
|
import confluent.log as log
|
2013-09-12 16:01:39 -04:00
|
|
|
import confluent.pluginapi as plugin
|
2013-09-20 14:36:55 -04:00
|
|
|
import eventlet
|
2014-02-02 19:16:59 -05:00
|
|
|
import eventlet.green.threading as threading
|
2013-09-20 14:36:55 -04:00
|
|
|
import random
|
2013-09-12 16:54:39 -04:00
|
|
|
|
2013-09-12 16:01:39 -04:00
|
|
|
_handled_consoles = {}
|
|
|
|
|
2014-02-06 13:13:16 -05:00
|
|
|
|
2013-09-12 16:01:39 -04:00
|
|
|
class _ConsoleHandler(object):
|
|
|
|
def __init__(self, node, configmanager):
|
2014-02-01 09:39:57 -05:00
|
|
|
self.rcpts = {}
|
2014-02-01 18:49:36 -05:00
|
|
|
self.cfgmgr = configmanager
|
|
|
|
self.node = node
|
2014-03-09 20:34:39 -04:00
|
|
|
self.logger = log.Logger(node, tenant=configmanager.tenant)
|
2013-09-15 13:22:50 -04:00
|
|
|
self.buffer = bytearray()
|
2014-02-01 18:49:36 -05:00
|
|
|
self._connect()
|
2014-03-10 13:15:31 -04:00
|
|
|
self.users = {}
|
2014-02-01 18:49:36 -05:00
|
|
|
|
|
|
|
def _connect(self):
|
|
|
|
self._console = plugin.handle_path(
|
|
|
|
"/node/%s/_console/session" % self.node,
|
|
|
|
"create", self.cfgmgr)
|
2013-09-12 16:01:39 -04:00
|
|
|
self._console.connect(self.get_console_output)
|
2013-09-20 14:36:55 -04:00
|
|
|
|
|
|
|
def unregister_rcpt(self, handle):
|
|
|
|
if handle in self.rcpts:
|
|
|
|
del self.rcpts[handle]
|
2013-09-12 16:54:39 -04:00
|
|
|
|
|
|
|
def register_rcpt(self, callback):
|
2013-09-20 14:36:55 -04:00
|
|
|
hdl = random.random()
|
|
|
|
while hdl in self.rcpts:
|
|
|
|
hdl = random.random()
|
|
|
|
self.rcpts[hdl] = callback
|
|
|
|
return hdl
|
2013-09-12 16:01:39 -04:00
|
|
|
|
2013-09-15 13:22:50 -04:00
|
|
|
def flushbuffer(self):
|
|
|
|
#TODO:log the old stuff
|
2013-09-20 14:36:55 -04:00
|
|
|
if len(self.buffer) > 1024:
|
|
|
|
self.buffer = bytearray(self.buffer[-1024:])
|
2013-09-15 13:22:50 -04:00
|
|
|
#Will be interesting to keep track of logged but
|
|
|
|
#retained data, must only log data not already
|
|
|
|
#flushed
|
|
|
|
#also, timestamp data...
|
|
|
|
|
2013-09-12 16:01:39 -04:00
|
|
|
def get_console_output(self, data):
|
2014-02-01 18:49:36 -05:00
|
|
|
# Spawn as a greenthread, return control as soon as possible
|
|
|
|
# to the console object
|
|
|
|
eventlet.spawn(self._handle_console_output, data)
|
|
|
|
|
2014-03-09 20:34:39 -04:00
|
|
|
def attachuser(self, username):
|
2014-03-10 13:15:31 -04:00
|
|
|
if username in self.users:
|
|
|
|
self.users[username] += 1
|
|
|
|
else:
|
|
|
|
self.users[username] = 1
|
2014-03-09 20:34:39 -04:00
|
|
|
self.logger.log(
|
|
|
|
logdata=username, ltype=log.DataTypes.event,
|
2014-03-10 13:15:31 -04:00
|
|
|
event=log.Events.clientconnect,
|
|
|
|
eventdata=self.users[username])
|
2014-03-09 20:34:39 -04:00
|
|
|
|
|
|
|
def detachuser(self, username):
|
2014-03-10 13:15:31 -04:00
|
|
|
self.users[username] -= 1
|
2014-03-09 20:34:39 -04:00
|
|
|
self.logger.log(
|
|
|
|
logdata=username, ltype=log.DataTypes.event,
|
2014-03-10 13:15:31 -04:00
|
|
|
event=log.Events.clientdisconnect,
|
|
|
|
eventdata=self.users[username])
|
2014-03-09 20:34:39 -04:00
|
|
|
|
2014-02-01 18:49:36 -05:00
|
|
|
def _handle_console_output(self, data):
|
|
|
|
if type(data) == int:
|
|
|
|
if data == conapi.ConsoleEvent.Disconnect:
|
|
|
|
self._connect()
|
|
|
|
return
|
2014-03-09 20:34:39 -04:00
|
|
|
self.logger.log(data)
|
2013-09-16 16:57:27 -04:00
|
|
|
self.buffer += data
|
2013-09-15 13:22:50 -04:00
|
|
|
#TODO: analyze buffer for registered events, examples:
|
|
|
|
# panics
|
|
|
|
# certificate signing request
|
2013-09-20 14:36:55 -04:00
|
|
|
if len(self.buffer) > 8192:
|
2013-09-15 13:22:50 -04:00
|
|
|
#call to function to get generic data to log if applicable
|
|
|
|
#and shrink buffer
|
|
|
|
self.flushbuffer()
|
2013-09-20 14:36:55 -04:00
|
|
|
for rcpt in self.rcpts.itervalues():
|
2013-10-10 14:17:08 -04:00
|
|
|
try:
|
|
|
|
rcpt(data)
|
|
|
|
except:
|
|
|
|
pass
|
2013-09-12 16:54:39 -04:00
|
|
|
|
2013-09-15 13:22:50 -04:00
|
|
|
def get_recent(self):
|
2013-09-20 14:36:55 -04:00
|
|
|
"""Retrieve 'recent' data
|
|
|
|
|
|
|
|
Replay data in the intent to perhaps reproduce the display.
|
|
|
|
"""
|
|
|
|
#For now, just try to seek back in buffer to find a clear screen
|
|
|
|
#If that fails, just return buffer
|
|
|
|
#a scheme always tracking the last clear screen would be too costly
|
|
|
|
#an alternative would be to emulate a VT100 to know what the
|
|
|
|
#whole screen would look like
|
2013-09-15 13:22:50 -04:00
|
|
|
#this is one scheme to clear screen, move cursor then clear
|
2013-09-20 14:36:55 -04:00
|
|
|
bufidx = self.buffer.rfind('\x1b[H\x1b[J')
|
2013-09-15 13:22:50 -04:00
|
|
|
if bufidx >= 0:
|
2013-09-16 16:57:27 -04:00
|
|
|
return str(self.buffer[bufidx:])
|
|
|
|
#another scheme is the 2J scheme
|
2013-09-20 14:36:55 -04:00
|
|
|
bufidx = self.buffer.rfind('\x1b[2J')
|
2013-09-15 13:22:50 -04:00
|
|
|
if bufidx >= 0:
|
2013-09-20 14:36:55 -04:00
|
|
|
# there was some sort of clear screen event
|
|
|
|
# somewhere in the buffer, replay from that point
|
|
|
|
# in hopes that it reproduces the screen
|
2013-09-16 16:57:27 -04:00
|
|
|
return str(self.buffer[bufidx:])
|
2013-09-15 13:22:50 -04:00
|
|
|
else:
|
2013-09-20 14:36:55 -04:00
|
|
|
#we have no indication of last erase, play back last kibibyte
|
|
|
|
#to give some sense of context anyway
|
|
|
|
return str(self.buffer[-1024:])
|
2013-09-15 13:22:50 -04:00
|
|
|
|
2013-09-12 16:54:39 -04:00
|
|
|
def write(self, data):
|
|
|
|
#TODO.... take note of data coming in from audit/log perspective?
|
|
|
|
#or just let echo take care of it and then we can skip this stack
|
|
|
|
#level?
|
|
|
|
self._console.write(data)
|
2013-09-12 16:01:39 -04:00
|
|
|
|
2014-02-06 13:13:16 -05:00
|
|
|
|
2013-09-12 16:01:39 -04:00
|
|
|
#this represents some api view of a console handler. This handles things like
|
|
|
|
#holding the caller specific queue data, for example, when http api should be
|
2013-09-13 16:07:39 -04:00
|
|
|
#sending data, but there is no outstanding POST request to hold it,
|
2013-09-12 16:54:39 -04:00
|
|
|
# this object has the job of holding the data
|
2013-09-12 16:01:39 -04:00
|
|
|
class ConsoleSession(object):
|
|
|
|
"""Create a new socket to converse with node console
|
|
|
|
|
|
|
|
This object provides a filehandle that can be read/written
|
|
|
|
too in a normal fashion and the concurrency, logging, and
|
|
|
|
event watching will all be handled seamlessly
|
|
|
|
|
|
|
|
:param node: Name of the node for which this session will be created
|
|
|
|
"""
|
|
|
|
|
2014-03-09 20:34:39 -04:00
|
|
|
def __init__(self, node, configmanager, username, datacallback=None):
|
2014-02-07 18:59:32 -05:00
|
|
|
self.tenant = configmanager.tenant
|
|
|
|
consk = (node, self.tenant)
|
|
|
|
self.ckey = consk
|
2014-03-09 20:34:39 -04:00
|
|
|
self.username = username
|
2014-02-07 18:59:32 -05:00
|
|
|
if consk not in _handled_consoles:
|
|
|
|
_handled_consoles[consk] = _ConsoleHandler(node, configmanager)
|
2014-03-09 20:34:39 -04:00
|
|
|
_handled_consoles[consk].attachuser(username)
|
2014-02-02 19:16:59 -05:00
|
|
|
self._evt = threading.Event()
|
2013-09-20 14:36:55 -04:00
|
|
|
self.node = node
|
2014-02-07 18:59:32 -05:00
|
|
|
self.conshdl = _handled_consoles[consk]
|
|
|
|
self.write = _handled_consoles[consk].write
|
2013-09-14 11:08:48 -04:00
|
|
|
if datacallback is None:
|
2013-09-20 14:36:55 -04:00
|
|
|
self.reaper = eventlet.spawn_after(15, self.destroy)
|
2014-02-07 18:59:32 -05:00
|
|
|
self.databuffer = _handled_consoles[consk].get_recent()
|
|
|
|
self.reghdl = _handled_consoles[consk].register_rcpt(self.got_data)
|
2013-09-14 11:08:48 -04:00
|
|
|
else:
|
2014-02-07 18:59:32 -05:00
|
|
|
self.reghdl = _handled_consoles[consk].register_rcpt(datacallback)
|
|
|
|
recdata = _handled_consoles[consk].get_recent()
|
2013-10-09 20:14:34 -04:00
|
|
|
if recdata:
|
|
|
|
datacallback(recdata)
|
2013-09-12 16:54:39 -04:00
|
|
|
|
2013-09-20 14:36:55 -04:00
|
|
|
def destroy(self):
|
2014-03-09 20:34:39 -04:00
|
|
|
_handled_consoles[self.ckey].detachuser(self.username)
|
2014-02-07 18:59:32 -05:00
|
|
|
_handled_consoles[self.ckey].unregister_rcpt(self.reghdl)
|
2013-09-20 14:36:55 -04:00
|
|
|
self.databuffer = None
|
2014-02-02 19:16:59 -05:00
|
|
|
self._evt = None
|
2013-09-20 14:36:55 -04:00
|
|
|
self.reghdl = None
|
|
|
|
|
2013-09-12 16:54:39 -04:00
|
|
|
def got_data(self, data):
|
2013-09-14 11:08:48 -04:00
|
|
|
"""Receive data from console and buffer
|
|
|
|
|
|
|
|
If the caller does not provide a callback and instead will be polling
|
|
|
|
for data, we must maintain data in a buffer until retrieved
|
|
|
|
"""
|
2013-09-16 16:57:27 -04:00
|
|
|
self.databuffer += data
|
2014-02-02 19:16:59 -05:00
|
|
|
self._evt.set()
|
2013-09-12 16:54:39 -04:00
|
|
|
|
|
|
|
def get_next_output(self, timeout=45):
|
|
|
|
"""Poll for next available output on this console.
|
|
|
|
|
|
|
|
Ideally purely event driven scheme is perfect. AJAX over HTTP is
|
|
|
|
at least one case where we don't have that luxury
|
|
|
|
"""
|
2013-09-20 14:36:55 -04:00
|
|
|
self.reaper.cancel()
|
2014-02-06 13:13:16 -05:00
|
|
|
if len(self.databuffer) == 0:
|
|
|
|
self._evt.wait(timeout)
|
2013-09-16 16:57:27 -04:00
|
|
|
retval = self.databuffer
|
|
|
|
self.databuffer = ""
|
2014-02-06 09:27:38 -05:00
|
|
|
if self._evt is not None:
|
|
|
|
self._evt.clear()
|
2013-09-20 14:36:55 -04:00
|
|
|
self.reaper = eventlet.spawn_after(15, self.destroy)
|
2013-09-16 16:57:27 -04:00
|
|
|
return retval
|
2013-09-12 16:01:39 -04:00
|
|
|
|
|
|
|
|
|
|
|
def handle_request(request=None, connection=None, releaseconnection=False):
|
|
|
|
"""
|
|
|
|
Process a request from confluent.
|
|
|
|
|
|
|
|
:param request: For 'datagram' style console, this represents a wait for
|
2013-09-16 16:57:27 -04:00
|
|
|
data or input.
|
2013-09-12 16:01:39 -04:00
|
|
|
:param connection: For socket style console, this is a read/write socket
|
|
|
|
that the caller has released from it's control and
|
|
|
|
console plugin will do all IO
|
2014-02-06 13:13:16 -05:00
|
|
|
:param releaseconnection: A function for console to call to indicate
|
|
|
|
confluent should resume control over the connection
|
2013-09-12 16:01:39 -04:00
|
|
|
|
|
|
|
"""
|
|
|
|
if request is not None: # This is indicative of http style
|
|
|
|
pass # TODO(jbjohnso): support AJAX style interaction.
|
|
|
|
# a web ui looking to actually take advantage will
|
|
|
|
# probably have to pull in the GPL javascript
|
|
|
|
# from shellinabox or something similar
|
|
|
|
# the way that works is URI encoded input with width, heiht,
|
|
|
|
# session or rooturl:opening
|
|
|
|
# opening session
|
|
|
|
# width=120&height=24&rooturl=/nodes/n1/console/session
|
|
|
|
# making a request to wait for data:
|
|
|
|
# width=120&height=24&session=blahblahblah
|
|
|
|
# <hitting enter>:
|
|
|
|
# width=120&height=24&session=blahblahblah&keys=0D
|
|
|
|
# pasting 'rabbit'
|
|
|
|
# width=120&height=24&session=blahblah&keys=726162626974
|
|
|
|
# if no client session indicated, it expects some session number
|
|
|
|
# in return.
|
|
|
|
# the responses:
|
|
|
|
# <responding to session open>: (the session seems to be opaque
|
|
|
|
# {"session":"h5lrOKViIeQGp1nXjKWpAQ","data":""}
|
2014-02-06 13:13:16 -05:00
|
|
|
# <responding to wait for data with data, specically a prompt
|
|
|
|
# that sets title>
|
|
|
|
# {"session":"blah","data":"\r\n\u001B]0;bob@thor:~\u0007$ "}
|
2013-09-12 16:01:39 -04:00
|
|
|
# <responding to wait with no data (seems to wait 46 seconds)
|
|
|
|
# {"session":"jSGBPmAxavsD/1acSl/uog","data":""}
|
|
|
|
# closed session returns HTTP 400 to a console answer
|
|
|
|
elif connection is not None: # This is a TLS or unix socket
|
2014-02-06 13:13:16 -05:00
|
|
|
ConsoleSession(connection, releaseconnection)
|