Do kogo jest skierowany ten artykuł
Artykuł może być interesujący dla administratorów systemów, którzy musieli zmierzyć się z zadaniem stworzenia serwisu „jednorazowych” stanowisk pracy.
Prolog
Dział IT młodej, dynamicznie rozwijającej się firmy z małą regionalną siecią zwrócił się z prośbą o zorganizowanie „stanowisk samoobsługowych” do użycia przez ich zewnętrznych klientów. Stanowiska te miały być wykorzystywane do rejestracji na zewnętrznych portalach firmy, przesyłania danych z zewnętrznych urządzeń oraz współpracy z portalami rządowymi.
WaŜnym aspektem był fakt, że większość oprogramowania jest dostosowana do systemu MS Windows (na przykład „Deklaracja”), a mimo przesunięcia w kierunku otwartych formatów, MS Office pozostaje dominującym standardem w wymianie dokumentów elektronicznych. W związku z tym, rezygnacja z MS Windows w realizacji tego zadania nie była możliwa.
Głównym problemem była możliwość gromadzenia różnych danych użytkowników sesji, które mogłyby prowadzić do ich wycieku do osób trzecich. . Ale w przeciwieństwie do quasi-instytucji rządowych (państwowa instytucja autonomiczna) MFC, takie niedociągnięcia będą znacznie surowiej karane w organizacjach niepublicznych. Następnym poważnym problemem było wymaganie pracy z zewnętrznymi nośnikami danych, na których na pewno znajdą się złośliwe oprogramowania. Prawdopodobieństwo wprowadzenia złośliwych programów z internetu było postrzegane jako mniej możliwe, ze względu na ograniczenia w dostępie do internetu według białej listy adresów. Do analizy wymagań przyłączyli się pracownicy innych działów, wnosząc swoje wymagania i życzenia, a ostateczne wymagania wyglądały następująco:
Wymagania dotyczące bezpieczeństwa informacji
- Po użyciu wszystkie dane użytkownika (w tym pliki tymczasowe i klucze rejestru) muszą zostać usunięte.
- Wszystkie procesy uruchomione przez użytkownika muszą zakończyć się po zakończeniu pracy.
- Dostęp do internetu wyłącznie według białej listy adresów.
- Ograniczenia dotyczące możliwości uruchamiania zewnętrznego kodu.
- W przypadku bezczynności sesji przez więcej niż 5 minut, sesja powinna zostać automatycznie zakończona, a stacja powinna przeprowadzić czyszczenie.
Wymagania klienta
- Liczba klientów stacji na oddział - nie więcej niż 4.
- Minimalny czas oczekiwania na gotowość systemu, od momentu "usiadł na krześle" do rozpoczęcia pracy z oprogramowaniem klienckim.
- Możliwość podłączenia urządzeń peryferyjnych (skanery, pendrive'y) bezpośrednio z miejsca instalacji "stanowiska samoobsługowego".
- Życzenia klienta
- Prezentacja materiałów reklamowych (zdjęcia) w momencie bezczynności kompleksu.
Męki twórcze
Po długiej zabawie z systemami livecd dla Windowsu, doszliśmy jednogłośnie do wniosku, że powstałe rozwiązanie nie spełnia co najmniej trzech krytycznych punktów. Albo długo się ładuje, albo nie jest do końca live, albo dostosowanie ich wiązało się z wielkimi problemami. Być może źle szukaliśmy i możesz polecić zestaw narzędzi, byłbym wdzięczny.
Następnie zaczęliśmy rozważać VDI, ale dla tego zadania większość rozwiązań jest albo zbyt droga, albo wymaga dużej uwagi. Chętnie zobaczylibyśmy proste narzędzie, z minimalną ilością magii, które można by rozwiązać prostym restartem/usunięciem usługi. Na szczęście mamy serwery klasy low-end w oddziałach, od wygasającej usługi, które mogliśmy użyć jako bazę technologiczną.
Co z tego ostatecznie wyszło? Cóż, tego nie mogę ci zdradzić, bo NDA, ale w trakcie poszukiwań opracowaliśmy ciekawy schemat, który dobrze sprawdził się w testach laboratoryjnych, choć nie wszedł do produkcji.
Kilka zastrzeżeń: autor nie rości sobie prawa do tego, że proponowane rozwiązanie w pełni rozwiązuje wszystkie postawione zadania i czyni to dobrowolnie i z radością. Autor zgadza się z twierdzeniem, że jego angielski jest bardzo słaby. Skoro rozwiązanie już nie jest rozwijane, nie ma co liczyć na naprawę błędów czy zmiany funkcjonalności, wszystko jest w twoich rękach. Autor zakłada, że masz choć trochę wiedzy o KVM i przeczytałeś przeglądowy artykuł o protokole Spice oraz trochę pracowałeś z Centos lub innym dystrybucją GNU Linux.
W tym artykule chciałbym omówić rdzeń powstałego rozwiązania, a mianowicie interakcję klienta z serwerem oraz istotę procesów cyklu życia maszyn wirtualnych w ramach omawianego rozwiązania. Jeśli artykuł zainteresuje publikę, opiszę szczegóły implementacji obrazów live do tworzenia cienkich klientów na bazie Fedory oraz opowiem o aspektach konfiguracji maszyn wirtualnych i serwery KVM w celu optymalizacji wydajności i bezpieczeństwa.
Jeśli weźmiemy kolorowy papier,
Farby, pędzle i klej,
I jeszcze odrobina zręczności…
Można zrobić sto złotych!
Schemat i opis stanowiska testowego

Całe wyposażenie znajduje się w sieci filii, na zewnątrz wychodzi tylko kanał internetowy. Serwer proxy istniał już historycznie, nic nadzwyczajnego w sobie nie ma. Jednak to właśnie na nim, między innymi, będzie odbywać się filtracja ruchu z maszyn wirtualnych (w skrócie WМ dalej w tekście). Nic nie stoi na przeszkodzie, aby umieścić tę usługę na serwerze KVM, jedynie należy zwrócić uwagę, jak zmieni się obciążenie od niego w systemie dyskowym.
Stacja Klienta – właściwie, "stacje samoobsługowe", "frontend" naszego serwisu. Są to nettopów Lenovo IdeaCentre. Co jest świetne w tym urządzeniu? Prawie wszystko, szczególnie zadowala duża ilość złączy USB i czytnik kart na przednim panelu. W naszym schemacie w czytniku kart znajduje się karta SD z włączoną ochroną przed zapisem, na której zapisany jest zmodyfikowany obraz live Fedora 28. Oczywiście, do nettopu podłączony jest monitor, klawiatura i mysz.
Switch – niczym się nie wyróżniający aparatowy switch drugiego poziomu, znajduje się w serwerowni i miga lampkami. Nie jest podłączony do żadnych sieci, poza siecią "stacji samoobsługowych".
KVM_Server – rdzeń schematu, w testach laboratoryjnych Core 2 Quad Q9650 z 8 GB pamięci RAM pewnie obsługiwał 3 maszyny wirtualne z Windows 10. System dyskowy – adaptec 3405 2 dyski Raid 1 + SSD. W testach polowych Xeon 1220 bardziej poważny LSI 9260 + SSD łatwo obsługiwał 5-6 WМ. Serwery dostaliśmy w ramach usługi, koszty inwestycyjne byłyby niewielkie. Na tym serwerze(ach) zainstalowany jest system wirtualizacji KVM z pulą maszyn wirtualnych pool_Vm.
Vm — maszyna wirtualna, backend naszego serwisu. To w niej odbywa się praca użytkownika.
Enp5s0 – interfejs sieciowy skierowany w stronę sieci „stacji samoobsługowych”, na którym działają dhcpd, ntpd, httpd oraz xinetd nasłuchujący na porcie „signal”.
Lo0 – interfejs pseudoklasy loopback. Standardowy.
Spice_console – bardzo interesująca rzecz, ponieważ w przeciwieństwie do klasycznego RDP, w konfiguracji z KVM i protokołem Spice pojawia się dodatkowy element – port konsoli maszyny wirtualnej. W zasadzie, łącząc się z tym portem TCP, otrzymujemy konsolę VM bez potrzeby łączenia się z nią przez jej interfejs sieciowy. Cała interakcja z VM w zakresie przesyłania sygnału jest obsługiwana przez serwer. Najbliższy odpowiednik pod względem funkcji – IPKVM. To znaczy, że na ten port przesyłany jest obraz monitora VM, a także dane o ruchu myszy, a (co najważniejsze) interakcja przez protokół Spice umożliwia bezproblemowe przekierowanie urządzeń USB do wirtualnej maszyny, tak jakby to urządzenie było podłączone do samej VM. Sprawdzono dla pamięci flash, skanerów, kamer internetowych.
Vnet0, virbr0 i wirtualne karty sieciowe VM tworzą sieć wirtualnych maszyn.
Jak TO działa
Po stronie Client Station
Stacja kliencka uruchamia się w trybie graficznym z zmodyfikowanego obrazu live Fedora 28, otrzymując adres IP przez DHCP z przestrzeni adresowej sieci 169.254.24.0/24. W trakcie uruchamiania tworzone są zasady zapory, które pozwalają na nawiązywanie połączeń z portami „signal” i „spice” serwera. Po zakończeniu uruchamiania stacja oczekuje na autoryzację użytkownika „Client”. Po autoryzacji użytkownika uruchamiany jest menedżer okien „openbox” oraz wykonywany jest skrypt autostartowy autostart w imieniu zalogowanego użytkownika. Między innymi skrypt autostartowy uruchamia skrypt 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
Opis zmiennych pliku client.conf
server_ip — adres KVM_Server
vdi_signal_port — port KVM_Server, na którym działa xinetd
vdi_spice_port — port sieciowy KVM_Server, z którego będzie odbywać się przekierowanie zapytania na połączenie od klienta remote-viewer do portu spice przypisanej VM (szczegóły poniżej)
animation_folder — folder, z którego pobierane są obrazy do wyświetlania animacji bullshit
background_folder — folder, z którego pobierane są obrazy do wyświetlania prezentacji w trybie oczekiwania. Szczegóły dotyczące animacji w następnej części artykułu.
Skrypt remote.sh pobiera ustawienia z pliku konfiguracyjnego /etc/client.conf i za pomocą nc nawiązuje połączenie z portem "vdi_signal_port" serwera KVM, odbierając strumień danych z serwera, wśród których oczekuje na linie "RULE ADDED, CONNECT NOW". Po odebraniu oczekiwanej linii uruchamiany jest proces remote-viewer w trybie kiosku, nawiązując połączenie z portem "vdi_spice_port" serwera. Wykonanie skryptu jest wstrzymywane do momentu zakończenia pracy remote-viewera.
Remote-viewer, łącząc się z portem "vdi_spice_port", dzięki przekierowaniu po stronie serwera, trafia na port "spice_console" interfejsu lo0, co oznacza konsolę maszyny wirtualnej i dzieje się właściwie praca użytkownika. W czasie oczekiwania na połączenie, użytkownikowi wyświetlana jest animacja w stylu bullshit, w postaci pokazu slajdów z plików jpeg, a ścieżka do katalogu z obrazkami jest określona przez wartość zmiennej animation_folder z pliku konfiguracyjnego.
W przypadku utraty połączenia z portem "spice_console" maszyny wirtualnej, sygnalizującym wyłączenie/ponowne uruchomienie maszyny wirtualnej (czyli faktyczne zakończenie sesji użytkownika), następuje zakończenie wszystkich procesów uruchomionych w imieniu autoryzowanego użytkownika, co prowadzi do ponownego uruchomienia lightdm i powrotu na ekran logowania.
Od strony serwera KVM
Na porcie "signal" karty sieciowej enp5s0 czeka na połączenie xinetd. Po nawiązaniu połączenia na porcie "signal" xinetd uruchamia skrypt vm_manager.sh bez przekazywania jakichkolwiek parametrów wejściowych i przekierowuje wynik wykonania skryptu do sesji nc Klienta.
/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
Opis zmiennych pliku konfiguracyjnego vm_manager.conf
srv_scripts_dir — folder zlokalizowania skryptów vm_manager.sh, vm_connect.sh, vm_delete.sh, vm_create.sh, vm_clear.sh
srv_pool_size — rozmiar puli VM
srv_start_port_pool — początkowy port, po którym zacznie się rozmieszczenie portów konsol spice maszyn wirtualnych
srv_tmp_dir — folder na pliki tymczasowe
base_host — podstawowa VM (złoty obraz), z którego będą tworzone klony VM w puli
input_iface — interfejs sieciowy serwera, skierowany w stronę Client Stations
vdi_spice_port — port sieciowy serwera, z którego będzie odbywało się przekierowanie żądania połączenia z klienta remote-viewer do portu spice dedykowanej VM
count_conn_tryes — czas oczekiwania, po którym uznaje się, że połączenie z VM nie zostało nawiązane (szczegóły działania zob. vm_connect.sh)
Skrypt vm_manager.sh odczytuje plik konfiguracyjny z pliku vm_manager.conf, ocenia stan maszyn wirtualnych w puli według kilku parametrów, w tym: ile VM jest uruchomionych, czy są dostępne wolne VM. W tym celu odczytuje plik clear.list, w którym znajdują się numery portów 'spice_console' 'świeżo utworzonych' (zob. poniżej cykl tworzenia VM) maszyn wirtualnych i sprawdza, czy z nimi nawiązano połączenie. W przypadku wykrycia portu z nawiązanym połączeniem sieciowym (co nie powinno mieć miejsca) wyświetlane jest ostrzeżenie, a port przenoszony jest do waste.list. Po wykryciu pierwszego portu z pliku clear.list, z którym w tej chwili nie ma połączenia, vm_manager.sh wywołuje skrypt vm_connect.sh i przekazuje mu jako parametr numer tego portu.
/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
Skrypt vm_connect.sh dodaje reguły zapory, które tworzą przekierowanie portu 'vdi_spice_port' interfejsu serwera enp5s0 na 'spice console port' VM, zlokalizowanym na interfejsie lo0 serwera, przekazanym jako parametr uruchomienia. Port przenoszony jest do conn_wait.list, a VM uznawana jest za oczekującą na połączenie. Do sesji Client Station na porcie 'signal' serwera przesyłany jest ciąg 'RULE ADDED, CONNECT NOW', który jest oczekiwany przez uruchomiony na niej skrypt remote.sh. Rozpoczyna się cykl oczekiwania na połączenie z określoną liczbą prób, określaną wartością zmiennej 'count_conn_tryes' z pliku konfiguracyjnego. Co sekundę do sesji nc będzie przesyłany ciąg 'RULE ADDED, CONNECT NOW' i sprawdzane będzie, czy nawiązano połączenie z portem 'spice_console'.
Jeśli w określonym czasie nie nawiązano połączenia, port 'spice_console' przenoszony jest z powrotem do clear.list. Wykonanie vm_connect.sh kończy się, a wykonanie vm_manager.sh zostaje wznowione, co uruchamia cykl czyszczenia.
Jeśli połączenie Client Station z portem „spice_console” na interfejsie lo0 jest ustalone, zasady zapory ogniowej, które tworzą przekierowanie między portem „spice” serwera a portem „spice_console”, są usuwane, a dalsze utrzymanie połączenia zachodzi dzięki mechanizmowi wykrywania stanu zapory. W przypadku zerwania połączenia ponowne nawiązanie połączenia z portem „spice_console” nie będzie możliwe. Port „spice_console” jest przenoszony do waste.list, a VM jest uważana za „brudną” i nie może wrócić do puli „czystych” maszyn wirtualnych bez przejścia procesu czyszczenia. Wykonanie vm_connect.sh kończy się, wznowione zostaje wykonanie vm_manager.sh, które uruchamia cykl czyszczenia.
Cykl czyszczenia zaczyna się od przeglądania pliku waste.list, do którego przenoszone są numery portów „spice_console” maszyn wirtualnych, z którymi nawiązywano połączenie. Sprawdzana jest obecność aktywnego połączenia na każdym porcie „spice_console” z listy. Jeśli połączenie jest nieobecne, uznaje się, że maszyna wirtualna nie jest już używana, a port jest przenoszony do recycle.list, a proces usuwania maszyny wirtualnej (patrz poniżej), do której należał ten port, jest uruchamiany. Jeśli wykryto aktywne połączenie sieciowe na porcie, uważa się, że maszyna wirtualna jest używana i podejmowane są wobec niej żadne działania. Jeśli port nie jest nasłuchiwany, uznaje się, że VM jest wyłączona i już nie jest potrzebna. Port jest przenoszony do recycle.list, a proces usuwania maszyny wirtualnej jest uruchamiany. W tym celu wywoływany jest skrypt vm_delete.sh, któremu jako parametr przekazywany jest numer portu „spice_console” VM, którą należy usunąć.
/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
Usuwanie maszyny wirtualnej jest dość trywialną operacją, skrypt vm_delete.sh ustala nazwę maszyny wirtualnej, której port został przekazany jako parametr uruchomienia. Wymuszane jest zatrzymanie VM, usunięcie VM z hyperwizora, a wirtualny dysk twardy tej VM jest usuwany. Port „spice_console” jest usuwany z recycle.list. Wykonanie vm_delete.sh kończy się, wznowione zostaje wykonanie vm_manager.sh.
Skrypt vm_manager.sh, po zakończeniu operacji czyszczenia zbędnych maszyn wirtualnych z listy waste.list, rozpoczyna cykl tworzenia maszyn wirtualnych w puli.
Proces zaczyna się od określenia dostępnych portów do umieszczenia "spice_console". W tym celu, w oparciu o parametr pliku konfiguracyjnego "srv_start_port_pool", który definiuje początkowy port dla puli "spice_console" maszyn wirtualnych, oraz parametr "srv_pool_size", określający maksymalną liczbę maszyn wirtualnych, następuje kolejna iteracja wszystkich możliwych portów. Dla każdego określonego portu następuje wyszukiwanie w plikach clear.list, waste.list, conn_wait.list, recycle.list. Jeśli port zostanie znaleziony w którymkolwiek z tych plików, uznaje się go za zajęty i pomija. Jeśli port nie został odnaleziony w wymienionych plikach, zostaje dodany do pliku recycle.list i rozpoczyna się proces tworzenia nowej maszyny wirtualnej. W tym celu wywoływany jest skrypt vm_create.sh, któremu przekazywany jest jako parametr numer portu "spice_console", dla którego należy stworzyć 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
Proces tworzenia nowej maszyny wirtualnej
Skrypt vm_create.sh odczytuje z pliku konfiguracyjnego wartość zmiennej "base_host", która określa wzorzec maszyny wirtualnej, na podstawie której będzie tworzony klon. Wykonuje wyeksportowanie konfiguracji XML VM z bazy hipernadzorcy, przeprowadza szereg kontrolnych sprawdzeń obrazu qcow dysku VM i po pomyślnym ich zakończeniu tworzy plik konfiguracyjny XML dla nowego VM oraz „linked clone” obrazu dysku nowego VM. Następnie nowa konfiguracja XML VM zostaje załadowana do bazy hipernadzorcy i VM zostaje uruchomiona. Port "spice_console" jest przenoszony z recycle.list do clear.list. Kończy się wykonanie vm_create.sh oraz vm_manager.sh.
Przy następnym połączeniu wszystko zaczyna się od początku.
W przypadku sytuacji awaryjnych w zestawie znajduje się skrypt vm_clear.sh, który wymusza przejście przez wszystkie VM z puli i ich usunięcie z zerowaniem wartości list. Wywołanie go na etapie ładowania pozwala rozpocząć pracę (niedokończonego) VDI z czystą kartką.
/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="
Na tym chciałbym zakończyć pierwszą część swojej opowieści. Zawarte informacje powinny być wystarczające dla administratorów systemów, by spróbować niedokończonego VDI w praktyce. Jeśli społeczność uzna ten temat za interesujący, w drugiej części opowiem o modyfikacji livecd Fedora i przekształceniu jej w kiosk.
Źródło: habr.com
