KVM (sub)VDI com máquinas virtuais descartáveis usando bash

Para quem é este artigo

Este artigo pode ser de interesse para administradores de sistemas que enfrentam a tarefa de criar um serviço de "estações de trabalho descartáveis".

Prólogo

O departamento de TI de uma jovem e dinâmica empresa em expansão com uma pequena rede regional solicitou a criação de "estações de autoatendimento" para uso por seus clientes externos. Essas estações deveriam ser usadas para registro em portais externos da empresa, upload de dados a partir de dispositivos externos e trabalho com portais governamentais.

Um aspecto importante era o fato de que a maior parte do software estava "adaptado" para MS Windows (por exemplo, "Declaração"), e apesar da movimentação em direção a formatos abertos, o MS Office continua sendo o padrão dominante para a troca de documentos eletrônicos. Portanto, não podíamos abrir mão do MS Windows para resolver essa questão.

O principal problema parecia ser a possibilidade de acumulação de vários dados das sessões dos usuários, que poderiam levar ao vazamento para terceiros. Essa situação já prejudicou os MFCs. Mas ao contrário do MFC (instituição governamental autônoma), organizações não governamentais seriam punidas significativamente mais severamente por tais falhas. A próxima questão crítica foi a exigência de trabalhar com dispositivos de armazenamento externos, nos quais, sem dúvida, haveria uma infinidade de malwares. A probabilidade de infecção por malwares a partir da internet foi considerada menos provável, devido à limitação do acesso à internet a uma lista branca de endereços. Colaboradores de outros departamentos se envolveram na elaboração dos requisitos, adicionando suas exigências, e os requisitos finais foram os seguintes:

Requisitos de Segurança da Informação

  • Após o uso, todos os dados do usuário (incluindo arquivos temporários e chaves do registro) devem ser excluídos.
  • Todos os processos iniciados pelo usuário devem ser finalizados após o término do trabalho.
  • Acesso à internet deve ser limitado a lista branca de endereços.
  • Restrições sobre a possibilidade de execução de código de terceiros.
  • Se a sessão ficar inativa por mais de 5 minutos, a sessão deve ser encerrada automaticamente e a estação deve realizar uma limpeza.

Requisitos do cliente

  • Número máximo de estações de clientes por filial – 4.
  • Tempo mínimo de espera pela prontidão do sistema, do momento em que "sentou na cadeira" até o início do uso do software do cliente.
  • Capacidade de conectar dispositivos periféricos (escâneres, pen drives) diretamente no local de instalação da "estação de autoatendimento".
  • Desejos do cliente
  • Demonstração de materiais publicitários (imagens) durante o tempo de inatividade do complexo.

As dificuldades da criatividade

Após muitas tentativas com live CDs do Windows, chegamos à conclusão unânime de que a solução resultante não atende a pelo menos 3 pontos críticos. Eles ou carregam lentamente, ou não são realmente live, ou a personalização foi acompanhada de muitas dificuldades. Talvez nossa pesquisa tenha sido insuficiente, e você consiga sugerir um conjunto de ferramentas, ficarei agradecido.

Em seguida, começamos a olhar para VDI, mas para essa tarefa a maioria das soluções ou é muito cara, ou exige uma atenção intensa. Era desejável uma ferramenta simples, com o mínimo de «mágica», e cujos problemas poderiam ser resolvidos com um simples reinício/reinício do serviço. Felizmente, tínhamos hardware de servidor, de classe baixa nas filiais, de um serviço em descontinuação, que poderíamos usar como base tecnológica.

Qual foi o resultado final? Bem, o que resultou, não posso lhe contar, pois é confidencial, mas durante as buscas desenvolvemos um esquema interessante, que se mostrou eficaz em testes laboratoriais, embora não tenha sido produzido em série.

Um pequeno disclaimer: o autor não reivindica que a solução proposta resolva completamente todas as tarefas e o faça de forma fácil e divertidamente. O autor reconhece antecipadamente que seu inglês é muito ruim. Como a solução não está mais em desenvolvimento, não podemos contar com correções de bugs ou alterações de funcionalidade, tudo está em suas mãos. O autor assume que você está pelo menos um pouco familiarizado com KVM e leu um artigo introdutório sobre o protocolo Spice e trabalhou um pouco com Centos ou outra distribuição GNU Linux.

