- 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(a)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