KVM (недо)VDI с еднократни виртуални машини с помощта на bash

Кому е предназначена тази статия

Тази статия може да бъде интересна на системни администратори, пред които е стояла задачата да създадат услуга за „еднократни“ работни места.

Преамбула

В отдел ИТ поддръжка на млада динамично развиваща се компания с малка регионална мрежа, се обърнаха с молба да организират „станции за самообслужване“ за използване от техните външни клиенти. Тези станции се предполага да се използват за регистрация на външни портали на компанията, зареждане на данни от външни устройства, работа с държавни портали.

Важно обстоятелство бе фактът, че голяма част от софтуера е „настроен“ за MS Windows (например, „Декларация“), и въпреки движението към открити формати, MS Office остава доминиращ стандарт при обмена на електронни документи. Така че да се откажем от MS Windows при решението на задачата не беше възможно.

Основният проблем се виждаше в възможността за натрупване на различни данни от потребителски сесии, които биха могли да доведат до тяхното изтичане на трети лица. Такава ситуация вече е подведена МФЦ. Но за разлика от квазидържавния (държавно автономно учреждение) МФЦ, за подобни пропуски, недържавните организации ще бъдат наказани значително по-тежко. Следващият критичен проблем бе изискването за работа с външни носители на данни, на които, без съмнение, ще има много злонамерен софтуер. Вероятността за занос на злонамерен софтуер от интернет се оценяваше като по-малко вероятна, заради ограничаването на достъпа до интернет по бял списък на адреси. Към разработката на изискванията се включиха служители от други отдели, внасяйки своите изисквания и желания; финалните изисквания изглеждаха по следния начин:

Изисквания за информационна безопасност

  • След използване, всички потребителски данни (включително временни файлове и ключове на регистъра) трябва да бъдат изтрити.
  • Всички процеси, стартирани от потребителя, след приключване на работата трябва да бъдат завършени.
  • Достъп до интернет по бял списък на адреси.
  • Ограничения за възможността за стартиране на страничен код.
  • В случай на бездействие на сесията над 5 минути, сесията трябва да се завършва автоматично, станцията трябва да извърши почистване.

Изисквания на клиента

  • Брой клиентски станции на клон – не повече от 4.
  • Минималното време за изчакване на готовността на системата, от момента на „седнал за стола“ до започване на работа с клиентския софтуер.
  • Възможност за свързване на периферни устройства (скенери, флашки) директно от мястото на инсталация на „станцията за самообслужване“.
  • Изисквания на клиента
  • Демонстрация на рекламни материали (снимки) по време на простоя на комплекса.

Творчески мъки

След като се наситихме на Windows livecd, стигнахме до единодушното заключение, че полученото решение не задоволява минимум 3 критични точки. Те или се зареждат дълго, или не са съвсем live, или персонализирането им е било свързано с огромни главоболия. Може би не сме търсили добре, и вие можете да предложите набор от инструменти, ще бъда благодарен.

Тогава започнахме да се насочваме към VDI, но за тази задача повечето решения са или твърде скъпи, или изискват внимателно внимание. Искаше се прост инструмент, с минимално количество магия, повечето проблеми с който могат да се решат с обикновен рестарт/презапуск на услугата. За щастие, разполагахме със сървърно оборудване от нисък клас в клоновете, от стартиращия на експлоатация сервис, което можехме да използваме за технологична база.

Какво постигнахме в крайна сметка? Ами, какво постигнахме, не мога да ви разкажа, защото NDA, но в процеса на търсене разработихме интересна схема, която показа добри резултати в лабораторни тестове, макар и да не премина в серийно производство.

Няколко уточнения: авторът не претендира, че предложеното решение напълно решава всички поставени задачи и прави това доброволно и с песен. Авторът предварително се съгласява с твърдението, че Sein Englishe sprache is zehr schlecht. Тъй като решението вече не се развива, няма да можете да разчитате на корекции на грешки или изменения на функционалността, всичко е във вашите ръце. Авторът предполага, че вие поне малко сте запознати с KVM и сте прочели обзорна статия за протокола Spice и малко сте работили с Centos или друг GNU/Linux разпространение.

В тази статия бих искал да разгледам основата на полученото решение, а именно взаимодействието между клиента и сървъра, както и същността на процесите в жизнения цикъл на виртуалните машини в рамките на разглежданото решение. Ако статията се окаже интересна за публиката, ще опиша детайлите на реализацията на live образи за създаване на тънки клиенти на базата на Fedora и ще говоря за детайлите на настройката на виртуалните машини и KVM сървъри за оптимизиране на производителността и защитеността.