Neste artigo, gostaria de discutir a estrutura da solução gerada, especificamente a interação entre cliente e servidor e a essência dos processos do ciclo de vida das máquinas virtuais no contexto da solução abordada. Se o artigo interessar ao público, descreverei os detalhes da implementação de imagens live para criação de clientes leves baseados em Fedora e falarei sobre os detalhes da configuração das máquinas virtuais e servidores KVM para otimização de desempenho e segurança.

Se pegarmos papel colorido,
tintas, pincéis e cola,
e um pouco de habilidade...
pode-se fazer cem reais!

Esquema e descrição do banco de testes

KVM (sub)VDI com máquinas virtuais descartáveis usando bash

Todo o equipamento está localizado dentro da rede da filial, com um canal de internet saindo para fora. O servidor proxy já existia historicamente, não apresenta nada extraordinário. Mas é nele que, entre outras coisas, ocorrerá a filtragem do tráfego das máquinas virtuais (abreviado como VM no texto). Nada impede que esse serviço seja hospedado em um servidor KVM, a única coisa a se observar é como a carga dele irá mudar no subsistema de armazenamento.

Estação do Cliente – na verdade, as 'estações de autoatendimento', o 'frontend' do nosso serviço. Representam-se como nettops Lenovo IdeaCentre. O que torna esse equipamento bom? Quase tudo, especialmente a grande quantidade de portas USB e o leitor de cartão no painel frontal. Em nosso esquema, um cartão SD com proteção contra gravação está inserido no leitor, onde está gravada uma imagem live modificada do Fedora 28. Naturalmente, o nettop está conectado a um monitor, teclado e mouse.

Switch – um switch de hardware de segundo nível nada notável, que está no servidor e pisca as luzes. Não está conectado a nenhuma rede, exceto à rede das 'estações de autoatendimento'.

Servidor KVM – o núcleo do esquema, nos testes de bancada, um Core 2 Quad Q9650 com 8 GB de RAM aguentou confortavelmente 3 máquinas virtuais com Windows 10. O subsistema de armazenamento – Adaptec 3405 com 2 discos em RAID 1 + SSD. Em testes de campo, um Xeon 1220, com um LSI 9260 mais poderoso + SSD, facilmente suportou de 5 a 6 VMs. Os servidores foram obtidos de um serviço descontinuado, onde os custos de capital seriam baixos. Neste(s) servidor(es) está implementado o sistema de virtualização KVM com um pool de máquinas virtuais pool_Vm.

VM – máquina virtual, o backend do nosso serviço. É nela que ocorre o trabalho do usuário.

Enp5s0 – interface de rede voltado para a rede das "estações de autoatendimento", onde residem dhcpd, ntpd, httpd, e xinetd escuta na porta "signal".

Lo0 – pseudointerface de loopback. Padrão.

Spice_console – Uma coisa muito interessante, pois, ao contrário do RDP clássico, ao implementar a combinação KVM + protocolo Spice, surge uma entidade adicional – a porta da console da máquina virtual. Basicamente, ao conectar-se a esta porta TCP, obtemos o console da Vm, sem a necessidade de conectar à Vm através de sua interface de rede. Todo o gerenciamento de sinal com a Vm é assumido pelo servidor. O analógico mais próximo em termos de funcionalidade – IPKVM. Ou seja, nesta porta é transmitida a imagem do monitor da Vm, e também são enviadas informações sobre o movimento do mouse e (o mais importante) a interação pelo protocolo Spice permite direcionar dispositivos USB para a máquina virtual como se este dispositivo estivesse conectado à própria Vm. Testado para pen drives, scanners, câmeras web.

Vnet0, virbr0 e as interfaces de rede virtuais da Vm formam uma rede de máquinas virtuais.

Como ISTO funciona

Do lado da Estação Cliente

A estação cliente inicia em modo gráfico a partir de uma imagem live modificada do Fedora 28, obtendo um endereço IP via dhcp do espaço de endereços da rede 169.254.24.0/24. Durante o processo de inicialização, são criadas regras de firewall que permitem conexões às portas "signal" e "spice" do servidor. Após a inicialização, a estação aguarda a autorização do usuário "Client". Após a autorização do usuário, o gerenciador de área de trabalho "openbox" é iniciado e o script de autoexecução autostart é executado em nome do usuário autorizado. Entre outras coisas, o script de autoexecução inicia o 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

Descrição das variáveis do arquivo client.conf
server_ip — endereço do KVM_Server
vdi_signal_port — porta do KVM_Server onde está o xinetd
vdi_spice_port — porta de rede do KVM_Server, da qual será feito o redirecionamento do pedido de conexão do cliente remote-viewer para a porta spice da Vm específica (detalhes abaixo)
animation_folder — pasta de onde são retiradas as imagens para demonstrar animações bullshit
background_folder — pasta de onde são retiradas as imagens para demonstrações de apresentações em modo de espera. Mais detalhes sobre animações na próxima parte do artigo.

