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


Данная статья может быть интересна системным администраторам, перед которыми вставала задача создать сервис «одноразовых» рабочих мест.

Пролог


В отдел ИТ сопровождения молодой динамично развивающейся компании с небольшой региональной сетью, обратились с просьбой организовать «станции самообслуживания» для использования их внешними клиентами. Данные станции предполагалось использовать для регистрации на внешних порталах компании, загрузки данных с внешних устройств, работы с государственными порталами.

Важным аспектом являлся тот факт, что большая часть программного обеспечения «заточена» под MS Windows (например, «Декларация»), а несмотря на движение в сторону открытых форматов, MS Office остается доминирующим стандартом при обмене электронными документами. Таким образом, отказаться от MS Windows при решении данной задачи мы не могли.

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

Требования ИБ

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

Требования заказчика

  • Количество клиентских станций на филиал – не более 4-х.
  • Минимальное время ожидания готовности системы, от момента «сел за стул» до начала работы с клиентским ПО.
  • Возможность подключения периферийных устройств (сканеры, флэшки) непосредственно с места установки «станции самообслуживания».
  • Пожелания заказчика
  • Демонстрация рекламных материалов (картинки) в момент простоя комплекса.

Муки творчества


Вдоволь наигравшись с виндовыми livecd, мы пришли к единодушному выводу, что получающееся решение не удовлетворяет минимум 3 критичным пунктам. Они либо долго грузятся, либо не совсем live, либо кастомизация их была сопряжена с дикими болями. Возможно, мы плохо искали, и вы сможете посоветовать набор какой-то инструментов, буду благодарен.

Дальше мы стали смотреть в сторону VDI, но для этой задачи большинство решений либо слишком дороги, либо требуют пристального внимания. А хотелось простой инструмент, с минимальным количеством магии, большинство проблем которого можно было бы решить простой перезагрузкой/перезапуском сервиса. К счастью, у нас было серверное оборудование, low end класса в филиалах, от выводящегося из эксплуатации сервиса, которое мы могли использовать для технологической базы.

Что в итоге получилось? А вот, что в итоге получилось, я вам рассказать не смогу, ибо NDA, но в процессе поисков мы разработали интересную схему, которая хорошо себя показала в лабораторных испытаниях, хоть и не пошла в серию.

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

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

Если взять цветной бумаги,
Краски, кисточки и клей,
И еще чуть-чуть сноровки…
Можно сделать сто рублей!

Схема и описание тестового стенда




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

Client Station – собственно, «станции самообслуживания», «фронтенд» нашего сервиса. Представляют из себя неттопы Lenovo IdeaCentre. Чем хорош этот агрегат? Да почти всем, особенно радует большое количество USB разъемов и кардридер на лицевой панели. В нашей схеме в кардридер вставлена SD карта с включенной аппаратной защитой от записи, на которою записан модифицированный live образ Fedora 28. Само собой, к неттопу подключен монитор, клавиатура и мышь.

Switch – ничем не примечательный аппаратный switch второго уровня, стоит в серверной и мигает лампочками. Ни к каким сетям, кроме сети «станций самообслуживания» не подключен.

KVM_Server – ядро схемы, в стендовых испытаниях Core 2 Quad Q9650 с 8 Гб оперативной памяти уверенно тянул на себе 3 виртуальных машины с Windows10. Дисковая подсистема – 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 protocol, появляется дополнительная сущность – порт консоли виртуальной машины. Фактически, подключаясь на этот TCP порт, мы получаем консоль Vm, без необходимости подключатся к Vm через её сетевой интерфейс. Всё взаимодействие с Vm по передаче сигнала, сервер берет на себя. Ближайший аналог по функции – IPKVM. Т.е. на этот порт передается изображение монитора ВМ, на него же передаются данные о перемещении мыши, и (что самое главное) взаимодействие через Spice протокол позволяет бесшовно перенаправлять USB устройства в виртуальную машину, словно это устройство подключено к самой Vm. Проверено для флэш накопителей, сканеров, вэб-камер.

Vnet0, virbr0 и виртуальные сетевые карты Vm образуют сеть виртуальных машин.

Как ЭТО работает


Со стороны Client Station

Клиентская станция загружается в графическом режиме с модифицированного 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 — папка, откуда берутся изображения для демонстрации bullshit animation
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 т.е. на консоль виртуальной машины и происходит, непосредственно, работа пользователя. В процессе ожидания подключения, пользователю демонстрируется bullshit animation, в виде слайд-шоу из 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.conf
srv_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 — размер пула Vm
srv_start_port_pool — начальный порт, после которого начнется расположение портов spice консолей виртуальных машин
srv_tmp_dir — папка для размещения временных файлов
base_host — базовая Vm (золотой образ) с которого будут делаться клоны Vm в пул
input_iface — сетевой интерфейс сервера, смотрящий в сторону Client Stations
vdi_spice_port — сетевой порт сервера, с которого будет происходить перенаправление запроса на соединение от remote-viewer клиента к spice порту выделенной Vm
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, правила файерволла создающие редирект между «spice» портом сервера и «spice_console» портом удаляются и дальнейшее поддержание соединения происходит за счет механизма определения состояния файерволла. В случае разрыва соединения, повторно установить связь с «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 из пула и удаляет их с обнулением значений list-ов. Вызов его на этапе загрузки позволяет начать работу (недо)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 и превращения ее в киоск.

Комментарии (12)


  1. kvaps
    02.08.2019 15:38

    Хех, респект!


    Как-то раз я делал что-то подобное но на базе OpenNebula.
    Как адепту shell-скриптинга, я бы очень советовал вам взглянуть на неё.


    OpenNebula очень простая и гибкая платформа, она может выступать в качестве удобного фремворка, для создания и управления VM, а поверх неё можно реализовать любую логику.


    1. Ion_Storm Автор
      02.08.2019 19:24

      Очень интересно, с каким гипервизором использовали? Много ли проблем возникало в эксплуатации?


      1. kvaps
        02.08.2019 22:44

        Гипервизор — KVM
        Проблем, конкретно с использованием OpenNebula, почти не возникало. Все что были — либо особенности, либо достаточно легкоразрешимые, особенно с условием того что почти вся логика в OpenNebula реализованна в виде простых bash-скриптов.


        1. Ion_Storm Автор
          03.08.2019 06:15

          Обязательно посмотрю, поиграюсь. Мы сознательно упрощали схему. Хотелось штуку простую как лопата, устойчивую к отказам электроснабжения и с низкими трудозатратами по сопровождению.


  1. mikeus
    03.08.2019 01:17

    У вас какая-то присказка из разряда «от тоби, небоже, що нам не гоже». А вариант просто поставить на клиентские станции винду, и при логауте пользователя восстанавливать состояние системы из теневой копии тома с перезагрузкой не пробовали сделать?


    1. Ion_Storm Автор
      03.08.2019 05:58

      Скажем так, это серебряный призер внутреннего отбора. Не так уж и не гоже, да? От варианта с локальным восстановлением мы и хотели уйти, слишком оно трудозатратное по сопровождению. Обновления как софта, так и системы, в этом случае требует отдельного процесса. В случае, описанном в статье, достаточно ночью разлить новый образ виртуального диска по серверам и готово!


      1. darken99
        03.08.2019 12:58

        В винде же вроде как есть режим киоска
        Его не рассматривали?


        1. Ion_Storm Автор
          03.08.2019 14:51

          Он, вроде как, был только в альфе. На полном дистрибутиве его выпилили. Но главный момент был в боязни вирусни. На флешках нам тянут гадости тонны. Хотелось гарантий, что ни одна зараза не переживет перезагрузки.


    1. Ion_Storm Автор
      03.08.2019 07:24

      Дополню. Скорость начала работы каждого следующего клиента, с учетом времени на перезагрузку и файловые операции, нас не удовлетворила.


      1. Ion_Storm Автор
        03.08.2019 09:47

        Срок редактирования комментрия истек. Я его перечитал и сам себя не понял.Имеется в виду, в случае локального восстановления первоначального состояния системы, скорость работы нам не понравилась. В случае (недо)VDI вся работа по перезапуска VM происходит в фоне и на времени ожидания начала работы клиентов с сервисом, не сказывается.


        1. mikeus
          03.08.2019 16:15

          Я просто думаю, что восстановление максимум пару сотен мегов изменённых пользовательским сеансом файлов (реестр и прочее) из VSS-копии не должно отнимать много времени, единственно что требуется подчистить созданные файлы в директориях доступных пользователю на запись. KVM/QEMU дифференциальные диски умеет. Можно использовать Virtualbox, который кстати можно поставить прямо на клиентскую машину — одну-то виртуалку она потянет. Соответственно киоск Fedora при входе пользователя просто запускает локальную VM с дифференциальным диском на VBoxSDL-фронтэнде во весь экран.


          1. Ion_Storm Автор
            03.08.2019 16:38

            Про время восстановления, даже на ssd, с учетом времени на POST, меньше 40сек — минуты мы выжать не смогли.
            Вариант с virtualbox мы прикидывали. Но вариант с одним сервером и образом на 4 станции филиала нам понравился больше. Банально, 10 филиалов по 4 машины — 40 точек контроля.
            Если переложить функционал рабочего места на сервак, удобно поддерживать винду в актуальном состоянии, на золотой образ накатил обновы, протестировал, разлил на 10 серверов и забыл. Тоже и с прикладным ПО, захотят завтра еще какую-нибудь софтину, поставил и разлил.
            Да и производительность у KVM на порядок лучше чем у virtualbox. Ну и memory overcommit на сдачу.