Usando SpicePorts para comunicar un guest KVM con el cliente SPICE

Desde la versión 0.12.2 del servidor y la 0.15 del cliente, SPICE soporta un tipo de canal llamado SpicePort. Un puerto permite comunicación de datos arbitrarios entre un proceso en el guest y el cliente de SPICE. De esta manera, se pueden dar servicios añadidos sobre la conexión con el escritorio remoto. Por ejemplo, en flexVM estamos trabajando en servicios de transferencia de ficheros, redirección de puertos TCP/UDP y compartición de impresoras. En esta entrada mostraremos cómo se crea y utiliza un SpicePort.

Crear un SpicePort

Todo comienza por la creación del puerto al lanzar una máquina virtual. Desde la versión 1.4.50, podemos indicarle a QEMU que queremos crear un puerto y asociarlo a un dispositivo serie virtual. Ámbos deben tener un nombre (SPICE recomienda que sea un FQDN), que puede ser el mismo para los dos. El nombre del dispositivo serie es el que utilizará el proceso del guest para establecer la comunicación, y el nombre del puerto es el que permitirá al cliente identificar el canal correspondiente a esa comunicación. Por ejemplo, vamos a utilizar el nombre ‘es.flexvdi.guest_agent‘ tanto para el dispositivo como para el puerto. Así, a QEMU debemos pasarle las siguientes opciones:

-chardev spiceport,id=spiceport,name=es.flexvdi.guest_agent
-device virtserialport,chardev=spiceport,name=es.flexvdi.guest_agent

Lanzando un guest Linux con estas opciones, veremos que aparece un dispositivo serie virtual llamado /dev/virtio-ports/es.flexvdi.guest_agent (que enlaza con /dev/vportXXX). En un guest Windows, en cambio, podemos acceder a este dispositivo con el nombre \\.\Global\es.flexvdi.guest_agent.

También podemos utilizar un dispositivo serie ISA clásico:

-chardev spiceport,id=spiceport,name=es.flexvdi.guest_agent
-device isa-serial,chardev=spiceport,id=serial0

En este caso, accederemos al puerto utilizando el API correspondiente para puertos serie. Por ejemplo, en Linux, a través de /dev/ttyS0.

Libvirt, desde la versión 1.2.2, también tiene un mecanismo para definir un SpicePort. Los dos casos anteriores se expresan en el XML de la siguiente manera, respectivamente:

...
<devices>
  <channel type="spiceport">
    <source channel="es.flexvdi.guest_agent"/>
    <target type="virtio" name="es.flexvdi.guest_agent"/>
  </channel>
  ...
  <serial type="spiceport">
    <source channel="es.flexvdi.guest_agent"/>
    <target port="0"/>
  </serial>
</devices>
...

Una vez que el puerto está creado, cualquier proceso con permisos de administración puede utilizarlo para enviar y recibir datos del cliente SPICE. La semántica de uso de un puerto SPICE es la misma que cualquier otro dispositivo virtio-serial, y está explicada en esta página de Linux-KVM.

Comunicación desde el lado del cliente

La librería cliente de SPICE se basa en Glib. Glib proporciona un mecanismo de señales y slots que permiten asociar callbacks a ciertos eventos. En particular, es posible indicarle a SPICE que queremos ser notificados cuando se abra un nuevo canal y cuando se reciban datos. Para ejemplificar esto, tomamos como referencia el cliente Spicy. En la función connection_new() de spicy.c se crea un objeto de tipo SpiceSession, que es el que se utiliza para registrar los eventos que se quieren monitorizar, de la siguiente manera:

  1. Llamamos a nuestra función de registro una vez que se ha creado la sesión.
  2. conn->session = spice_session_new();
    ...
    flexvdi_port_register_session(conn->session);
    
  3. Esta función registra los eventos de creación y destrucción de canales, con dos funciones que llamamos channel_new() y channel_destroy(), respectivamente.
  4. void flexvdi_port_register_session(SpiceSession * session) {
        g_signal_connect(session, "channel-new",
                         G_CALLBACK(channel_new), NULL);
        g_signal_connect(session, "channel-destroy",
                         G_CALLBACK(channel_destroy), NULL);
        ...
    }
    
  5. En particular, la función channel_new() comprueba si el nuevo canal es de tipo SpicePort, y en tal caso registra los eventos de apertura del puerto por parte del guest y de recepción de datos.
  6. static void channel_new(SpiceSession * s, SpiceChannel * channel) {
        if (SPICE_IS_PORT_CHANNEL(channel)) {
            g_signal_connect(channel, "notify::port-opened",
                             G_CALLBACK(port_opened), NULL);
            g_signal_connect(channel, "port-data",
                             G_CALLBACK(port_data), NULL);
        }
    }
    
  7. La función port_opened() debe comprobar si el puerto que se ha abierto es el que nos interesa. En tal caso, hay que guardarlo para usarlo posteriormente.
  8. static SpicePortChannel * channel;
    
    static void port_opened(SpiceChannel *channel, GParamSpec *pspec) {
        gchar *name = NULL;
        gboolean opened = FALSE;
        g_object_get(channel, "port-name", &name, "port-opened", &opened, NULL);
        if (g_strcmp0(name, "es.flexvdi.guest_agent") == 0 && opened) {
            g_printerr("flexVDI guest agent connected\n");
            channel = SPICE_PORT_CHANNEL(channel);
            ...
        }
    }
    
  9. Para escribir datos en el SpicePort y que le lleguen al guest, se usa la función spice_port_write_async(). Aparte del primer parámetro, que debe ser el canal en el que queremos escribir, el resto de parámetros son los habituales en una escritura asíncrona de Glib: los datos en un buffer, su tamaño, un objeto GCancellable, un callback de finalización y un puntero a datos que se le pasarán al callback:
  10. spice_port_write_async(channel, buffer, size, cancellable, port_write_cb, buffer);
    
  11. Al recibir datos, la función port_data debe comprobar que se reciben por el canal que nos interesa.
  12. static void port_data(SpicePortChannel * pchannel, gpointer data, int size) {
        if (pchannel == channel) {
            ...
        }
    }
    
  13. Finalmente, cuando se cierra el canal correspondiente al puerto, hay que eliminar la referencia para no utilizarlo en el futuro.
  14. static void channel_destroy(SpiceSession * s, SpiceChannel * channel) {
        if (SPICE_IS_PORT_CHANNEL(channel)) {
            if (SPICE_PORT_CHANNEL(channel) == channel) {
                channel = NULL;
            }
        }
    }