O script remote.sh obtém as configurações do arquivo de configuração /etc/client.conf e, utilizando o nc, conecta-se na porta "vdi_signal_port" do servidor KVM e recebe um fluxo de dados do servidor, entre os quais espera as linhas "RULE ADDED, CONNECT NOW". Ao receber a linha desejada, o processo remote-viewer é iniciado em modo quiosque, estabelecendo uma conexão na porta "vdi_spice_port" do servidor. A execução do script é suspensa até a conclusão do remote-viewer.

O remote-viewer, ao conectar-se na porta "vdi_spice_port", através do redirecionamento do lado do servidor, acessa a porta "spice_console" da interface lo0, ou seja, a console da máquina virtual, permitindo que o usuário trabalhe diretamente. Durante o tempo de espera pela conexão, uma animação de preenchimento, na forma de um slideshow de arquivos jpeg, é exibida ao usuário; o caminho para o diretório com as imagens é determinado pelo valor da variável animation_folder no arquivo de configuração.

Ao perder a conexão com a porta "spice_console" da máquina virtual, sinalizando a desligamento/reinício da máquina virtual (ou seja, o término efetivo da sessão do usuário), são finalizados todos os processos iniciados em nome do usuário autenticado, o que leva ao reinício do lightdm e ao retorno à tela de autenticação.

Do lado do Servidor KVM

Na porta "signal" do adaptador de rede enp5s0, o xinetd aguarda conexões. Após a conexão na porta "signal", o xinetd inicia o script vm_manager.sh sem passar quaisquer parâmetros de entrada, redirecionando o resultado da execução do script para a sessão nc da Estação 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

Descrição das variáveis do arquivo de configuração vm_manager.conf
srv_scripts_dir — pasta onde os scripts vm_manager.sh, vm_connect.sh, vm_delete.sh, vm_create.sh, vm_clear.sh estão localizados
srv_pool_size — tamanho do pool Vm
srv_start_port_pool — porta inicial, após a qual os ports das consoles spice das máquinas virtuais serão alocados
srv_tmp_dir — pasta para colocar arquivos temporários
base_host — Vm base (imagem padrão) a partir da qual clones Vm serão feitos no pool
input_iface — interface de rede do servidor voltada para as Estações Clientes
vdi_spice_port — porta de rede do servidor, a partir da qual a solicitação de conexão do cliente remote-viewer será redirecionada para a porta spice da Vm dedicada
count_conn_tryes — é o temporizador de espera, após o qual é considerado que a conexão com a VM não ocorreu (veja detalhes no vm_connect.sh)

O script vm_manager.sh lê o arquivo de configuração vm_manager.conf, avalia o estado das máquinas virtuais no pool com base em vários parâmetros, nomeadamente: quantas VMs estão implantadas e se há VMs limpas disponíveis. Para isso, lê o arquivo clear.list, que contém os números das portas "spice_console" das VMs "recentemente criadas" (veja o ciclo de criação de VMs abaixo) e verifica a existência de conexão estabelecida com elas. Ao detectar uma porta com conexão de rede estabelecida (o que não deve acontecer), um aviso é emitido e a porta é movida para waste.list. Quando a primeira porta do arquivo clear.list é encontrada sem conexão neste momento, o vm_manager.sh chama o script vm_connect.sh e passa o número dessa porta como parâmetro.

/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

O script vm_connect.sh adiciona regras de firewall que criam um redirecionamento da porta "vdi_spice_port" do servidor na interface enp5s0 para a porta "spice console port" da VM, localizada na interface lo0 do servidor, passada como parâmetro de execução. A porta é movida para conn_wait.list e a VM é considerada aguardando conexão. Na sessão Client Station, uma linha "RULE ADDED, CONNECT NOW" é enviada pela porta "signal" do servidor, a qual é aguardada pelo script remote.sh em execução. Começa um ciclo de espera pela conexão, com o número de tentativas determinado pelo valor da variável "count_conn_tryes" do arquivo de configuração. A cada segundo, a linha "RULE ADDED, CONNECT NOW" será enviada para a sessão nc, e a existência da conexão estabelecida com a porta "spice_console" será verificada.

Se, após o número de tentativas estabelecido, não houver conexão, a porta "spice_console" é novamente movida para clear.list. A execução do vm_connect.sh é encerrada, retornando à execução do vm_manager.sh, que inicia o ciclo de limpeza.

Se a conexão da Client Station for feita à porta "spice_console" na interface lo0, as regras do firewall que criam o redirecionamento entre a porta "spice" do servidor e a porta "spice_console" são removidas, e a manutenção da conexão ocorre por meio do mecanismo de detecção de estado do firewall. No caso de uma interrupção na conexão, não será possível restabelecer a conexão com a porta "spice_console". A porta "spice_console" é movida para waste.list, a VM é considerada "suja" e não conseguirá voltar ao pool de máquinas virtuais "limpas" sem passar por uma limpeza. A execução de vm_connect.sh é encerrada, e a execução de vm_manager.sh é reiniciada, que inicia o ciclo de limpeza.

O ciclo de limpeza começa com a verificação do arquivo waste.list, que contém os números das portas "spice_console" das máquinas virtuais às quais a conexão foi estabelecida. É determinada a presença de uma conexão ativa em cada porta "spice_console" da lista. Se a conexão estiver ausente, considera-se que a máquina virtual não está mais sendo usada e a porta é movida para recycle.list, iniciando o processo de remoção da máquina virtual (veja abaixo), à qual esta porta pertencia. Se for detectada uma conexão de rede ativa na porta, considera-se que a máquina virtual está em uso, e nenhuma ação será tomada. Se a porta não estiver sendo escutada, considera-se que a VM está desligada e não é mais necessária. A porta é movida para recycle.list e inicia-se o processo de remoção da máquina virtual. Para isso, o script vm_delete.sh é chamado, ao qual é passado como parâmetro o número da porta "spice_console" da VM que precisa ser removida.

/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

A remoção da máquina virtual é uma operação relativamente trivial; o script vm_delete.sh determina o nome da máquina virtual a qual a porta, passada como parâmetro de execução, pertence. A VM é forçosamente parada, removida do hipervisor e o disco rígido virtual desta VM é apagado. A porta "spice_console" é removida do recycle.list. A execução de vm_delete.sh é encerrada, e a execução de vm_manager.sh é reiniciada.

O script vm_manager.sh, ao finalizar as operações de limpeza de máquinas virtuais desnecessárias da lista waste.list, inicia o ciclo de criação de máquinas virtuais no pool.

O processo começa com a definição das portas disponíveis para a hospedagem do "spice_console". Para isso, com base no parâmetro do arquivo de configuração "srv_start_port_pool", que determina a porta inicial para o pool de "spice_console" das máquinas virtuais, e o parâmetro "srv_pool_size", que define o número máximo de máquinas virtuais, são examinadas todas as possíveis opções de porta. Para cada porta específica, realiza-se uma busca nos arquivos clear.list, waste.list, conn_wait.list e recycle.list. Se a porta for encontrada em qualquer um dos arquivos mencionados, ela é considerada ocupada e é ignorada. Caso a porta não seja encontrada nos arquivos indicados, ela é adicionada ao arquivo recycle.list e inicia-se o processo de criação de uma nova máquina virtual. Para isso, chama-se o script vm_create.sh, ao qual é passado como parâmetro o número da porta "spice_console" para a qual a VM deve ser criada.

/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

Processo de criação de uma nova máquina virtual

O script vm_create.sh lê do arquivo de configuração o valor da variável "base_host", que determina o modelo da máquina virtual da qual será feito o clone. Ele extrai a configuração XML da VM do banco de dados do hipervisor, realiza uma série de verificações na imagem de disco qcow da VM e, ao concluir com sucesso, cria um arquivo de configuração XML para a nova VM e a imagem de disco "linked clone" da nova VM. Após isso, a configuração XML da nova VM é carregada no banco de dados do hipervisor e a VM é iniciada. A porta "spice_console" é transferida de recycle.list para clear.list. A execução do vm_create.sh é concluída e a execução do vm_manager.sh se encerra.
Na próxima conexão, tudo começa do zero.

Para situações de emergência, há um script vm_clear.sh que forçadamente percorre todas as VMs do pool e as remove, reiniciando os valores das listas. Chamá-lo na etapa de inicialização permite começar o (pré)VDI com uma página em branco.

/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="

Aqui, gostaria de encerrar a primeira parte do meu relato. O que foi exposto deve ser suficiente para que os administradores de sistema experimentem o pré-VDI na prática. Se a comunidade achar o tema interessante, na segunda parte, falarei sobre a modificação do livecd Fedora e sua transformação em um quiosque.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster