
- This server opens an unix socket server that listens to clients insterested in use a guest serial console. - Each connection is isolated in its own process, that is destroyed when the client disconnects (timeout/ctrl+q/browser tab closed). Such isolation avoids any block operation on the main process. - Websockify is the responsible to receive the connection from the client websocket to the local unix socket server. Signed-off-by: Jose Ricardo Ziviani <joserz@linux.vnet.ibm.com> --- serial_console.py | 290 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 serial_console.py diff --git a/serial_console.py b/serial_console.py new file mode 100644 index 0000000..b0659a5 --- /dev/null +++ b/serial_console.py @@ -0,0 +1,290 @@ +import libvirt +import os +import socket +import sys +import threading +import time + +from multiprocessing import Process + + +from wok.utils import wok_log +from wok.plugins.kimchi import model + + +SOCKET_QUEUE_BACKLOG = 0 +DEFAULT_TIMEOUT = 120 # seconds +CTRL_Q = '\x11' + + +class socket_server(object): + """Unix socket server for guest console access. + + Implements a unix socket server for each guest, this server will receive + data from a particular client, forward that data to the guest console, + receive the response from the console and send the response back to the + client. + + Features: + - one socket server per client connection; + - server listens to unix socket; + - exclusive connection per guest; + - websockity handles the proxy between the client websocket to the + local unix socket; + + Note: + - old versions (< 0.6.0)of websockify don't handle their children + processes accordingly, leaving a zombie process behind (this also + happens with novnc). + """ + + def __init__(self, guest_name): + """Constructs a unix socket server. + + Listens to connections on /tmp/<guest name>. + """ + self._guest_name = guest_name + self._server_addr = '/tmp/%s' % guest_name + if os.path.exists(self._server_addr): + wok_log.error('Cannot connect to %s due to an existing ' + 'connection', guest_name) + raise RuntimeError('There is an existing connection to %s' % + guest_name) + + self._socket = socket.socket(socket.AF_UNIX, + socket.SOCK_STREAM) + self._socket.setsockopt(socket.SOL_SOCKET, + socket.SO_REUSEADDR, + 1) + self._socket.bind(self._server_addr) + self._socket.listen(SOCKET_QUEUE_BACKLOG) + self._stop = False + wok_log.info('socket server to guest %s created', guest_name) + + def _send_to_client(self, stream, event, opaque): + """Handles libvirt stream readable events. + + Each event will be send back to the client socket. + """ + try: + data = stream.recv(1024) + + except Exception as e: + wok_log.info('Error when reading from console: %s', e.message) + return + + # return if no data received or client socket(opaque) is not valid + if not data or not opaque: + return + + opaque.send(data) + + def libvirt_event_loop(self, guest, client): + """Runs libvirt event loop. + """ + # stop the event loop when the guest is not running + while guest.is_running(): + libvirt.virEventRunDefaultImpl() + + # shutdown the client socket to unblock the recv and stop the + # server as soon as the guest shuts down + client.shutdown(socket.SHUT_RD) + + def listen(self): + """Prepares the environment before starts to accept connections + + Initializes and destroy the resources needed to accept connection. + """ + libvirt.virEventRegisterDefaultImpl() + try: + guest = libvirt_guest(self._guest_name) + + except Exception as e: + wok_log.error('Cannot open the guest %s due to %s', + self._guest_name, e.message) + self._socket.close() + sys.exit(1) + + except (KeyboardInterrupt, SystemExit): + self._socket.close() + sys.exit(1) + + console = None + try: + console = guest.get_console() + self._listen(guest, console) + + # clear resources aquired when the process is killed + except (KeyboardInterrupt, SystemExit): + pass + + finally: + wok_log.info("Shutting down the socket server to %s console", + self._guest_name) + self._socket.close() + if os.path.exists(self._server_addr): + os.unlink(self._server_addr) + + try: + console.eventRemoveCallback() + + except Exception as e: + wok_log.info('Callback is probably removed: %s', e.message) + + guest.close() + + def _listen(self, guest, console): + """Accepts client connections. + + Each connection is directly linked to the desired guest console. Thus + any data received from the client can be send to the guest console as + well as any response from the guest console can be send back to the + client console. + """ + client, client_addr = self._socket.accept() + client.settimeout(DEFAULT_TIMEOUT) + wok_log.info('Client %s connected to %s', + str(client_addr), + self._guest_name) + + # register the callback to receive any data from the console + console.eventAddCallback(libvirt.VIR_STREAM_EVENT_READABLE, + self._send_to_client, + client) + + # start the libvirt event loop in a python thread + libvirt_loop = threading.Thread(target=self.libvirt_event_loop, + args=(guest, client)) + libvirt_loop.start() + + while True: + data = '' + try: + data = client.recv(1024) + + except Exception as e: + wok_log.info('Client %s disconnected from %s: %s', + str(client_addr), + self._guest_name, + e.message) + break + + if not data or data == CTRL_Q: + break + + # if the console can no longer be accessed, close everything + # and quits + try: + console.send(data) + + except: + wok_log.info('Console of %s is not accessible', + self._guest_name) + break + + # clear used resources when the connection is closed and, if possible, + # tell the client the connection was lost. + try: + client.send('\r\n\r\nClient disconnected\r\n') + + except: + pass +# socket_server + + +class libvirt_guest(object): + + def __init__(self, guest_name): + """ + Constructs a guest object that opens a connection to libvirt and + searchs for a particular guest, provided by the caller. + """ + try: + libvirt = model.libvirtconnection.LibvirtConnection( + 'qemu:///system') + self._guest = model.vms.VMModel.get_vm(guest_name, libvirt) + + except Exception as e: + wok_log.error('Cannot open guest %s: %s', guest_name, e.message) + raise + + self._libvirt = libvirt.get() + self._name = guest_name + self._stream = None + + def get_name(self): + return self._name + + def is_running(self): + """ + Checks if this guest is currently in a running state. + """ + return self._guest.state(0)[0] == libvirt.VIR_DOMAIN_RUNNING or \ + self._guest.state(0)[0] == libvirt.VIR_DOMAIN_PAUSED + + def get_console(self): + """ + Opens a console to this guest and returns a reference to it. + Note: If another instance (eg: virsh) has an existing console opened + to this guest, this code will steal that console. + """ + # guest must be in a running state to get its console + counter = 10 + while not self.is_running(): + wok_log.info('Guest %s is not running, waiting for it', + self._name) + + counter -= 1 + if counter <= 0: + return None + + time.sleep(1) + + # attach a stream in the guest console so we can read from/write to it + if self._stream is None: + wok_log.info('Opening the console for guest %s', + self._name) + self._stream = self._libvirt.newStream(libvirt.VIR_STREAM_NONBLOCK) + self._guest.openConsole(None, + self._stream, + libvirt.VIR_DOMAIN_CONSOLE_FORCE | + libvirt.VIR_DOMAIN_CONSOLE_SAFE) + return self._stream + + def close(self): + """Closes the libvirt connection. + """ + self._libvirt.close() +# guest + + +def main(guest_name): + """Main entry point to create a socket server. + + Starts a new socket server to listen messages to/from the guest. + """ + server = None + try: + server = socket_server(guest_name) + + except Exception as e: + wok_log.error('Cannot create the socket server for %s due to %s', + guest_name, e.message) + raise + + proc = Process(target=server.listen) + proc.start() + return proc + + +if __name__ == '__main__': + """Executes a stand alone instance of the socket server. + + This may be useful for testing/debugging. + """ + argc = len(sys.argv) + if argc != 2: + print 'usage: ./%s <guest_name>' % sys.argv[0] + sys.exit(1) + + main(sys.argv[1]) -- 1.9.1