[Kimchi-devel] [PATCH] [Kimchi 2/6] Implement the web serial console server
Aline Manera
alinefm at linux.vnet.ibm.com
Fri Feb 5 16:43:23 UTC 2016
On 02/05/2016 02:33 PM, Jose Ricardo Ziviani wrote:
>
>
> On 05-02-2016 14:20, Aline Manera wrote:
>>
>>
>> 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.
>
> OK
>
>>
>>> + """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?
>
> Do you mean to save the socket file into /var/lib/kimchi/vnc-tokens
> instead of /tmp?
>
> My initial idea is that /tmp is always clean after any unexpected
> reboot or something. But I can change it.
>
No! Keep it on /tmp!
Now I understood /var/lib/kimchi/vnc-tokens will handle the websockets
connection and the /tmp/*guest-name* will be for libvirt communication.
>>
>> Also using the guest_name is not a good idea as we support
>> internationalization and some special character may not be accepted.
>
> guest_name here is already encoded to utf-8 (.encode('utf-8')), but
> I'll double check if any issue could happen
>
>>
>>> + 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()
>
> OK
>
>>
>>> + 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.
>
> Using the same libvirt connection under a new process doesn't work
> well. I believe that a reference is passed to the new process that,
> when finished, it closes that connection (and close Kimchi
> connection's to libvirt as well because both points to the same object).
>
> I'll investigate a bit more, if that's the case I could check on how
> to increase the refcount for such object and let you know then.
OK! If it is really needed to create a new connection, make sure to use
the same libvirt URI used in the whole application.
You can get that information from Model() class.
>
>>
>>> + 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