viernes, 17 de agosto de 2007

Hotplug en Xen

Xen es un sistema de paravirtualización (enlace en inglés), es decir, una manera de correr varios sistemas operativos en una máquina física, cada sistema creyendo que corre en una máquina real cuando lo hace en una máquina virtual con las características que hayamos definido.

Es paravirtualización, y no virtualización, porque los sistemas que van a correr en las máquinas virtuales deben ser modificados para correr sobre Xen. No obstante, las últimas versiones de Xen ofrecen virtualización real si se utilizan sobre procesadores de última generación, con instrucciones específicas.

El caso es que la máquina virtual no utiliza discos duros reales, sino elementos que desde la máquina real se le exportan como tales. Dichos elementos pueden ser un disco real, una partición o un fichero, y desde la máquina virtual se verán como un disco duro o una partición.

Entre las virtudes de Xen está la posibilidad de añadir o eliminar tales elementos de la máquina virtual en caliente, como si se enchufaran o desenchufaran de ella.

Y ahora, la almendra del asunto: Quiero que cuando enchufe un dispositivo en la máquina real (llamada "el hierro"), se enchufe automáticamente a la máquina virtual.

Esta solución la desarrollé yo, a partir de una idea de Kuko "Maestro Xen" Armas, cuando, teniendo otras cosas mejores que hacer en la empresa (Grupo CPD), me empeñé en sacar adelante la idea en vez de darlo por imposible.

El primer paso es detectar que se ha enchufado un dispositivo determinado al hierro. De eso se encarga udev. El segundo paso es introducir ese dispositivo en la máquina virtual. De eso se encarga xm, uno de los comandos de Xen.

Y en medio, hay que decidir a qué máquina virtual, de las varias que puede haber corriendo, se debe enchufar el dispositivo introducido. De esto se encarga XenHotplug, la solución que he desarrollado.

XenHotplug se compone de reglas de udev, guiones shell llamados desde esas reglas y un directorio de registro. Uno de esos guiones shell utiliza la firma de discos que presentaba en el artículo anterior.

Reglas de udev



Debemos crear un fichero de reglas de udev. Residirá en el directorio /etc/udev/rules.d y lo podemos llamar, por ejemplo, 10-local.rules. Ese nombre hará que se ejecute después de ciertas reglas fundamentales (las 5-early.rules) pero antes que el resto de reglas predefinidas.

En dicho fichero detectaremos la introducción o eliminación en el sistema de un dispositivo determinado y diremos qué hay que hacer con él:

SUBSYSTEM=="block", SYSFS{product}=="Mass Storage Device", \
SYSFS{manufacturer}=="Prolific Technology Inc.", \
SYSFS{start}=="63", ACTION=="add", \
PROGRAM="/usr/local/sbin/identify.sh %P", \
SYMLINK+="externalusbdisk%c", \
RUN+="/usr/local/sbin/action.sh %c %k"

SUBSYSTEM=="block", ACTION=="remove", \
RUN+="/usr/local/sbin/action.sh 0 %k"


La primera regla detecta la introducción en el sistema (ACTION=="add") de un dispositivo de bloque (SUBSYSTEM=="block") que puede ser cualquier cosa que se comporte como un disco duro externo o un disquete externo, que sea de la clase de dispositivos tipo disco (SYSFS{product}=="Mass Storage Device"), de una determinada marca (SYSFS{manufacturer}=="Prolific Technology Inc.").

Esto detectará la introducción de cualquier dispositivo de bloque, es decir, tanto el disco externo como sus particiones. Hay que tener en cuenta que al enchufar el disco, se genera un evento de introducción del disco completo, pero además el núcleo (kernel) del sistema revisa el disco y detecta sus particiones, que son añadidas al sistema, además del propio disco. Por eso, cuando enchufamos un disco particionado, aparecen los dispositivos correspondientes a las particiones, y no tenemos que crearlos a mano.

La regla, además, distingue si se trata del evento correspondiente al disco completo o a la partición, ya que la pieza SYSFS{start}=="63" sólo la cumple la primera partición. De esta manera la regla no casa con el disco completo, ni con las restantes particiones de un disco, sino sólo con la primera.

La siguiente pieza de la regla (PROGRAM="/usr/local/sbin/identify.sh %P") llama a un programa (guión de shell) de identificación del disco, que nos dirá de qué disco se trata exactamente. Así podremos distinguir los discos de esa marca y modelo que queremos que se enchufen a la máquina virtual de los que no. El programa se llama con un argumento (%P) que indica qué nombre de dispositivo se ha asignado al disco completo.

El programa proporciona un número de dispositivo, si se trata de un disco conocido (para lo que usamos la firma del disco) o la cadena NOID si se trata de un disco no identificado. En cualquiera de los dos casos, se crea un enlace simbólico /dev/externalusbdiskX (donde X es el número proporcionado antes), es decir, puede aparecer un enlace como /dev/externalusbdisk2 para el disco identificado como 2, o /dev/externalusbdiskNOID para un disco no identificado, gracias a la parte de la regla que dice SYMLINK+="externalusbdisk%c".

Finalmente, se ejecuta otro programa (guión de shell), con el dispositivo y el enlace simbólico ya creados, al que se pasan como argumentos el mismo número o NOID establecido antes y el nombre real del dispositivo (RUN+="/usr/local/sbin/action.sh %c %k"). Este programa borrará el enlace /dev/externalusbdiskNOID, si se trata de un dispositivo no identificado, o añadirá la partición a la máquina virtual y la registrará, si se trata de un dispositivo identificado.

La segunda regla detecta la retirada del sistema (ACTION=="remove") de cualquier (no podemos pedir mucha información acerca de un dispositivo ya retirado, ¿no?) dispositivo de bloque (SUBSYSTEM=="block"), y ejecuta el mismo programa que en el caso anterior, que en esta ocasión detectará si el dispositivo está registrado como insertado en la máquina virtual y, en tal caso, lo eliminará de la misma y lo desregistrará.

Guiones shell



Ahora veremos los programas que son llamados desde udev. El primero es identify.sh:

#!/bin/bash
# fichero /usr/local/sbin/identify.sh de XenHotplug

disp=$1
gen_id=`cat /usr/local/lib/identify.in`
disk_id=`dd if=/dev/$1 count=3 bs=1 skip=440 2> /dev/null`
disk_sn=`dd if=/dev/$1 count=1 bs=1 skip=443 2> /dev/null`
disk_sn_f=`echo -n $disk_sn|od -A n -N 1 --format=u`
if [ "$disk_id" = "$gen_id" ] ; then
echo $disk_sn_f
else
echo NOID
fi


Como se puede ver, el programa guarda en la variable $disp el nombre del dispositivo padre que la regla de udev le pasa como argumento, a continuación guarda en la variable $gen_id el contenido del fichero /usr/local/lib/identify.in, que es la firma de disco que empleamos para reconocerlos, y a continuación lee datos del propio disco.

Primero lee los bytes que deberían tener la firma de disco y los almacena en la variable $disk_id, y luego lee el byte de número de serie y lo almacena en la variable $disk_sn. Este número de serie, que es un byte con un valor entre 0 y 255 y que por lo tanto puede representar un carácter cualquiera, hemos de transformarlo en una cadena de texto para poderlo comparar, por eso lo formateamos como tal y lo almacenamos en la variable disk_sn_f.

Finalmente, si la firma del disco es la que esperamos, devolveremos el número de serie, que la regla udev empleará como ya hemos visto, y si el disco no es uno de los que buscamos, devolveremos la cadena NOID.

Obviamente, el contenido de /usr/local/lib/identify.in es la firma binaria que esperamos:

# hexdump /usr/local/lib/identify.in -e "\"%x\""
eeeeee


El segundo programa, action.sh, es el encargado de registrar y desregistrar los dispositivos, y de insertarlos y eliminarlos de la máquina virtual, así como de borrar el enlace simbólico /dev/externalusbdiskNOID en su caso:

#!/bin/bash
# fichero /usr/local/sbin/action.sh de XenHotplug

virt=mimaquinavirtual

if [ "$ACTION" = "add" ] ; then
if [ "$1" = NOID ] ; then
rm -f /dev/externalusbdisk$1
else
mkdir /var/xenhotplug/$1
echo $2 > /var/xenhotplug/$1/devname
/usr/sbin/xm block-list $virt > /var/xenhotplug/$1/prelist
/usr/sbin/xm block-attach $virt phy:externalusbdisk$1 hdb$1 \
w 2>&1
/usr/sbin/xm block-list $virt > /var/xenhotplug/$1/postlist
diff -u /var/xenhotplug/$1/{pre,post}list > \
/var/xenhotplug/$1/difflist
cat /var/xenhotplug/$1/difflist | grep ^+ | tail -n -1 | \
awk '{print \$1}'|awk -F + '{print \$2}' > \
/var/xenhotplug/$1/vdev
fi
else
if [ "$ACTION" = "remove" ] ; then
for file in /var/xenhotplug/* ; do
filecont=`cat $file/devname`
if [ "$filecont" = "$2" ] ; then
vdev=`cat $file/vdev`
/usr/local/sbin/assure-detach.sh $virt $vdev 2>&1
rm -rf $file
fi
done
fi
fi


El código actual no es exactamente ese, sino que incluye varias líneas de depurado y de registro que aquí no se muestran por simplicidad.


Es obvio que se trata de código aún no muy pulido, como en el guión shell anterior, no obstante hace su trabajo bastante bien. Eso sí: asume que la máquina virtual se llama mimaquinavirtual y que es la única máquina en la que insertamos dispositivos por esta vía, y asume que está permanentemente encendida.

Bueno, al grano. En primer lugar, vemos que hay dos grandes bloques de código, el primero se ejecuta si la variable de entorno $ACTION tiene el valor add y el segundo si tiene el valor remove. Esta variable de entorno la establece udev para el programa que ejecuta cuando se produce un evento.

El primer bloque, correspondiente a add, se subdivide en dos partes. Por un lado, si el programa se llama con un primer argumento de NOID, que recordemos es lo que ocurre cuando se introduce un disco no firmado, el programa borra el enlace simbólico espúreo y sale. Por otro lado, si el programa se llama con cualquier otro argumento supondremos que se trata del número de disco y entraremos en harina.

En primer lugar crearemos el directorio donde registramos este disco (mkdir /var/xenhotplug/$1), lo registramos en un fichero en ese directorio (echo $2 > /var/xenhotplug/$1/devname, hallamos la lista de dispositivos conectados a la máquina virtual (/usr/sbin/xm block-list $virt > /var/xenhotplug/$1/prelist), insertamos el disco en la máquina virtual (/usr/sbin/xm block-attach $virt phy:externalusbdisk$1 hdb$1 w 2>&1) y volvemos a hallar la lista de dispositivos conectados (/usr/sbin/xm block-list $virt > /var/xenhotplug/$1/postlist). Esto hay que hacerlo así porque no es posible determinar el número de dispositivo que Xen asigna a cada dispositivo insertado.

De hecho sí es posible, ya que el número asignado es una combinación de los números mayor y menor de dispositivo, pero aparte de complicado sólo vale cuando la máquina virtual es Linux.


Debido a ello debemos determinar el número de dispositivo asignado, para lo que hallamos la diferencia entre las dos listas de dispositivos (diff -u /var/xenhotplug/$1/{pre,post}list > /var/xenhotplug/$1/difflist), que es la línea correspondiente al dispositivo añadido, y extraemos de ella el citado número (cat /var/xenhotplug/$1/difflist |grep ^+|tail -n -1|awk '{print \$1}'|awk -F + '{print \$2}' > /var/xenhotplug/$1/vdev) y lo registramos también. Trabajo hecho.

Por otro lado, el segundo bloque, que se ejecuta cuando se retira un dispositivo, parece un poco más sencillo. Revisa a través de todos los dispositivos registrados en busca del que acaba de desaparecer (for file in /var/xenhotplug/* ; do ... done), poniendo temporalmente el nombre de cada uno en la variable $filecont (filecont=`cat $file/devname`) y comparándolo con el que buscamos (if [ "$filecont" = "$2" ] ; then ... fi). Cuando lo encuentra, registra temporalmente en la variable $vdev el número de dispositivo buscado (vdev=`cat $file/vdev`), llama a otro programa que se asegura de desconectar ese dispositivo de la máquina virtual (/usr/local/sbin/assure-detach.sh $virt $vdev 2>&1) y desregistra el dispositivo (rm -rf $file).

Directorio de registro



Bueno, el funcionamiento del directorio de registro quedó ya explicado arriba, con el fichero que lo usa, pero en resumen: el directorio /var/xenhotplug se llena con otros directorios, uno por cada dispositivo registrado. En el interior de estos se generan varios ficheros, de los cuales sólo son importantes dos, /var/xenhotplug/X/vdev, que contiene el número de dispositivo virtual Xen, y /var/xenhotplug/X/devname, que contiene el nombre de dispositivo del hierro que se utilizó para añadir a la máquina virtual. Crear uno de estos directorios con esos dos ficheros es lo que llamamos registrar un dispositivo, y borrarlo es lo que llamamos desregistrarlo.

assure-detach.sh



Estoooo... no lo voy a presentar. No está acabado. Y de hecho no funciona bien (aún). Pero básicamente lo que hace es llamar a /usr/sbin/xm block-detach $virt $vdev. El problema que tiene en este momento es que si el dispositivo está en uso en la máquina virtual, ésta ignora la orden de eliminarlo. Estoy trabajando en ello.

No hay comentarios: