[Kimchi-devel] [PATCH v3] [Kimchi 2/8] Implement the web serial console server

Jose Ricardo Ziviani joserz at linux.vnet.ibm.com
Thu Feb 11 13:47:04 UTC 2016


 - 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 at linux.vnet.ibm.com>
---
 serialconsole.py | 315 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 315 insertions(+)
 create mode 100644 serialconsole.py

diff --git a/serialconsole.py b/serialconsole.py
new file mode 100644
index 0000000..94aaa6f
--- /dev/null
+++ b/serialconsole.py
@@ -0,0 +1,315 @@
+#
+# Project Kimchi
+#
+# Copyright IBM, Corp. 2016
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
+#
+
+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 SocketServer(Process):
+    """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, URI):
+        """Constructs a unix socket server.
+
+        Listens to connections on /tmp/<guest name>.
+        """
+        Process.__init__(self)
+
+        self._guest_name = guest_name
+        self._uri = URI
+        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)
+        wok_log.info('socket server to guest %s created', guest_name)
+
+    def run(self):
+        """Implements customized run method from Process.
+        """
+        self.listen()
+
+    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 = LibvirtGuest(self._guest_name, self._uri)
+
+        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 LibvirtGuest(object):
+
+    def __init__(self, guest_name, uri):
+        """
+        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(uri)
+            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, URI):
+    """Main entry point to create a socket server.
+
+    Starts a new socket server to listen messages to/from the guest.
+    """
+    server = None
+    try:
+        server = SocketServer(guest_name, URI='qemu:///system')
+
+    except Exception as e:
+        wok_log.error('Cannot create the socket server for %s due to %s',
+                      guest_name, e.message)
+        raise
+
+    server.start()
+    return server
+
+
+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




More information about the Kimchi-devel mailing list