KVM (sub)VDI con máquinas virtuales temporales usando bash

¿A quién está destinado este artículo?

Este artículo puede ser de interés para administradores de sistemas que se enfrentaron a la tarea de crear un servicio de «puestos de trabajo desechables».

Prólogo

El departamento de soporte informático de una joven y dinámicamente en crecimiento empresa con una pequeña red regional solicitó organizar «estaciones de autoservicio» para que sus clientes externos las utilizaran. Estas estaciones estaban destinadas a ser utilizadas para el registro en los portales externos de la empresa, la carga de datos desde dispositivos externos y el trabajo con portales gubernamentales.

Un aspecto importante era que gran parte del software está «adaptado» a MS Windows (por ejemplo, «Declaración»), y a pesar del movimiento hacia formatos abiertos, MS Office sigue siendo el estándar dominante para el intercambio de documentos electrónicos. Por lo tanto, no podíamos prescindir de MS Windows al abordar esta tarea.

El principal problema era la posibilidad de acumulación de diversos datos de sesiones de usuario, lo que podría llevar a su filtración a terceros. Una situación así ya ha perjudicado a los MFC.. Pero a diferencia de los MFC cuasiestatales (institución estatal autónoma), las organizaciones no gubernamentales serán castigadas mucho más severamente por tales fallos. El siguiente problema crítico fue el requerimiento de trabajar con soportes de datos externos, que, sin duda, contendrían un montón de malware. La probabilidad de introducir malware desde la red de internet se consideraba menos probable, debido a la restricción del acceso a internet según una lista blanca de direcciones. A la elaboración de requisitos se unieron empleados de otros departamentos, aportando sus requisitos y deseos; los requisitos finales se veían de la siguiente manera:

Requisitos de seguridad de la información

  • Después de su uso, todos los datos del usuario (incluidos los archivos temporales y las claves del registro) deben ser eliminados.
  • Todos los procesos iniciados por el usuario deben finalizar al concluir su trabajo.
  • Acceso a internet según una lista blanca de direcciones.
  • Restricciones en la posibilidad de ejecutar código de terceros.
  • En caso de inactividad de la sesión durante más de 5 minutos, la sesión debe cerrarse automáticamente y la estación debe realizar una limpieza.

Requisitos del cliente

  • Número de estaciones de clientes por filial: no más de 4.
  • El tiempo mínimo de espera para la disponibilidad del sistema, desde el momento en que te sientas hasta que comienzas a trabajar con el software del cliente.
  • La posibilidad de conectar dispositivos periféricos (escáneres, pen drives) directamente desde el lugar de instalación de la "estación de autoservicio".
  • Deseos del cliente
  • Demostración de materiales publicitarios (imágenes) durante el tiempo de inactividad del complejo.

Dificultades creativas

Después de jugar lo suficiente con los livecd de Windows, llegamos a la conclusión unánime de que la solución resultante no satisface al menos 3 puntos críticos. O tardan mucho en cargarse, o no son completamente en vivo, o su personalización estuvo relacionada con un dolor extremo. Tal vez no buscamos bien y ustedes puedan recomendarme un conjunto de herramientas, lo agradecería.

Luego comenzamos a mirar hacia VDI, pero para esta tarea, la mayoría de las soluciones son demasiado caras o requieren atención constante. Se deseaba una herramienta simple, con la menor cantidad de magia posible, cuyos problemas pudieran resolverse con un simple reinicio/reinicio del servicio. Afortunadamente, teníamos hardware de servidor de gama baja en las sucursales, proveniente de un servicio que estaba siendo descontinuado, que podíamos utilizar como base tecnológica.

¿Qué resultado obtuvimos al final? No podré contarles lo que resultó, debido a un NDA, pero durante el proceso de búsqueda desarrollamos un esquema interesante que funcionó bien en pruebas de laboratorio, aunque no se llevó a la producción.

Un poco de descargo de responsabilidad: el autor no afirma que la solución propuesta resuelva completamente todas las tareas planteadas y lo hace de forma voluntaria y entusiasta. El autor acepta de antemano que su habilidad en inglés es muy mala. Dado que la solución ya no se desarrolla, no se puede contar con arreglos de errores o cambios de funcionalidad, todo está en sus manos. El autor asume que ustedes están al menos un poco familiarizados con KVM y han leído un artículo introductorio sobre el protocolo Spice y han trabajado algo con CentOS u otra distribución de GNU/Linux.

En este artículo, me gustaría desglosar la estructura de la solución obtenida, a saber, la interacción entre el cliente y el servidor, así como la naturaleza de los procesos en el ciclo de vida de las máquinas virtuales en el contexto de la solución en cuestión. Si el artículo resulta interesante para el público, describiré los detalles de la implementación de imágenes live para crear clientes ligeros basados en Fedora y hablaré sobre los detalles de la configuración de máquinas virtuales. servidores KVM para optimizar el rendimiento y la seguridad.

Si tomamos papel de colores,
pinturas, pinceles y pegamento,
y también un poco de destreza...
¡se puede hacer cien rublos!

Esquema y descripción del banco de pruebas

KVM (sub)VDI con máquinas virtuales temporales usando bash

Todo el hardware se encuentra dentro de la red de la sucursal, solo el canal de internet está expuesto hacia afuera. El servidor proxy ya existía históricamente, no representa nada extraordinario. Pero es precisamente en este servidor donde, entre otras cosas, se filtrará el tráfico de las máquinas virtuales (abreviado como VM en el texto). No hay nada que impida colocar este servicio en un servidor KVM, solo hay que observar cómo cambia la carga que genera sobre el subsistema de almacenamiento.

Estación Cliente – en sí, "estaciones de autoservicio", el "frontend" de nuestro servicio. Se presentan como nettops Lenovo IdeaCentre. ¿Qué tiene de bueno esta máquina? Pues casi todo, especialmente destaca la gran cantidad de puertos USB y el lector de tarjetas en el panel frontal. En nuestro esquema, en el lector de tarjetas está insertada una tarjeta SD con protección de escritura habilitada, en la que se ha grabado una imagen live modificada de Fedora 28. Por supuesto, al nettop están conectados un monitor, teclado y ratón.

Switch – un switch de hardware de nivel dos sin características notables, se encuentra en la sala de servidores y parpadea con luces. No está conectado a ninguna red, excepto a la red de las "estaciones de autoservicio".

KVM_Server – el núcleo del esquema, en pruebas de banco, un Core 2 Quad Q9650 con 8 GB de RAM manejaba sin problemas 3 máquinas virtuales con Windows 10. El subsistema de almacenamiento – adaptec 3405 con 2 discos en Raid 1 + SSD. En pruebas de campo, un Xeon 1220 con un LSI 9260 más robusto + SSD manejaba fácilmente de 5 a 6 VM. Los servidores los obtuvimos del servicio que fue cancelado, los costos de capital no serían muchos. En este (o estos) servidores se desplegó el sistema de virtualización KVM con un pool de máquinas virtuales pool_Vm.

VM — máquina virtual, el backend de nuestro servicio. En ella se lleva a cabo el trabajo del usuario.

Enp5s0 – interfaz de red que se conecta a la red de «estaciones de autoservicio», donde residen dhcpd, ntpd, httpd, y xinetd escucha el puerto «signal».

Lo0 – interfaz pseudo bucle inverso. Estándar.

Spice_console – Una cosa muy interesante; dado que a diferencia del RDP clásico, al desplegar una combinación de KVM + protocolo Spice, aparece una entidad adicional: el puerto de la consola de la máquina virtual. De hecho, al conectarnos a este puerto TCP, tenemos acceso a la consola de la VM, sin necesidad de conectarnos a la VM a través de su interfaz de red. Todo el intercambio de señales se realiza por parte del servidor. El análogo más cercano en función es el IPKVM. Es decir, a este puerto se transmite la imagen del monitor de la VM, así como se envían los datos sobre el movimiento del ratón, y (lo más importante) la interacción a través del protocolo Spice permite redirigir dispositivos USB de manera fluida a la máquina virtual, como si estuvieran conectados a la propia VM. Comprobado para unidades USB, escáneres y cámaras web.

Vnet0, virbr0 y las tarjetas de red virtuales de la VM forman una red de máquinas virtuales.

Cómo FUNCIONA ESTO

Desde la Estación Cliente

La estación cliente arranca en modo gráfico desde una imagen live modificada de Fedora 28, obteniendo una dirección IP por DHCP del espacio de direcciones de la red 169.254.24.0/24. Durante el proceso de arranque, se crean reglas de firewall que permiten conexiones a los puertos «signal» y «spice» del servidor. Después de completar el arranque, la estación espera la autorización del usuario «Client». Tras la autorización, se inicia el gestor de ventanas «openbox» y se ejecuta el script de autoinicio autostart en nombre del usuario autorizado. Entre otras cosas, el script de autoinicio ejecuta el script remote.sh.

$HOME/.config/openbox/scripts/remote.sh

#!/bin/sh

server_ip=$(/usr/bin/cat /etc/client.conf |/usr/bin/grep "server_ip" 
|/usr/bin/cut -d "=" -f2)
vdi_signal_port=$(/usr/bin/cat /etc/client.conf |/usr/bin/grep "vdi_signal_port" 
 |/usr/bin/cut -d "=" -f2)
vdi_spice_port=$(/usr/bin/cat /etc/client.conf |/usr/bin/grep "vdi_spice_port" 
|/usr/bin/cut -d "=" -f2)
animation_folder=$(/usr/bin/cat /etc/client.conf |/usr/bin/grep "animation_folder" 
|/usr/bin/cut -d "=" -f2)

process=/usr/bin/remote-viewer

while true
do
 if [ -z `/usr/bin/pidof feh` ]
 then
 /usr/bin/echo $animation_folder
 /usr/bin/feh -N -x -D1 $animation_folder &
 else
 /usr/bin/echo
 fi
/usr/bin/nc -i 1 $server_ip $vdi_signal_port |while read line
 do
  if /usr/bin/echo "$line" |/usr/bin/grep "RULE ADDED, CONNECT NOW!"
  then
   /usr/bin/killall feh
   pid_process=$($process "spice://$server_ip:$vdi_spice_port"  
   "--spice-disable-audio" "--spice-disable-effects=animation"  
   "--spice-preferred-compression=auto-glz" "-k" 
   "--kiosk-quit=on-disconnect" | /bin/echo $!)
   /usr/bin/wait $pid_process
   /usr/bin/killall -u $USER
   exit
  else
   /usr/bin/echo $line >> /var/log/remote.log
  fi
 done
done

/etc/client.conf

server_ip=169.254.24.1
vdi_signal_port=5905
vdi_spice_port=5906
animation_folder=/usr/share/backgrounds/animation
background_folder=/usr/share/backgrounds2/fedora-workstation

Descripción de las variables del archivo client.conf
server_ip — dirección del KVM_Server
vdi_signal_port — puerto del KVM_Server donde reside xinetd
vdi_spice_port — puerto de red del KVM_Server, desde el cual se redirige la solicitud de conexión del cliente remote-viewer al puerto spice de la VM dedicada (más detalles a continuación)
animation_folder — carpeta de donde se obtienen las imágenes para mostrar la animación incorrección
background_folder — carpeta de donde se obtienen las imágenes para mostrar presentaciones en modo espera. Más sobre la animación en la siguiente parte del artículo.

El script remote.sh toma la configuración del archivo de configuración /etc/client.conf y establece una conexión a través de nc en el puerto "vdi_signal_port" del servidor KVM, recibiendo un flujo de datos del servidor, entre los cuales espera la línea "RULE ADDED, CONNECT NOW". Al recibir la línea buscada, se inicia el proceso remote-viewer en modo kiosco, estableciendo conexión en el puerto "vdi_spice_port" del servidor. La ejecución del script se suspende hasta que finaliza la ejecución de remote-viewer.

Remote-viewer, al conectarse al puerto "vdi_spice_port", gracias a la redirección en el lado del servidor, accede al puerto "spice_console" de la interfaz lo0, es decir, a la consola de la máquina virtual, y se lleva a cabo, directamente, el trabajo del usuario. Durante el tiempo de espera de la conexión, se muestra al usuario una animación de relleno, en forma de presentación de diapositivas de archivos jpeg, siendo el camino al directorio con las imágenes determinado por el valor de la variable animation_folder del archivo de configuración.

Al perder la conexión con el puerto "spice_console" de la máquina virtual, señalando el apagado/reinicio de la máquina virtual (es decir, la finalización efectiva de la sesión del usuario), se cierran todos los procesos iniciados en nombre del usuario autenticado, lo que provoca el reinicio de lightdm y el regreso a la pantalla de autorización.

Desde el lado del servidor KVM

En el puerto "signal" de la tarjeta de red enp5s0 espera conexión xinetd. Tras la conexión al puerto "signal", xinetd ejecuta el script vm_manager.sh sin pasarle ningún parámetro de entrada y redirige el resultado de la ejecución del script a la sesión nc de la estación cliente.

/etc/xinetd.d/test-server

service vdi_signal

{
port	=	5905
socket_type	=	stream
protocol	=	tcp
wait	=	no
user	=	root
server	=	/home/admin/scripts_vdi_new/vm_manager.sh
}

/home/admin/scripts_vdi_new/vm_manager.sh


#!/usr/bin/sh

#<SET LOCAL VARIABLES FOR SCRIPT>#
SRV_SCRIPTS_DIR=$(/usr/bin/cat /etc/vm_manager.conf  
|/usr/bin/grep "srv_scripts_dir" |/usr/bin/cut -d "=" -f2)
/usr/bin/echo "SRV_SCRIPTS_DIR=$SRV_SCRIPTS_DIR"
export SRV_SCRIPTS_DIR=$SRV_SCRIPTS_DIR
SRV_POOL_SIZE=$(/usr/bin/cat /etc/vm_manager.conf 
|/usr/bin/grep "srv_pool_size" |/usr/bin/cut -d "=" -f2)
/usr/bin/echo "SRV_POOL_SIZE=$SRV_POOL_SIZE"
export "SRV_POOL_SIZE=$SRV_POOL_SIZE"
SRV_START_PORT_POOL=$(/usr/bin/cat /etc/vm_manager.conf  
|/usr/bin/grep "srv_start_port_pool" |/usr/bin/cut -d "=" -f2)
/usr/bin/echo SRV_START_PORT_POOL=$SRV_START_PORT_POOL
export SRV_START_PORT_POOL=$SRV_START_PORT_POOL
SRV_TMP_DIR=$(/usr/bin/cat /etc/vm_manager.conf 
|/usr/bin/grep "srv_tmp_dir" |/usr/bin/cut -d "=" -f2)
/usr/bin/echo "SRV_TMP_DIR=$SRV_TMP_DIR"
export SRV_TMP_DIR=$SRV_TMP_DIR
date=$(/usr/bin/date)
#</SET LOCAL VARIABLES FOR SCRIPT>#

/usr/bin/echo "# $date START EXECUTE VM_MANAGER.SH #"

make_connect_to_vm() {

#<READING CLEAR.LIST AND CHECK PORT FOR NETWORK STATE>#
/usr/bin/echo "READING CLEAN.LIST AND CHECK PORT STATE"
#<CHECK FOR NO ONE PORT IN CLEAR.LIST>#

if [ -z `/usr/bin/cat $SRV_TMP_DIR/clear.list` ]
then
 /usr/bin/echo "NO AVALIBLE PORTS IN CLEAN.LIST FOUND"
 /usr/bin/echo "Will try to make housekeeper, and create new vm"
 make_housekeeper
else
 #<MINIMUN ONE PORT IN CLEAR.LIST FOUND>#
  /usr/bin/cat $SRV_TMP_DIR/clear.list |while read line
   do
    clear_vm_port=$(($line))
    /bin/echo "FOUND PORT $clear_vm_port IN CLEAN.LIST. TRY NETSTAT"  
    "CHECK FOR PORT=$clear_vm_port"

    #<NETSTAT LISTEN CHECK FOR PORT FROM CLEAN.LIST>#
    if /usr/bin/netstat -lnt |/usr/bin/grep ":$clear_vm_port" > /dev/null
     then
     /bin/echo "$clear_vm_port IS LISTEN"
     #<PORT IS LISTEN. CHECK FOR IS CONNECTED NOW>#
     if /usr/bin/netstat -nt |/usr/bin/grep ":$clear_vm_port"  
     |/usr/bin/grep "ESTABLISHED" > /dev/null
       then
#<PORT LISTEN AND ALREADY CONNECTED! MOVE PORT FROM CLEAR.LIST 
# TO WASTE.LIST>#
       /bin/echo "$clear_vm_port IS ALREADY CONNECTED, MOVE PORT TO WASTE.LIST"
       /usr/bin/sed -i "/$clear_vm_port/d" $SRV_TMP_DIR/clear.list
       /usr/bin/echo $clear_vm_port >> $SRV_TMP_DIR/waste.list
       else
#<PORT LISTEN AND NO ONE CONNECT NOW. MOVE PORT FROM CLEAR.LIST TO 
# CONN_WAIT.LIST AND CREATE IPTABLES RULES>##
       /usr/bin/echo "OK, $clear_vm_port IS NOT ALREADY CONNECTED"
       /usr/bin/sed -i "/$clear_vm_port/d" $SRV_TMP_DIR/clear.list
       /usr/bin/echo $clear_vm_port >> $SRV_TMP_DIR/conn_wait.list
       $SRV_SCRIPTS_DIR/vm_connect.sh $clear_vm_port
#<TRY TO CLEAN VM IN WASTE.LIST AND CREATE NEW WM>#
       /bin/echo "TRY TO CLEAN VM IN WASTE.LIST AND CREATE NEW VM"
       make_housekeeper
       /usr/bin/echo "# $date STOP EXECUTE VM_MANAGER.SH#"
       exit
       fi
     else
     #<PORT IS NOT A LISTEN. MOVE PORT FROM CLEAR.LIST TO WASTE.LIST>#
     /bin/echo " "$clear_vm_port" is NOT LISTEN. REMOVE PORT FROM CLEAR.LIST"
     /usr/bin/sed -i "/$clear_vm_port/d" $SRV_TMP_DIR/clear.list
     /usr/bin/echo $clear_vm_port >> $SRV_TMP_DIR/waste.list
    make_housekeeper
     fi
   done
fi
}

make_housekeeper() {
/usr/bin/echo "=Execute housekeeper="
/usr/bin/cat $SRV_TMP_DIR/waste.list |while read line
 do
 /usr/bin/echo "$line"
 if /usr/bin/netstat -lnt |/usr/bin/grep ":$line" > /dev/null
  then
  /bin/echo "port_alive, vm is running"
  if /usr/bin/netstat -nt |/usr/bin/grep ":$line"  
   |/usr/bin/grep "ESTABLISHED" > /dev/null
    then
    /bin/echo "port_in_use can't delete vm!!!"
    else
    /bin/echo "port_not in use. Deleting vm"
    /usr/bin/sed -i "/$line/d" $SRV_TMP_DIR/waste.list
    /usr/bin/echo $line >> $SRV_TMP_DIR/recycle.list
    $SRV_SCRIPTS_DIR/vm_delete.sh $line
    fi
  else
  /usr/bin/echo "posible vm is already off. Deleting vm"
  /usr/bin/echo "MOVE VM IN OFF STATE $line FROM WASTE.LIST TO"  
  "RECYCLE.LIST AND DELETE VM"
  /usr/bin/sed -i "/$line/d" $SRV_TMP_DIR/waste.list
  /usr/bin/echo $line >> $SRV_TMP_DIR/recycle.list
  $SRV_SCRIPTS_DIR/vm_delete.sh "$line"
 fi
done
create_clear_vm
}

create_clear_vm() {
/usr/bin/echo "=Create new VM="
while [ $SRV_POOL_SIZE -gt 0 ]
do
 new_vm_port=$(($SRV_START_PORT_POOL+$SRV_POOL_SIZE))
 /usr/bin/echo "new_vm_port=$new_vm_port"
 if /usr/bin/grep "$new_vm_port" $SRV_TMP_DIR/clear.list > /dev/null
  then
  /usr/bin/echo "$new_vm_port port is already defined in clear.list"
  else
  if /usr/bin/grep "$new_vm_port" $SRV_TMP_DIR/waste.list > /dev/null
   then
   /usr/bin/echo "$new_vm_port port is already defined in waste.list"
   else
    if /usr/bin/grep "$new_vm_port" $SRV_TMP_DIR/recycle.list > /dev/null
    then
    /usr/bin/echo "$new_vm_port PORT IS ALREADY DEFINED IN RECYCLE LIST"
    else
    if  /usr/bin/grep "$new_vm_port" $SRV_TMP_DIR/conn_wait.list > /dev/null
     then
     /usr/bin/echo "$new_vm_port PORT IS ALREADY DEFINED IN CONN_WAIT LIST"
     else
     /usr/bin/echo "PORT IN NOT DEFINED IN NO ONE LIST WILL CREATE" 
     "VM ON PORT $new_vm_port"
     /usr/bin/echo $new_vm_port >> $SRV_TMP_DIR/recycle.list
     $SRV_SCRIPTS_DIR/vm_create.sh $new_vm_port
     fi
    fi
   fi
 fi
 SRV_POOL_SIZE=$(($SRV_POOL_SIZE-1))
done
/usr/bin/echo "# $date STOP EXECUTE VM_MANAGER.SH #"
}
make_connect_to_vm |/usr/bin/tee -a /var/log/vm_manager.log

/etc/vm_manager.confsrv_scripts_dir=/home/admin/scripts_vdi_new
srv_pool_size=4
srv_start_port_pool=5920
srv_tmp_dir=/tmp/vm_state
base_host=win10_2
input_iface=enp5s0
vdi_spice_port=5906
count_conn_tryes=10

Descripción de las variables del archivo de configuración vm_manager.conf
srv_scripts_dir — carpeta donde se encuentran los scripts vm_manager.sh, vm_connect.sh, vm_delete.sh, vm_create.sh, vm_clear.sh
srv_pool_size — tamaño del pool de VM
srv_start_port_pool — puerto inicial, posteriormente se comenzará a asignar puertos de las consolas spice de las máquinas virtuales
srv_tmp_dir — carpeta para ubicar archivos temporales
base_host — VM base (imagen maestra) de la cual se harán clones de VM en el pool
input_iface — interfaz de red del servidor, que mira hacia las estaciones clientes
vdi_spice_port — puerto de red del servidor, desde el cual se llevará a cabo la redirección de la solicitud de conexión del cliente remote-viewer al puerto spice de la VM dedicada
count_conn_tryes — temporizador de espera, tras el cual se considera que no se ha producido conexión a la VM (consultar detalles de funcionamiento en vm_connect.sh)

El script vm_manager.sh lee el archivo de configuración de vm_manager.conf, evalúa el estado de las máquinas virtuales en el pool según varios parámetros, a saber: cuántas VM están desplegadas, si hay VM limpias disponibles. Para ello, lee el archivo clear.list que contiene los números de puertos de «spice_console» de las máquinas virtuales «recién creadas» (consultar ciclo de creación de VM a continuación) y verifica la existencia de una conexión establecida con ellas. Al detectar un puerto con conexión de red establecida (lo cual no debería suceder en absoluto), se genera una advertencia y el puerto se mueve a waste.list. Al detectar el primer puerto del archivo clear.list con el cual actualmente no hay conexión, vm_manager.sh llama al script vm_connect.sh y le pasa como parámetro el número de este puerto.

/home/admin/scripts_vdi_new/vm_connect.sh

#!/bin/sh

date=$(/usr/bin/date)

/usr/bin/echo "#" "$date" "START EXECUTE VM_CONNECT.SH#"

#<SET LOCAL VARIABLES FOR SCRIPT>#
free_port="$1"

input_iface=$(/usr/bin/cat /etc/vm_manager.conf |/usr/bin/grep "input_iface" 
|/usr/bin/cut -d "=" -f2)
/usr/bin/echo "input_iface=$input_iface"

vdi_spice_port=$(/usr/bin/cat /etc/vm_manager.conf   
|/usr/bin/grep "vdi_spice_port" |/usr/bin/cut -d "=" -f2)
/usr/bin/echo "vdi_spice_port=$vdi_spice_port"

count_conn_tryes=$(/usr/bin/cat /etc/vm_manager.conf  
|/usr/bin/grep "count_conn_tryes" |/usr/bin/cut -d "=" -f2)
/usr/bin/echo "count_conn_tryes=$count_conn_tryes"
#</SET LOCAL VARIABLES FOR SCRIPT>#

#<CREATE IPTABLES RULES AND SEND SIGNAL TO CONNECT>#
/usr/bin/echo "create rule for port" $free_port
/usr/sbin/iptables -I INPUT -i $input_iface -p tcp -m tcp --dport  
$free_port  -j ACCEPT
/usr/sbin/iptables -I OUTPUT -o $input_iface -p tcp -m tcp --sport 
$free_port -j ACCEPT
/usr/sbin/iptables -t nat -I PREROUTING -p tcp -i $input_iface --dport  
$vdi_spice_port -j DNAT --to-destination 127.0.0.1:$free_port
/usr/bin/echo "RULE ADDED, CONNECT NOW!"
#</CREATE IPTABLES RULES AND SEND SIGNAL TO CONNECT>#

#<WAIT CONNECT ESTABLISHED AND ACTIVATE CONNECT TIMER>#
while [ $count_conn_tryes -gt 0 ]
do
if /usr/bin/netstat -nt |/usr/bin/grep ":$free_port"  
|/usr/bin/grep "ESTABLISHED" > /dev/null
 then
  /bin/echo "$free_port NOW in use!!!"
  /usr/bin/sleep 1s
  /usr/sbin/iptables -t nat -D PREROUTING -p tcp -i $input_iface --dport  
  $vdi_spice_port -j DNAT --to-destination 127.0.0.1:$free_port
  /usr/sbin/iptables -D INPUT -i $input_iface -p tcp -m tcp --dport  
  $free_port  -j ACCEPT
  /usr/sbin/iptables -D OUTPUT -o $input_iface -p tcp -m tcp --sport  
  $free_port -j ACCEPT
  /usr/bin/sed -i "/$free_port/d" $SRV_TMP_DIR/conn_wait.list
  /usr/bin/echo $free_port >> $SRV_TMP_DIR/waste.list
  return
 else
   /usr/bin/echo "$free_port NOT IN USE"
   /usr/bin/echo "RULE ADDED, CONNECT NOW!"
   /usr/bin/sleep 1s
 fi
count_conn_tryes=$((count_conn_tryes-1))
done
#</WAIT CONNECT ESTABLISED AND ACTIVATE CONNECT TIMER>#

#<IF COUNT HAS EXPIRED. REMOVE IPTABLES RULE AND REVERT 
# VM TO CLEAR.LIST>#
/usr/bin/echo "REVERT IPTABLES RULE AND REVERT VM TO CLEAN 
LIST $free_port"
/usr/sbin/iptables -t nat -D PREROUTING -p tcp -i $input_iface --dport 
$vdi_spice_port -j DNAT --to-destination 127.0.0.1:$free_port
/usr/sbin/iptables -D INPUT -i $input_iface -p tcp -m tcp --dport $free_port 
-j ACCEPT
/usr/sbin/iptables -D OUTPUT -o $input_iface -p tcp -m tcp --sport  
$free_port -j ACCEPT
/usr/bin/sed -i "/$free_port/d" $SRV_TMP_DIR/conn_wait.list
/usr/bin/echo $free_port >> $SRV_TMP_DIR/clear.list
#</COUNT HAS EXPIRED. REMOVE IPTABLES RULE AND REVERT VM 
#TO CLEAR.LIST>#
/usr/bin/echo "#" "$date" "END EXECUTE VM_CONNECT.SH#"

# Attention! Must Be!  sysctl net.ipv4.conf.all.route_localnet=1

El script vm_connect.sh establece las reglas del firewall que crean un redireccionamiento del puerto «vdi_spice_port» del servidor de la interfaz enp5s0 al «puerto de consola spice» de la VM, ubicada en la interfaz lo0 del servidor, que se pasa como parámetro de inicio. El puerto se mueve a conn_wait.list, y la VM se considera a la espera de conexión. En la sesión de la Client Station, en el puerto «signal» del servidor, se envía la cadena «RULE ADDED, CONNECT NOW», que espera el script remote.sh que se está ejecutando. Comienza un ciclo de espera de conexión con el número de intentos determinado por el valor de la variable «count_conn_tryes» en el archivo de configuración. Cada segundo, se enviará a la sesión nc la cadena «RULE ADDED, CONNECT NOW» y se comprobará la existencia de una conexión establecida hasta el puerto «spice_console».

Si durante el número establecido de intentos no se produce conexión, el puerto «spice_console» se mueve de nuevo a clear.list. La ejecución de vm_connect.sh se completa y se reanuda la ejecución de vm_manager.sh, que inicia el ciclo de limpieza.

Si se registra una conexión de la Estación Cliente al puerto «spice_console» en la interfaz lo0, se eliminan las reglas del firewall que crean un redireccionamiento entre el puerto «spice» del servidor y el puerto «spice_console», y el mantenimiento posterior de la conexión se realiza a través del mecanismo de detección del estado del firewall. En caso de ruptura de la conexión, no será posible restablecer la conexión con el puerto «spice_console». El puerto «spice_console» se traslada a waste.list, la VM se considera "sucia" y no podrá volver al grupo de máquinas virtuales "limpias" sin pasar por un proceso de limpieza. La ejecución de vm_connect.sh finaliza, se reanuda la ejecución de vm_manager.sh, que inicia el ciclo de limpieza.

El ciclo de limpieza comienza revisando el archivo waste.list, en el que se trasladan los números de los puertos «spice_console» de las máquinas virtuales a las que se estableció conexión. Se determina la presencia de una conexión activa en cada puerto «spice_console» de la lista. Si no hay conexión, se considera que la máquina virtual ya no está en uso, y el puerto se traslada a recycle.list, iniciándose el proceso de eliminación de la máquina virtual (ver más abajo) a la que pertenecía este puerto. Si se encuentra una conexión de red activa en el puerto, se considera que la máquina virtual está en uso, y no se realizan acciones sobre ella. Si el puerto no está siendo escuchado, se considera que la VM está apagada y ya no es necesaria. El puerto se traslada a recycle.list y se inicia el proceso de eliminación de la máquina virtual. Para esto se llama al script vm_delete.sh, al cual se le pasa como parámetro el número del puerto «spice_console» de la VM que debe eliminarse.

/home/admin/scripts_vdi_new/vm_delete.sh


#!/bin/sh

#<Set local VARIABLES>#
port_to_delete="$1"
date=$(/usr/bin/date)
#</Set local VARIABLES>#

/usr/bin/echo "# $date START EXECUTE VM_DELETE.SH#"
/usr/bin/echo "TRY DELETE VM ON PORT: $vm_port"

#<VM NAME SETUP>#
vm_name_part1=$(/usr/bin/cat /etc/vm_manager.conf |/usr/bin/grep 'base_host' 
|/usr/bin/cut -d'=' -f2)
vm_name=$(/usr/bin/echo "$vm_name_part1""-""$port_to_delete")
#</VM NAME SETUP>#

#<SHUTDOWN AND DELETE VM>#
/usr/bin/virsh destroy $vm_name
/usr/bin/virsh undefine $vm_name
/usr/bin/rm -f /var/lib/libvirt/images_write/$vm_name.qcow2
/usr/bin/sed -i "/$port_to_delete/d" $SRV_TMP_DIR/recycle.list
#</SHUTDOWN AND DELETE VM>#

/usr/bin/echo "VM ON PORT $vm_port HAS BEEN DELETE AND REMOVE" 
 "FROM RECYCLE.LIST. EXIT FROM VM_DELETE.SH"
/usr/bin/echo "# $date STOP EXECUTE VM_DELETE.SH#"
exit

La eliminación de la máquina virtual es una operación bastante trivial, el script vm_delete.sh determina el nombre de la máquina virtual a la que pertenece el puerto, pasado como parámetro de inicio. Se realiza una detención forzada de la VM, se elimina la VM del hipervisor, y se elimina el disco duro virtual de dicha VM. El puerto «spice_console» se elimina de recycle.list. La ejecución de vm_delete.sh finaliza, y se reanuda la ejecución de vm_manager.sh.

El script vm_manager.sh, al finalizar las operaciones de limpieza de máquinas virtuales innecesarias de la lista waste.list, comienza un ciclo de creación de máquinas virtuales en el grupo.

El proceso comienza con la identificación de los puertos disponibles para la colocación de "spice_console". Para ello, se basa en el parámetro del archivo de configuración "srv_start_port_pool", que establece el puerto inicial para el grupo "spice_console" de máquinas virtuales, y en el parámetro "srv_pool_size", que define la cantidad máxima de máquinas virtuales. Se realiza un recorrido secuencial de todas las opciones posibles de puertos. Para cada puerto determinado, se verifica su presencia en clear.list, waste.list, conn_wait.list y recycle.list. Si el puerto se encuentra en cualquiera de estos archivos, se considera ocupado y se omite. Si no se encuentra en los archivos mencionados, se agrega a recycle.list y se inicia el proceso de creación de una nueva máquina virtual. Para ello, se llama al script vm_create.sh, al cual se le pasa como parámetro el número del puerto "spice_console" para el que se debe crear la VM.

/home/admin/scripts_vdi_new/vm_create.sh


#!/bin/sh
/usr/bin/echo "#" "$date" "START RUNNING VM_CREATE.SH#"

new_vm_port=$1
date=$(/usr/bin/date)
a=0
/usr/bin/echo SRV_TMP_DIR=$SRV_TMP_DIR

#<SET LOCAL VARIABLES FOR SCRIPT>#
base_host=$(/usr/bin/cat /etc/vm_manager.conf |/usr/bin/grep "base_host" 
|/usr/bin/cut -d "=" -f2)
/usr/bin/echo "base_host=$base_host"
#</SET LOCAL VARIABLES FOR SCRIPT>#

hdd_image_locate() {

/bin/echo "Run STEP 1 - hdd_image_locate"

hdd_base_image=$(/usr/bin/virsh dumpxml $base_host  
|/usr/bin/grep "source file" |/usr/bin/grep "qcow2" |/usr/bin/head -n 1 
|/usr/bin/cut -d "'" -f2)
if [ -z "$hdd_base_image" ]
then
 /bin/echo "base hdd image not found!"
else
 /usr/bin/echo "hdd_base_image found is a $hdd_base_image. Run next step 2"

#< CHECK FOR SNAPSHOT ON BASE HDD >#

  if [ 0 -eq `/usr/bin/qemu-img info "$hdd_base_image" | /usr/bin/grep -c "Snapshot"` ]
  then
  /usr/bin/echo "base image haven't snapshot, run NEXT STEP 3"
  else
  /usr/bin/echo "base hdd image have a snapshot, can't use this image"
  exit
  fi
#</ CHECK FOR SNAPSHOT ON BASE HDD >#

#< CHECK FOR HDD IMAGE IS LINK CLONE >#
  if [ 0 -eq `/usr/bin/qemu-img info "$hdd_base_image" |/usr/bin/grep -c "backing file"
  then
  /usr/bin/echo "base image is not a linked clone, NEXT STEP 4"
  /usr/bin/echo "Base image check complete!"
  else
  /usr/bin/echo "base hdd image is a linked clone, can't use this image"
  exit
  fi
fi
#</ CHECK FOR HDD IMAGE IS LINK CLONE >#
cloning
    }

cloning() {
# <Step_1 turn the base VM off >#
 /usr/bin/virsh shutdown $base_host > /dev/null 2>&1
 # </Step_1 turn the base VM off >#

#<Create_vm_config>#

/usr/bin/echo "Free port for Spice VM is $new_vm_port"

 #<Setup_name_for_new_VM>#
new_vm_name=$(/bin/echo $base_host"-"$new_vm_port)
#</Setup_name_for_new_VM>#

#<Make_base_config_as_clone_base_VM>#
/usr/bin/virsh dumpxml $base_host > $SRV_TMP_DIR/$new_vm_name.xml
#<Make_base_config_as_clone_base_VM>#

##<Setup_New_VM_Name_in_config>##
/usr/bin/sed -i "s%<name>$base_host</name>%<name>$new_vm_name</name>%g" $SRV_TMP_DIR/$new_vm_name.xml
#</Setup_New_VM_Name_in_config>#

#<UUID Changing>#
old_uuid=$(/usr/bin/cat $SRV_TMP_DIR/$new_vm_name.xml |/usr/bin/grep "<uuid>")
/usr/bin/echo old UUID $old_uuid
new_uuid_part1=$(/usr/bin/echo "$old_uuid" |/usr/bin/cut -d "-" -f 1,2)
new_uuid_part2=$(/usr/bin/echo "$old_uuid" |/usr/bin/cut -d "-" -f 4,5)
new_uuid=$(/bin/echo $new_uuid_part1"-"$new_vm_port"-"$new_uuid_part2)
/usr/bin/echo $new_uuid
/usr/bin/sed -i "s%$old_uuid%$new_uuid%g" $SRV_TMP_DIR/$new_vm_name.xml
#</UUID Changing>#


#<Spice port replace>#
old_spice_port=$(/usr/bin/cat  $SRV_TMP_DIR/$new_vm_name.xml  
|/usr/bin/grep "graphics type='spice' port=")
/bin/echo old spice port $old_spice_port
new_spice_port=$(/usr/bin/echo "<graphics type='spice' port='$new_vm_port' autoport='no' listen='127.0.0.1'>")
/bin/echo $new_spice_port
/usr/bin/sed -i "s%$old_spice_port%$new_spice_port%g" $SRV_TMP_DIR/$new_vm_name.xml
#</Spice port replace>#

#<MAC_ADDR_GENERATE>#
mac_new=$(/usr/bin/hexdump -n6 -e '/1 ":%02X"' /dev/random|/usr/bin/sed s/^://g)
/usr/bin/echo New Mac is $mac_new
#</MAC_ADDR_GENERATE>#

#<GET OLD MAC AND REPLACE>#
mac_old=$(/usr/bin/cat $SRV_TMP_DIR/$new_vm_name.xml |/usr/bin/grep "mac address=")
/usr/bin/echo old mac is $mac_old
/usr/bin/sed -i "s%$mac_old%$mac_new%g" $SRV_TMP_DIR/$new_vm_name.xml
#<GET OLD MAC AND REPLACE>#

#<new_disk_create>#
/usr/bin/qemu-img create -f qcow2 -b $hdd_base_image /var/lib/libvirt/images_write/$new_vm_name.qcow2
#</new_disk_create>#

#<attach_new_disk_in_confiig>#
/usr/bin/echo hdd base image is $hdd_base_image
/usr/bin/sed -i "s%<source file='$hdd_base_image'/>%<source file='/var/lib/libvirt/images_write/$new_vm_name.qcow2'/>%g" $SRV_TMP_DIR/$new_vm_name.xml
#</attach_new_disk_in_confiig>#

starting_vm
    #</Create_vm config>#
}

starting_vm() {

/usr/bin/virsh define $SRV_TMP_DIR/$new_vm_name.xml
/usr/bin/virsh start $new_vm_name
while [ $a -ne 1 ]
do
if /usr/bin/virsh list --all |/usr/bin/grep "$new_vm_name" |/usr/bin/grep "running" > /dev/null 2>&1
then
a=1
/usr/bin/sed -i "/$new_vm_port/d" $SRV_TMP_DIR/recycle.list
/usr/bin/echo $new_vm_port >> $SRV_TMP_DIR/clear.list
/usr/bin/echo "#" "$date" "VM $new_vm_name IS STARTED #"
else
 /usr/bin/echo "#VM $new_vm_name is not ready#"
a=0
/usr/bin/sleep 2s
fi
done
/usr/bin/echo "#$date  EXIT FROM VM_CREATE.SH#"
exit
}

hdd_image_locate

El proceso de creación de una nueva máquina virtual

El script vm_create.sh lee del archivo de configuración el valor de la variable "base_host", que determina la plantilla de la máquina virtual de la cual se hará un clon. Extrae la configuración XML de la VM de la base del hipervisor, realiza una serie de verificaciones del archivo de imagen qcow de la VM y, si todo se completa con éxito, crea un archivo de configuración XML para la nueva VM y una imagen de disco "linked clone" para la nueva VM. Luego, el XML de la nueva VM se carga en la base del hipervisor y se inicia la VM. El puerto "spice_console" se transfiere de recycle.list a clear.list. Se termina la ejecución de vm_create.sh y se finaliza la ejecución de vm_manager.sh.
En la siguiente conexión, todo comienza de nuevo.

Para casos de emergencia, hay un script vm_clear.sh que recorre todas las VM del grupo y las elimina, restableciendo los valores de las listas. Su llamada en la etapa de carga permite comenzar la (incompleta) VDI desde cero.

/home/admin/scripts_vdi_new/vm_clear.sh

#!/usr/bin/sh

#set VARIABLES#
SRV_SCRIPTS_DIR=$(/usr/bin/cat /etc/vm_manager.conf  
|/usr/bin/grep "srv_scripts_dir" |/usr/bin/cut -d "=" -f2)
/usr/bin/echo "SRV_SCRIPTS_DIR=$SRV_SCRIPTS_DIR"
export SRV_SCRIPTS_DIR=$SRV_SCRIPTS_DIR

SRV_TMP_DIR=$(/usr/bin/cat /etc/vm_manager.conf  
|/usr/bin/grep "srv_tmp_dir" |/usr/bin/cut -d "=" -f2)
/usr/bin/echo "SRV_TMP_DIR=$SRV_TMP_DIR"
export SRV_TMP_DIR=$SRV_TMP_DIR

SRV_POOL_SIZE=$(/usr/bin/cat /etc/vm_manager.conf  
|/usr/bin/grep "srv_pool_size" |/usr/bin/cut -d "=" -f2)
/usr/bin/echo "SRV_POOL_SIZE=$SRV_POOL_SIZE"

SRV_START_PORT_POOL=$(/usr/bin/cat /etc/vm_manager.conf  
|/usr/bin/grep "srv_start_port_pool" |/usr/bin/cut -d "=" -f2)
/usr/bin/echo SRV_START_PORT_POOL=$SRV_START_PORT_POOL
#Set VARIABLES#


/usr/bin/echo "= Cleanup ALL VM="

/usr/bin/mkdir $SRV_TMP_DIR

/usr/sbin/service iptables restart
/usr/bin/cat /dev/null > $SRV_TMP_DIR/clear.list
/usr/bin/cat /dev/null > $SRV_TMP_DIR/waste.list
/usr/bin/cat /dev/null > $SRV_TMP_DIR/recycle.list
/usr/bin/cat /dev/null > $SRV_TMP_DIR/conn_wait.list

port_to_delete=$(($SRV_START_PORT_POOL+$SRV_POOL_SIZE))

        while [ "$port_to_delete" -gt "$SRV_START_PORT_POOL" ]
          do
		$SRV_SCRIPTS_DIR/vm_delete.sh $port_to_delete
		port_to_delete=$(($port_to_delete-1))
        done

/usr/bin/echo "= EXIT FROM VM_CLEAR.SH="

Con esto, querría concluir la primera parte de mi relato. Lo expuesto debería ser suficiente para que los administradores del sistema prueben la versión incompleta de VDI en acción. Si la comunidad encuentra este tema interesante, en la segunda parte hablaré sobre la modificación de livecd Fedora y su transformación en un quiosco.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster