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

Aline Manera alinefm at linux.vnet.ibm.com
Fri Feb 5 16:20:55 UTC 2016



On 02/04/2016 05:47 PM, Jose Ricardo Ziviani wrote:
>   - 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>
> ---
>   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

License header is missing.

I also suggest to rename the file to serialconsole.py (without 
underscore) to have the imports listed properly then.

> @@ -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):

It is better to use SocketServer() to keep code styling consistent.

> +    """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

Why is it needed as websocket already has a controller file under 
/var/lib/kimchi/vnc-tokens?

Also using the guest_name is not a good idea as we support 
internationalization and some special character may not be accepted.

> +        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):
> +

LibvirtGuest()

> +    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)
> +

That way, we will open a new libvirt connection. We can reuse the same 
along the whole application.

And also, you are assuming the "qemu:///system" driver and it is not 
true for MockModel (one more reason to do not open a new connection to 
libvirt)

You can receive it as a parameter for the LibvirtGuest() instance.

> +        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])




More information about the Kimchi-devel mailing list