Ако вземем цветна хартия,
Боя, четки и лепило,
И още малко сръчност…
Може да направите сто лева!

Схема и описание на тестовата платформа

KVM (недо)VDI с еднократни виртуални машини с помощта на bash

Всичкото оборудване се намира вътре в мрежата на филиала, навън излиза само интернет канал. Прокси сървърът исторически е съществувал, не представлява нищо извънредно. Но именно на него, между другото, ще се извършва филтрацията на трафика от виртуалните машини (съкратено ВМ в текста). Няма пречка този сервис да бъде разположен на KVM сървър, единственото, което трябва да се следи, е как ще се промени натоварването на дисковата подсистема.

Client Station – собствено, „станции за самообслужване“, „фронтенд“ на нашия сервис. Представляват неттопове Lenovo IdeaCentre. Какво е хубавото в този агрегат? Почти всичко, особено радва голямото количество USB портове и картов читатель на лицевата платка. В нашата схема в картовия четец е поставена SD карта с включена хардуерна защита от запис, на която е записан модифицираният live образ Fedora 28. Разбира се, към неттопа е свързан монитор, клавиатура и мишка.

Switch – нищо особено, второ ниво на хардуерния превключвател, стои в сървърната стая и мига с лампички. Не е свързан към никакви мрежи, освен мрежата „станции за самообслужване“.

KVM_Server – ядрото на схемата, в стендовите тестове Core 2 Quad Q9650 с 8 GB оперативна памет уверено поддържаше 3 виртуални машини с Windows 10. Дисковата подсистема – adaptec 3405 2 диска Raid 1 + SSD. В полевите изпитания Xeon 1220 по-сериозният LSI 9260 + SSD лесно поддържаше 5-6 ВМ. Сървърите бяха получени от отпадналия сервис, капиталовите разходи бяха малко. На този(те) сървър(и) е разположена системата за виртуализация KVM с пул от виртуални машини pool_Vm.

Vm — виртуална машина, бекенд на нашия сервис. В нея се осъществява работата на потребителя.

Enp5s0 – мрежов интерфейс, насочен към мрежата на "станции за самообслужване", на него живеят dhcpd, ntpd, httpd, и xinetd слуша "signal" порта.

Lo0 – псевдоинтерфейс за обратна връзка. Стандартен.

Spice_console – Много интересна функция, защото в разликата с класическия RDP, при настройка на KVM+Spice протокол, се появява допълнителен елемент – порт на конзолата на виртуалната машина. Всъщност, когато се свързвате към този TCP порт, получавате конзолата на Vm, без необходимост от свързване към Vm през нейния мрежов интерфейс. Всички взаимодействия с Vm за предаване на сигнала се извършват от сървъра. Най-близкият аналог по функция – IPKVM. Тоест, на този порт се предава изображението на монитора на ВМ, на него също така се предават данни за движението на мишката, и (най-важното) взаимодействието чрез Spice протокол позволява безпроблемно пренасочване на USB устройства във виртуалната машина, сякаш това устройство е свързано директно към самата Vm. Проверено е за USB флаш памети, скенери, уеб-камери.

Vnet0, virbr0 и виртуалните мрежови карти Vm образуват мрежа от виртуални машини.

Как Това работи

От страна на клиентската станция

Клиентската станция зарежда графичен режим от модифициран live образ на Fedora 28, получава IP адрес по dhcp от адресното пространство на мрежата 169.254.24.0/24. В процеса на зареждане се създават правила за фаервола, позволяващи връзки към "signal" и "spice" портовете на сървъра. След завършване на зареждането станцията изчаква авторизация на потребителя "Client". След авторизацията на потребителя стартира мениджъра на работните столове "openbox" и изпълнява скрипта за автоматично стартиране autostart от името на авторизирания потребител. Сред другото, скриптът за автоматично стартиране активира скрипта 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

Описание на променливите във файла client.conf
server_ip — адрес на KVM_Server
vdi_signal_port — порт на KVM_Server, на който "седи" xinetd
vdi_spice_port — мрежов порт на KVM_Server, от който ще се пренасочва заявка за свързване от клиента remote-viewer към spice порта на конкретната Vm (подробности по-долу)
animation_folder — папка, от която се вземат изображения за демонстрация на анимация
background_folder — папка, от която се вземат изображения за демонстрация на презентации в режим на изчакване. Повече за анимацията в следващата част на статията.

