En este post, describo el procedimiento que seguí para configurar un servicio de virtualización en un equipo dedicado. El objetivo principal es marcar la pauta para desarrollar un script que nos permitan automatizar y agilizar el aprovisionamiento de máquinas virtuales; esto con el objetivo personal de siempre tener instalaciones limpias y listas para trabajar. Además, se busca que las máquinas virtuales existan en la la misma red local como si de equipos reales se tratase, y no detrás de una NAT.

Para el servidor de virtualización, usé una vieja laptop (i3, 8GB, 1TB) con una instalación mínima de Debian 11. Todo el procedimiento descrito aquí se hizo desde mi computadora personal mediante una conexión SSH, la cual se encuentra en la misma red local que el servidor.

Instalar Dependencias

Nos conectamos al servidor mediante SSH y procedemos a instalar los paquetes necesarios.

apt install qemu qemu-system qemu-utils libvirt-clients \
libvirt-daemon-system virtinst virt-manager bridge-utils

Habilitamos el servicio de virtualización.

systemctl enable libvirtd

Switch Virtual con el Modo Puente

Figura 1

Figura 1: Representación gráfica de una interfaz física configurada como puente. Fuente

Procedemos a configurar la interfaz de red ethernet del servidor en modo puente (bridge). Esta configuración es la que nos va a permitir “compartir” la tarjeta de red física con las máquinas virtuales; de este modo, la tarjeta se comporta como un switch virtual (Ver Figura 1) por lo que reenviará los paquetes hacia y desde la red local a la que se encuentra conectada.

Para crear el puente, abrimos el archivo /etc/network/interfaces y veremos algo como lo siguiente.

...
auto eth0
iface eth0 inet static
	address 10.0.1.13
	netmask 10.0.1.255
	gateway 10.0.1.1
...

Como puedes apreciar, en mi caso tengo el servidor configurado con un a dirección estática. Si tú estás usando direccionamiento por DHCP, tu archivo se verá mas o menos como el siguiente. Sin embargo, te recomiendo que para estos casos siempre configures una IP estática en tu servidor.

...
auto eth0
iface eth0 inet dhcp
...

NOTA: es importante recalcar que los nombres en las interfaces pueden variar (p. ej. enp1s0, ens3). Además, en el archivo anterior, muy probablemente tendrás varias interfaces (loopback, wireless, etc.). Así que asegurate de que la que configures sea la que realmente quieres usar.

Ahora, lo que debemos hacer en este archivo es mover la configuración de nuestra interfaz física (en mi caso es eth0) a la interfaz en modo puente que vamos a crear. Además, el modo de la interfaz física lo configuraremos como manual. El archivo quedaría como sigue.

...
auto eth0
iface eth0 inet manual

auto br0
iface br0 inet static
	address 10.0.1.13
	netmask 10.0.1.255
	gateway 10.0.1.1
	bridge_ports eth0
	up /usr/sbin/brctl stp br0 on
...

Nótese que las 2 líneas del fondo son específicas para instanciar nuestra nueva interfaz en modo puente (br0). Luego de este paso, procedemos a reiniciar el servidor para aplicar los cambios.

Para confirmar que hemos hecho todo correctamente, procedemos a revisar la configuración IP con el siguiente comando.

ip -c a

Y en la salida deberíamos la nueva interfaz br0 con la dirección que antes tenía la interfaz física (a menos que te hayas decidido por otra). En contraste, la interfaz física no debería tener ninguna IP, como podemos ver en el siguiente fragmento.

...
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master br0 state UP group default qlen 1000
    link/ether XX:XX:XX:XX:XX:XX brd ff:ff:ff:ff:ff:ff
...
4: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether XX:XX:XX:XX:XX:XX brd ff:ff:ff:ff:ff:ff
    inet 10.0.1.13/24 brd 10.0.1.255 scope global br0
       valid_lft forever preferred_lft forever
...

Redirección del Servidor Gráfico para virt-manager

NOTA: este paso es opcional por si quieres utilizar el cliente gráfico.

Al incio de la publicación instalamos Virtual Machine Manager (o virt-manager); el cual, es una herramienta gráfica que simplifica la configuración. Sin embargo, no queremos estar parados frente al servidor. El punto es gestionar todo de forma remota. Y si queremos usar el administrador gráfico como si lo tuviéramos instalado en nuestra computadora necesitamos redireccionar el sistema de ventanas X mediante SSH. De esta forma, podremos ejecutar aplicaciones en el servidor, pero las veremos en nuestra estación de trabajo.

Para activar el redireccionamiento, vamos a /etc/ssh/sshd_config y modificamos las siguientes líneas.

X11Forwarding yes
X11UseLocalhost no

Finalmente, aplicar los cambios procedemos a reiniciar el servicio SSH.

systemctl restart sshd

Ahora, cada que queramos usar el administrador gráfico virt-manager, podremos hacerlo conectándonos por SSH usando el parámetro -X. Por ejemplo:

ssh root@10.0.1.13 -X

# y una vez logueados ejecutamos...

virt-manager

Y después de unos segundos veremos la ventana de virt-manager en nuestra estación de trabajo, desde donde podremos administrar nuestras máquinas virtuales, volúmenes de almacenamiento, redes virtuales, y en general casi todas las operaciones de virtualización que podríamos necesitar.

Estructura de Directorios

Por defecto, el almacenamiento de las máquinas virtuales se crea bajo la ruta /var/lib/libvirt; sin embargo, yo pondré todos mis datos en /home/virt_data ya que /home es la partición donde tengo más espacio y planeo usarlo todo para las máquinas virtuales. Siguiendo ese propósito, me di a la tarea de crear la siguiente estructura de directorios, la cual ya tiene algunos elementos que explicaré a continuación.

 /home/virt_data/
    ├── debian/
    │   ├── interfaces
    │   ├── new_debian.sh
    │   └── resolv.conf
    └── pools/
        ├── domains/
        │   ├── hacklab-w2012.qcow2
        ├── images/
        │   ├── debian-10-nocloud-amd64.qcow2
        └── isos/
            └── windows2012R2ServerEvaluationUS.iso

Dentro de /home/virt_data tengo un directorio debian en donde tengo (de momento) sólo un script que uso para aprovisionar máquinas virtuales de dicho sistema operativo. Además, hay también un par de archivos que uso para la configuración de red de la VM en cuestión. Pero hablaremos de eso más adelante. El directorio pools contiene los recursos de almacenamiento y gestión de las VM. Cada directorio con un propósito específico:

  • domains almacena el disco de todas las VM, tanto encendidas como apagadas.
  • images almacena imagenes tipo “cloud” (plantillas).
  • isos almacena las imágenes ISO para instalaciones convencionales (no usado en este ejercicio).

Considerando lo anterior, podemos notar que hay 1 máquina virtual instanciada (hacklab-w2012); ésta es una instancia de Windows Server y fué creada usando el procedimiento manual de instalación (arrancar la ISO, particionar, instalar, etc.). Por otro lado, en el directorio images existe una imagen de Debian 10 acondicionada para funcionar en la nube. Estas imágenes se suelen usar como base para generar instalaciones personalizadas en entornos de nube, pero sin realizar el proceso de instalación estándar, y es la que vamos a utilizar para crear un script de nos permita generar instancias personalizadas en cuestión de minutos.

Implementación de los Recursos de Almacenamiento

Ahora, veamos el procedimiento para generar la estructura mencionada en la sección anterior. Primero que nada, necesitamos registrar los 3 pools de almacenamiento que mencionamos: domains, images e isos. Estos pools ya contienen información, por lo que los definimos de tipo directorio y configuramos el encendido automático para tenerlos siempre disponibles en caso de que queramos marcar una VM con arranque automático.

for pool in {domains,images,isos}; do
    virsh pool-define-as --name $pool --type dir --target /home/virt_data/pools/$pool
    virsh pool-start $pool
    virsh pool-autostart $pool
done

Podemos verificar que los 3 volúmenes estén activos con el siguiente comando.

virsh pool-list --all

Es posible que aparezcan otros pools como default y boot; éstos son creados por libvirt automáticamente y tampoco los usamos en este caso.

Aprovisionamiento de VMs

Ahora que ya tenemos configurados los recursos, podemos comenzar a configurar nuestra primera instancia de Debian 10. Los pasos que vamos a realizar ahora los podremos integrar en un script que podamos llamar siempre que necesitemos una máquina virtual con características similares.

Vamos a suponer que necesitamos una VM con las siguientes características:

  • 1GB de RAM
  • 1 CPU
  • 20 GB de almacenamiento
  • Acceso por SSH a un usuario (sysadmin) sin privilegios con una clave pública designada
  • Software instalado: vim, wget, nmap y autocompletado de bash.
  • Que la máquina esté en la misma red local que la estación de trabajo, y con la dirección 10.0.1.30.

Entonces, primero tomamos nuestra imagen plantilla de nuestro directorio images, y creamos una copia en el directorio domains (recordemos que los volúmenes de las instancias irán en este directorio), y esta copia es sobre la que vamos a trabajar. De esta forma, siempre tendremos la plantilla base limpia y lista para crear VMs.

cd /home/virt_data/pools
cp -v images/debian-10-nocloud-amd64.qcow2 domains/vm01.qcow2

Ya ubicada nuestra plantilla en su destino, procedemos a redimensionarla, ya que originalmente tiene un tamaño virtual de 2GB, y nosotros necesitamos 20G.

qemu-img resize domains/vm01.qcow2 20G

Generamos los archivos de configuración de red que posteriormente cargaremos a la imagen. El primer archivo es para el direccionamiento IP, y el segundo para la resolución DNS.

# archivo 'interfaces'
source /etc/network/interfaces.d/*

auto lo
iface lo inet loopback

auto enp1s0
iface enp1s0 inet static
	address 10.0.0.30
	netmask 255.255.255.0
	network 10.0.0.0
	broadcast 10.0.0.255
	gateway 10.0.0.1
# archivo `resolv.conf`
nameserver 8.8.8.8
nameserver 8.8.4.4

Ahora si, procedemos a modificar la imagen con los parámetros que definimos antes. Además, agregamos un par de modificaciones que solucionan detalles sin mayor relevancia.

virt-customize \
	-a domains/vm01.qcow2 \
	--hostname vm01 \
	--upload interfaces:/etc/network/interfaces \
	--upload resolv.conf:/etc/resolv.conf \
	--run-command "echo 127.0.0.1 $NAME >> /etc/hosts" \
	--run-command "useradd -r -m -s /bin/bash sysadmin" \
	--root-password password:toor \
	--password sysadmin:password:sysadmin \
	--ssh-inject "sysadmin:file:/root/rsa_keys/key.pub" \
	--install "vim,wget,nmap,bash-completion,openssh-server" \
	--firstboot-command "dpkg-reconfigure -f noninteractive openssh-server" \

En este punto, ya tenemos nuestra imagen modificada, la cual vamos a pasar al siguiente comando para instanciar y activar la máquina virtual. Este último comando lleva el resto de la configuración.

virt-install \
	--name vm01 \
	--memory 1024 \
	--vcpus 1 \
	--disk domains/vm01.qcow2,device=disk,bus=virtio,format=qcow2 \
	--virt-type kvm \
	--os-variant debian10 \
	--network bridge=br0,model=virtio \
	--noautoconsole \
	--import

Luego de este comando, la imagen ya estará activa y funcionando como es esperado. Sin embargo, la primera vez hay que esperar unos minutos para que se realice el procedimiento de instalación automático.

En la siguiente imagen, podemos ver la salida de la ejecución de todos los pasos del aprovisonamiento integrados en un script.

prov-vm

Y finalmente, la prueba de conexión a la máquina virtual desde la estación de trabajo.

conn-vm

El script sería el siguiente.

#!/bin/bash
NAME=vm01
MEMORY=1024
CPUS=1
DISK=20G
IP=10.0.1.30
BRIDGE=br0
ROOT_PASSWORD=toor
USER=sysadmin
USER_PASSWORD=sysadmin
PUBLIC_KEY=/root/rsa_keys/t430s.pub
CONFIG=/home/virt_data/debian
DOMAINS=/home/virt_data/pools/domains
IMAGES=/home/virt_data/pools/images
NEW_IMAGE=$DOMAINS/$NAME.qcow2
BASE_IMAGE=$IMAGES/debian-10-nocloud-amd64.qcow2

echo "--- Copying '$BASE_IMAGE' -> '$NEW_IMAGE'..."
cp $BASE_IMAGE $NEW_IMAGE -v

echo "--- Extending '$NEW_IMAGE' to $DISK..."
qemu-img resize $NEW_IMAGE $DISK

sed -i "s/{IPADDR}/$IP/" $CONFIG/interfaces

echo "--- Customizing the image..."
virt-customize \
	-a $NEW_IMAGE \
	--hostname $NAME \
	--upload $CONFIG/interfaces:/etc/network/interfaces \
	--upload $CONFIG/resolv.conf:/etc/resolv.conf \
	--run-command "echo 127.0.0.1 $NAME >> /etc/hosts" \
	--run-command "useradd -r -m -s /bin/bash $USER" \
	--root-password password:$ROOT_PASSWORD \
	--password $USER:password:$USER_PASSWORD \
	--ssh-inject "$USER:file:$PUBLIC_KEY" \
	--update \
	--install "nmap,vim,wget,bash-completion,openssh-server" \
	--firstboot-command "dpkg-reconfigure -f noninteractive openssh-server" \

sed -i "s/$IP/{IPADDR}/" $CONFIG/interfaces

echo "--- Aprovisioning new VM..."
virt-install \
	--name $NAME \
	--memory $MEMORY \
	--vcpus $CPUS \
	--disk $NEW_IMAGE,device=disk,bus=virtio,format=qcow2 \
	--virt-type kvm \
	--os-variant debian10 \
	--network bridge=$BRIDGE,model=virtio \
	--noautoconsole \
	--import

Cabe mencionar que hicimos algunas mejoras adicionales para centralizar las partes del código que deberían modificarse si queremos alterar los parámetros. Aunque el procedimiento no es completamente automático, se acerca un poco a la interacción que tenemos con un servicio de cómputo en la nube ya que nos ahorra bastante tiempo.

Finalmente, si queremos desactivar o eliminar una VM, tenemos los siguientes comandos.

# ver vms activas
virsh list
# parar una vm
virsh shutdown <name>
# eliminar una vm (sin borrar el recurso de almacenamiento)
virsh undefine <name>
# eliminar una vm (con todo y recurso)
virsh undefine <name> --remove-all-storage

Se pueden hacer muchas más cosas, pero de momento lo dejaré aquí. Para más información, recomiendo revisar los manuales de virsh, virt-install, virt-customize y el resto de utilidades de libvirt y QEMU.