Скрипт remote.sh взима настройки от конфигурационного файла /etc/client.conf и устанавливает соединение с помощью nc на порту «vdi_signal_port» KVM сервера, получая поток данных от сервера, среди которых ожидает строки «RULE ADDED, CONNECT NOW». При нахождении искомой строки запускается процесс remote-viewer в киоск-режиме, устанавливая соединение на порту «vdi_spice_port» сервера. Выполнение скрипта приостанавливается до конца исполнения remote-viewer.

Remote-viewer, подключаясь к порту «vdi_spice_port», за счет редиректа со стороны сервера, попадает на порт «spice_console» интерфейса lo0, т.е. на консоль виртуальной машины, где происходит работа пользователя. В процессе ожидания подключения пользователю демонстрируется анимация, представляющая собой слайд-шоу из jpeg файлов, путь к каталогу с изображениями определяется значением переменной animation_folder из конфигурационного файла.

При потере соединения с портом «spice_console» виртуальной машины, сигнализирующего об выключении или перезагрузке виртуальной машины (что фактически завершает сессию пользователя), происходит завершение всех процессов, запущенных от имени авторизовавшегося пользователя, что приводит к перезапуску lightdm и возврату на экран авторизации.

Со стороны KVM Server

На порту «signal» сетевой карты enp5s0 ожидает соединения xinetd. После подключения к порту «signal» xinetd запускает скрипт vm_manager.sh без передачи ему вводных параметров и перенаправляет результаты выполнения скрипта в сессию nc Client Station.

/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

Описание переменных конфигурационного файла vm_manager.conf
srv_scripts_dir — папка для скриптов vm_manager.sh, vm_connect.sh, vm_delete.sh, vm_create.sh, vm_clear.sh
srv_pool_size — размер пула виртуальных машин
srv_start_port_pool — начальный порт, после которого начнется размещение портов spice для виртуальных машин
srv_tmp_dir — папка для временных файлов
base_host — базовая виртуальная машина (золотой образ), на основе которой будут создаваться клоны в пуле
input_iface — сетевой интерфейс сервера, подключенный к Client Stations
vdi_spice_port — сетевой порт сервера, посредством которого будет происходить перенаправление запроса на соединение от клиента remote-viewer к spice порту выделенной виртуальной машины
count_conn_tryes — таймер на изчакване, след изтичането на който се счита, че свързването към Vm не е станало (подробности за работата вижте vm_connect.sh)

Скриптът vm_manager.sh прочита конфигурационния файл от vm_manager.conf, оценява състоянието на виртуалните машини в пула по няколко параметъра, а именно: колко VM са разположени, има ли свободни новосъздадени VM. За целта той прочита файла clear.list, в който се съдържат номерата на портовете 'spice_console' на 'новосъздадените' (вижте по-долу цикъла на създаване на В.М.) виртуални машини и проверява наличието на установена връзка с тях. При откритие на порт с установена мрежова връзка, (което не трябва да се случва) се извежда предупреждение и портът се прехвърля в waste.list. При откритие на първия порт от файла clear.list, с който в момента няма връзка, vm_manager.sh извиква скрипта vm_connect.sh и му предава за параметър номера на този порт.

/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

Скриптът vm_connect.sh добавя правила за фаервола, които създават пренасочване на 'vdi_spice_port' от порта на интерфейса enp5s0 към 'spice console port' на VM, разположен на интерфейса lo0 на сървъра, предаден като параметър на стартиране. Портът се прехвърля в conn_wait.list, VM се счита за чакаща свързване. В сесията на Client Station на порта 'signal' на сървъра се предава низ 'RULE ADDED, CONNECT NOW', който се очаква от стартирания на нея скрипт remote.sh. Започва цикъл на изчакване на връзка с брой опити, определен от стойността на променливата 'count_conn_tryes' от конфигурационния файл. Всяка секунда в сесията nc ще бъде предаван низ 'RULE ADDED, CONNECT NOW' и ще се проверява наличието на установена връзка до порта 'spice_console'.

Ако в определения брой опити не е установена връзка, 'spice_console' портът се прехвърля обратно в clear.list. Изпълнението на vm_connect.sh приключва, възобновява се изпълнението на vm_manager.sh, който стартира цикъл на почистване.

Ако се установи свързване на Client Station с порта «spice_console» на интерфейса lo0, правилата на firewall-а, които създават пренасочване между порта «spice» на сървъра и порта «spice_console», се премахват и по-нататъшното поддържане на връзката се осъществява чрез механизма за определяне на състоянието на firewall-а. В случай на прекъсване на връзката, повторно установяване на свързването с порта «spice_console» няма да е възможно. Портът «spice_console» се прехвърля в waste.list, VM се счита за «мръсен» и не може да се върне в пул «чисти» виртуални машини без да премине през почистване. Изпълнението на vm_connect.sh приключва, възобновява се изпълнението на vm_manager.sh, който стартира цикъл за почистване.

Цикълът за почистване започва с преглед на файла waste.list, в който се прехвърлят номерата на «spice_console» портовете на виртуалните машини, с които е установено свързване. Определя се наличието на активна връзка на всеки «spice_console» порт от списъка. Ако връзката отсъства, се счита, че виртуалната машина вече не се използва и портът се прехвърля в recycle.list и се стартира процес на изтриване на виртуалната машина (вижте по-долу), на която принадлежи този порт. Ако се открие активна мрежова връзка на порта, се счита, че виртуалната машина е в употреба, за нея не се предприемат действия. Ако портът не се слуша, се счита, че VM е изключена и повече не е нужна. Портът се прехвърля в recycle.list и започва процес на изтриване на виртуалната машина. За целта се извиква скриптът vm_delete.sh, на който като параметър се предава номерът на «spice_console» порта на VM, която трябва да бъде изтрита.

/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

Изтриването на виртуална машина е достатъчно тривиална операция, скриптът vm_delete.sh извършва определяне на името на виртуалната машина, на която принадлежи портът, предаден като параметър за стартиране. Извършва се принудително спиране на VM, изтриване на VM от хипервизора, изтриване на виртуалния твърд диск на тази VM. Портът «spice_console» се изтрива от recycle.list. Изпълнението на vm_delete.sh приключва, възобновява се изпълнението на vm_manager.sh.

Скриптът vm_manager.sh, след приключване на операциите по почистване на излишни виртуални машини от списъка waste.list, започва цикъл за създаване на виртуални машини в пул.

Процесът започва с определянето на наличните за разполагане портове „spice_console“. За целта, на базата на параметъра от конфигурационния файл „srv_start_port_pool“, който задава началния порт за пула „spice_console“ виртуални машини, и параметъра „srv_pool_size“, определящ максималния брой виртуални машини, се извършва последователно подбиране на всички възможни варианти на портове. За всеки определен порт се търси в clear.list, waste.list, conn_wait.list, recycle.list. Ако портът бъде открит в който и да е от тези файлове, той се счита за зает и се пропуска. Ако портът не е намерен в посочените файлове, той се добавя в файла recycle.list и започва процесът на създаване на нова виртуална машина. За целта се извиква скриптът vm_create.sh, на който се предава номерът на порта „spice_console“, за който трябва да се създаде 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

Процесът на създаване на нова виртуална машина

Скриптът vm_create.sh чете от конфигурационния файл стойността на променливата „base_host“, която определя образеца на виртуалната машина, на базата на която ще бъде направен клонинг. Извършва се извеждане на XML конфигурацията на VM от хипервизора, извършват се редица проверки на qcow образа на диска на VM и при успешно завършване на тези проверки се създава XML конфигурационен файл за новата VM и „linked clone“ образ на диска на новата VM. След което XML конфигурацията на новата VM се зарежда в базата на хипервизора и VM се стартира. Портът „spice_console“ се прехвърля от recycle.list в clear.list. Изпълнението на vm_create.sh завършва и изпълнението на vm_manager.sh също.
При следващото свързване всичко започва отначало.

За аварийни случаи в комплекта има скрипт vm_clear.sh, който принудително преминава през всички VM от пула и ги изтрива, нулирайки стойностите на списъците. Извикването му на етапа на зареждане позволява да се започне работата (недо)VDI от чисто лист.

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

На това бих искал да завърша първата част от моя разказ. Изложеното би трябвало да е достатъчно за системните администратори, за да опитат недоVDI в действие. Ако общността намери тази тема интересна, във втората част ще разкажа за модификацията на livecd Fedora и превръщането ѝ в киоск.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster