image

«Скажи мне, Рождённый Женщиной,
— вопросил Кришна,
Куда движутся эти миры,
Зачем злой Парвана по ночам охотится
за своей второй сущностью,
И почему у ласточки Бшакти две ноги,
а у Меня двадцать четыре?»
— Элементарно, — сказал Арджуна, — берём вектор Пойнтинга…
БХАГАВАД-ГИТА (мл. тибетская)
Запев 30 тома

Предыстория


Когда-то давно для создания виртуальных машин (далее ВМ) в Gentoo Linux я использовал libvirt, всё бы ничего, но однажды свет увидела версия Qemu с поддержкой чипсета Q35, libvirt же ещё долгое (как мне казалось) время предлагал пользоваться только одним старым добрым i440FX. Хотелось попробовать новомодную функцию, не потеряв при этом в удобстве создания ВМ. После череды проб и ошибок получилась среда виртуализации на чистом Qemu, удобная и простая (по крайней мере, на мой взгляд), пригодная для быстрого создания похожих ВМ, как правило, недолгоживущих, так как специфика моей работы заключается именно в установке операционных систем (далее ОС) и программного обеспечения (далее ПО) на голые сервера. ОС — это обычно CentOS, Astra Linux, Windows Server. Выходит, новая версия ОС или ПО — создаются новые пустые ВМ для отработки установки всей совокупности ПО.

Дано


ОС хоста Gentoo Linux
Профиль default/linux/amd64/17.1/no-multilib
Ядро sys-kernel/gentoo-sources-5.13.0-r1
USE=«experimental symlink»

Здесь и далее снятые USE-флаги не показаны.

Внимание: версия ядра 5.13.0 содержала ошибку в коде сервера NFS (см., например, elrepo.org/bugs/view.php?id=1116), поэтому под ним не работала сетевая установка CentOS с помощью NFS
Менеджер служб sys-apps/systemd-248.3-r1
USE=«gcrypt hwdb kmod lz4 pam pcre policykit seccomp split-usr sysv-utils zstd»
Управление логическими томами sys-fs/lvm2-2.02.188
USE=«systemd thin udev»
Пользовательские инструменты виртуализации app-emulation/qemu-6.0.0-r50
USE=«caps io-uring usb vhost-net vnc»
QEMU_SOFTMMU_TARGETS=«x86_64»
QEMU_USER_TARGETS=""
Просмотрщик VNC net-libs/gtk-vnc-1.2.0
USE=«introspection vala»

К сожалению, по непонятным причинам этот ebuild с некоторых пор не устанавливает собственно клиент VNC, его приходится собирать отдельно такими командами:

ebuild /путь/до/дерева/портежей/net-libs/gtk-vnc/gtk-vnc-1.2.0.ebuild unpack
ebuild /путь/до/дерева/портежей/net-libs/gtk-vnc/gtk-vnc-1.2.0.ebuild compile

После этого шага собранный gvncviewer копируется из временного каталога сборки в /usr/local/bin, например
Управление сетью net-misc/connman-1.40
USE=«ethernet nftables wifi»
Сетевой экран net-firewall/nftables-0.9.9
USE=«gmp modern-kernel»
Сервер DNS, DHCP и TFTP net-dns/dnsmasq-2.85
USE=«dhcp idn inotify libidn2 tftp»
Сервер HTTP и FTP sys-apps/busybox-1.33.1
USE=«savedconfig systemd»

Вполне хватает для сетевой установки гостевых ОС
Сервер CIFS net-fs/samba-4.14.5-r1
USE=«system-mitkrb5 systemd»

Нужен для сетевой установки гостевого Windows Server
Средство для подключения к сокетам Unix net-misc/socat-1.7.4.1
USE=""
Оконный менеджер gui-wm/sway-1.6.1
USE=«X man swaybar swaybg swayidle swaylock swaymsg swaynag tray»

Указываю здесь на всякий случай, так как следующий пункт с этим связан
Средство автоматизации gui-apps/ydotool-0.1.9

Этот ebuild отсутствует в официальном дереве портежей, его нужно взять из оверлея pg_overlay, оттуда же берутся две зависимости:

dev-libs/libevdevplus-0.1.1
dev-libs/libuinputplus-0.1.4

Настройка ядра


    General setup  --->
      [*] Configure standard kernel features (expert users)  --->

# Выберем такой тип AIO (Asynchronous Input/Output) для ВМ, опять же, ужасно новомодный

        [*]   Enable IO uring support
[*] Virtualization  --->

# Без KVM, полагаю, ВМ будут о-о-очень медленными

<*>   Kernel-based Virtual Machine (KVM) support

# Следующие две настройки зависят от производителя центрального процессора (далее ЦП) хоста (мне достался Intel)

      <*>     KVM for Intel (and compatible) processors support
      < >     KVM for AMD processors support
[*] Networking support  --->
        Networking options  --->

# Полезно для сетевого взаимодействия с ВМ

        <*> 802.1d Ethernet Bridging
    Device Drivers  --->
      [*] Block devices  --->

# Для монтирования ISO-образов, понадобится при сетевой установке гостевых ОС

        <*>   Loopback device support
      [*] Multiple devices driver support (RAID and LVM)  --->
        <*>   Device mapper support

# Для создания снапшотов ВМ

<*>     Snapshot target

# Для бережного использования диска хоста

        <*>     Thin provisioning target
      [*] Network device support  --->
        [*]   Network core driver support

# Полезно для сетевого взаимодействия с ВМ

        <*>     Universal TUN/TAP device driver support
          Input device support  --->
            [*]   Miscellaneous devices  --->

# Это совсем не обязательно, но пригодится для автоматизации некоторых операций с ВМ

<M>   User level driver support

# Для ускорения работы сети в ВМ

      [*] VHOST drivers  --->
        <*>   Host kernel accelerator for virtio net
    File systems  --->
          CD-ROM/DVD Filesystems  --->

# Для монтирования ISO-образов, понадобится при сетевой установке гостевых ОС

            <M> ISO 9660 CDROM file system support
            [*]   Microsoft Joliet CDROM extensions

# Для монтирования ISO-образов, понадобится при сетевой установке гостевой Windows Server

            <M> UDF file system support
          Pseudo filesystems  --->
<i>

# Очень рекомендуется использовать огромные страницы для ВМ

        [*] HugeTLB file system support
      [*] Network File Systems  --->

# Понадобится при сетевой установке гостевой CentOS

        <M>   NFS server support
        [*]     NFS server support for NFS version 4

Для справки привожу выдержку из /usr/src/linux/.config:

CONFIG_IO_URING=y
CONFIG_VIRTUALIZATION=y
CONFIG_KVM=y
CONFIG_KVM_INTEL=y
CONFIG_BRIDGE=y
CONFIG_BLK_DEV_LOOP=y
CONFIG_DM_SNAPSHOT=y
CONFIG_DM_THIN_PROVISIONING=y
CONFIG_TUN=y
CONFIG_INPUT_UINPUT=m
CONFIG_VHOST_NET=y
CONFIG_ISO9660_FS=m
CONFIG_JOLIET=y
CONFIG_UDF_FS=m
CONFIG_HUGETLBFS=y
CONFIG_NFSD=m
CONFIG_NFSD_V4=y

Настройки, нужные для сетевого экрана, не привожу, чтобы не повторяться, они имеются в другой статье, под названием: «Прозрачный Squid с SSL-Bump для Gentoo с nft».

Настройка дискового хранилища


Допустим, у нас имеется 500 ГБ свободного пространства в группе томов «vg», создадим в нём так называемый «тонкий пул» с именем «thin»:

lvm lvcreate -c 64M -I 64M -L 500G -Zn --type thin-pool --thinpool thin vg

Ключ -Zn отключает обнуление первого блока создаваемых в этом пуле логических томов.

Настройка оперативной памяти


Мой ЦП поддерживает только двухмегабайтные огромные страницы, для гигабайтных нужны дополнительные настройки загрузчика. Количество огромных страниц зададим в /etc/sysctl.conf:

vm.nr_hugepages = 5000

Hugetlbs автоматически монтируется менеджером служб Systemd в /dev/hugepages.

Настройка сети


До перехода с OpenRC на Systemd никаких сложностей с настройкой сети не было. Так как Systemd может настроить только предельно простую сеть (по крайней мере, пока), после перехода потребовалась установка службы настройки сети, в этом качестве был выбран connman. Однако он не подходит для создания сетевого моста. Systemd мог бы создать своими средствами мост, но он не назначает пустому мосту IP-адрес.

В итоге пришлось создать отдельную службу /etc/systemd/system/create-br0.service:

[Unit]
Description=Bridge creation.
Before=network-pre.target
Before=nftables-restore.service

[Service]
Type=simple
ExecStart=/bin/bash /usr/local/sbin/create-br0.sh
RemainAfterExit=yes

[Install]
WantedBy=nftables-restore.service
WantedBy=dnsmasq.service

Сценарий /usr/local/sbin/create-br0.sh у меня выглядит так:

#!/bin/sh
ip link add br0 type bridge
ip link set br0 address 8e:fb:65:16:72:38
ip link set br0 up
ip addr add 192.168.120.1/24 dev br0

Чтобы connman не мешал, исключим в его настройках мост и сетевые стыки ВМ, в итоге /etc/connman/main.conf выглядит так:

[General]
AllowDomainnameUpdates=false
AllowHostnameUpdates=false
NetworkInterfaceBlacklist=lo,vnet,br

Настройка nft, за вычетом всего, никак не касающегося ВМ, выглядит так:

#!/sbin/nft -f

define icmp_types = { destination-unreachable, time-exceeded, parameter-problem, timestamp-request, echo-request, echo-reply }
define br = br0
define host = 192.168.120.1
define virtual_machines = 192.168.120.0/24
define br_bcast = 192.168.120.255
define eth = enp0s25
define wifi = wlp2s0
define dhcp_client = 192.168.120.224/27

# ВМ с выходом в Интернет

define privileged_vm = { 192.168.120.22, 192.168.120.127, 192.168.120.129 }
define nat_if = { $eth, $wifi }
define squid = 3128

flush ruleset

table ip filter {
  chain input {
    type filter hook input priority 0; policy drop;
    iif lo accept comment "allow loopback"
    icmp type $icmp_types accept comment "allow important ICMP types"
    ct state invalid counter drop comment "drop invalid packets"
    tcp flags syn tcp option maxseg size < 999 counter drop comment "TCP SACK Panic workaround"
    tcp flags & (syn | ack) == syn ct state untracked counter drop comment "drop initial packets from untracked ports"
    iif $br ip daddr $host ip saddr $virtual_machines tcp dport { domain, http, https, microsoft-ds, nfs, $squid, ftp } accept comment "allow services for virtual machines"
    ct state { established, related } accept comment "allow all related connections"
    iif $br udp dport { domain, bootps, tftp, 4011 } counter accept comment "allow DNS, DHCP, TFTP, proxyDHCP"
    counter comment "count dropped packets"
  }

  chain output {
    type filter hook output priority 100; policy drop;
    oif lo accept comment "allow loopback"
    icmp type $icmp_types counter accept comment "allow important ICMP types"
    oif { $eth, $wifi } udp dport . udp sport { bootps . bootpc } counter accept comment "allow exchange with DHCP servers"
    oif $br ip saddr $host ip daddr { $dhcp_client, 255.255.255.255 } udp sport . udp dport { bootps . bootpc } counter accept comment "allow DHCP of dnsmasq"
    oif $br ip saddr $host ip daddr $virtual_machines udp sport { domain, tftp } counter accept comment "allow DNS, TFTP"
    oif $br ip saddr $host ip daddr $virtual_machines tcp sport { domain, http, https, microsoft-ds, ftp } accept comment "allow DNS, HTTP, Samba, FTP"
    meta l4proto { tcp, udp } th sport >= 1025 accept comment "allow unprivileged ports"
    counter comment "count dropped packets"
  }

  chain forward {
    type filter hook forward priority 0; policy drop;
    tcp flags syn tcp option maxseg size set rt mtu counter comment "clamp TCP MSS to path MTU"
    iif $br ip daddr != $host meta l4proto { tcp, udp } th dport domain drop comment "deny DNS to external servers"
    iif $br ip saddr { $privileged_vm, $dhcp_client } accept comment "allow privileged virtual machine & DHCP clients"
    oif $br ip daddr { $privileged_vm, $dhcp_client } accept comment "allow privileged virtual machine & DHCP clients"
    counter comment "count dropped packets"
  }
}

table ip nat {
  chain postrouting {
    type nat hook postrouting priority 100; policy accept;
    oif $nat_if ip saddr { $privileged_vm, $dhcp_client } counter masquerade comment "masquerade virtual machine"
  }
}

Сценарий запуска ВМ


Для запуска по запросу отдельных ВМ нам понадобится следующий простой сценарий, назовём его /usr/local/sbin/qemu:

#!/bin/bash
VM=$2
if [[ "$1" != status && -z "$VM" ]]; then
	echo "No VM name was supplied"
	exit 4
fi
WAIT=60
CFGDIR=/etc/qemu
CHROOT=/var/tmp/qemu/empty
export LVM_SUPPRESS_FD_WARNINGS=1
[ -f /etc/conf.d/qemu.$VM ] && . /etc/conf.d/qemu.$VM
CFG=$CFGDIR/$VM.cfg

case $1 in
	start)
		if [[ -r $CFG ]]; then
			CPU=`sed -ne '/#CPU/{s/.*=\(.*\)/\1/p;q}' $CFG`
			X[0]=`sed -ne '/#MEM/{s/.*=\(.*\)/\1/p;q}' $CFG`
			if grep '^#kernel=' $CFG &> /dev/null; then
				X[1]='-kernel'
				X[2]=`sed -ne '/#kernel/{s/[^=]*=\(.*\)/\1/p;q}' $CFG`
				X[3]='-append'
				X[4]=`sed -ne '/#append/{s/[^=]*=\(.*\)/\1/p;q}' $CFG`
				X[5]='-initrd'
				X[7]=`sed -ne '/#initrd/{s/[^=]*=\(.*\)/\1/p;q}' $CFG`
			fi
			N=`sed -ne '/= "vnet[0-9]/{s/.*"vnet\([0-9]\+\).*/\1/p;q}' $CFG`
			echo "Starting $VM"
			taskset -c $CPU qemu-system-x86_64 -name $VM -nodefaults -daemonize -runas qemu -cpu host -machine q35 -readconfig $CFG -m "${X[@]}" \
				-mem-prealloc -mem-path /dev/hugepages -rtc base=utc -chroot $CHROOT -vga virtio -audiodev id=none,driver=none \
				-object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0,id=vrng0,max-bytes=1024,period=200 -vnc 127.0.0.1:$N || exit 1 
		else
			echo "Unrecognized VM: $VM"
			exit 3
		fi
	;;
	stop)
		if [[ -r $CFG ]]; then
			PID=`pgrep -f "qemu-system-x86_64 -name $VM "`
			if [[ -n $PID ]]; then
				echo "Stopping $VM"
				MON=`sed -ne 's/^\s*path\s*=\s*"\(.*\)"/\1/p' $CFG`
				echo '{ "execute": "qmp_capabilities" } { "execute": "system_powerdown" }' | socat - $MON > /dev/null
				while ps -p $PID >/dev/null 2>&1; do
					[[ $((i++)) -ge $WAIT ]] && break || sleep 1
				done
				[[ $i -gt $WAIT ]] && kill $PID
			fi
			[[ $i -gt $WAIT ]] && sleep 2
			pgrep -f "qemu-system-x86_64 -name $VM " && exit 1
		else
			echo "Unrecognized VM: $VM"
			exit 3
		fi
	;;
	status)
		ps -eo args | sed -ne 's/.*[q]emu-system-x86_64 -name \([^ ]\+\) .* -vnc \([^ ]\+\)/\1\t\2/p'
	;;
	*)
		echo "Unrecognized command: $1"
		exit 2
	;;
esac

Как видно из строки запуска qemu-system-x86_64:

  1. Процесс привязывается к определённому набору логических процессоров (командой taskset). Набор процессоров определяется в настроечном файле ВМ.
  2. Процесс работает с правами пользователя qemu (-runas qemu). Если он не создан, то его нужно создать с домашним каталогом /dev/null и оболочкой /sbin/nologin. Данный пользователь должен входить в группу kvm.
  3. Процессор для ВМ используется хостовый, без изменений (-cpu host). Чипсет — Q35 (-machine q35), из-за чего всё и начиналось.
  4. Большая часть оборудования берётся из настроечного файла ВМ (-readconfig $CFG).
  5. Память ВМ выделяется полностью (-mem-prealloc), для чего используются огромные страницы (-mem-path /dev/hugepages). Размер памяти берётся из настроечного файла ВМ (-m "${X[@]}"). Как вы можете заметить, в массиве $X передаётся не только размер памяти, но и некоторые другие настройки.
  6. Время БИОСа в ВМ будет в часовом поясе UTC (-rtc base=utc), это рекомендуется для Unix-подобных систем. Для Windows это не вызовет сложностей, так как с помощью редактирования реестра можно научить её понимать такое время.
  7. Процесс запирает себя в каталоге /var/tmp/qemu/empty с помощью вызова chroot (-chroot $CHROOT). В этот каталог мы позже поместим образ виртуального съёмного устройства хранения («флешки»).
  8. Audio-устройства ВМ не предоставляются (-audiodev id=none,driver=none). Обычно хватает перенаправления аудио с помощью RDP в случае ВМ на Windows.
  9. Каждой ВМ предоставляется устройство Virtio-RNG — паравиртуальный генератор случайных чисел (-object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0,id=vrng0,max-bytes=1024,period=200).
  10. Для доступа к консоли ВМ предоставляется VNC на адресе 127.0.0.1, номер экрана задаётся в настроечном файле (-vnc 127.0.0.1:$N).

Кроме этого сценария есть ещё один вспомогательный, /etc/qemu/ifup.sh:

#!/bin/bash
ip link set $1 master br0
ip link set $1 up

Создание ВМ


Выделение места в дисковом хранилище

Место выделяется в тонком пуле следующей командой:

lvm lvcreate -T vg/thin -V 10G -n ubuntu

Будет создан логический том размером 10 ГБ, путь к блочному устройству /dev/mapper/vg-ubuntu. Пока гостевая ОС не установлена, этот том места на диске не занимает.

Создание настроечного файла ВМ

Для каждой ВМ создаётся отдельный настроечный файл /etc/qemu/Имя_ВМ.cfg. Пример для линуксовой ВМ:

# qemu config file
# Эта директива используется сценарием запуска ВМ
# определяет, к каким процессорам привязывается процесс qemu
#CPU=0,1,2,3
# Эта директива используется сценарием запуска ВМ
# определяет размер памяти ВМ в МБ
#MEM=4096
# Следующие три директивы используются сценарием запуска ВМ
# используются для непосредственного запуска ядра самим qemu
#kernel=/var/tmp/qemu/vmlinuz-3.10.0-957.el7.x86_64
#initrd=/var/tmp/qemu/initramfs-3.10.0-957.el7.x86_64.img
#append=ro root=/dev/vda1 elevator=noop numa=off transparent_hugepage=never nosoftlockup mce=ignore_ce audit=0

# Следующие два блока настраивают первый виртуальный привод DVD
# используются при установке ОС с виртуального привода DVD
[drive "cd0"]
  file = "/opt/dist/ks-CentOS_2009-tm_7.2.0.155.iso"
  if = "none"
  readonly = "on"
  format = "raw"

[device]
  driver = "ide-cd"
  drive = "cd0"
  bus = "ide.0"
  unit = "0"

# Следующие два блока настраивают первый виртуальный жёсткий диск ВМ

[drive "vhd0"]
  file = "/dev/mapper/vg-centos"
  if = "none"
  format = "raw"


# Кеш в небезопасном режиме для скорости, надёжность хранения не важна в моём случае.
# Либо же, например, можно использовать такой режим при установке гостевой ОС,
# а затем переключиться в режим «none»

cache = "unsafe"
  cache.direct = "on"
  aio = "io_uring"

[device "virtio0"]
  driver = "virtio-blk-pci"
  scsi = "off"
  bus = "pcie.0"
  drive = "vhd0"

# Настройка сокета для взаимодействия с qemu с помощью команд QMP (QEMU Machine Protocol)

[chardev "charmonitor"]
  backend = "socket"
  path = "/var/tmp/qemu/centos.monitor"
  server = "on"
  wait = "off"

# Следующие два блока настраивают первый сетевой стык ВМ

[device "net0"]
  driver = "virtio-net-pci"
  netdev = "hostnet0"
  mac = "52:54:00:07:00:08"
  bus = "pcie.0"

# Настройка многоочерёдного (mutliqueue) сетевого стыка ВМ
# требуется поддержка и настройка в гостевой ОС (см. далее)
# число должно быть равно 2 * M + 2, где M — число логических процессоров ВМ

  vectors = "10"
  mq = "on"

[netdev "hostnet0"]
  type = "tap"

# Число после «vnet» определяет номер экрана VNC

  ifname = "vnet6"
  vhost = "on"
  script = "/etc/qemu/ifup.sh"
  downscript = "no"

# Настройка многоочерёдного (multiqueue) сетевого стыка ВМ
# в гостевой ОС включается командой: ethtool -L eth0 combined 4
# число должно быть равно М, где M — число логических процессоров ВМ

queues = "4"

[mon]
  chardev = "charmonitor"
  mode = "control"

[machine]
  type = "q35"
  accel = "kvm"
  usb = "on"

# Конфигурация процессора с точки зрения ВМ

[smp-opts]
  sockets = "1"
  cores = "2"
  threads = "2"

# Следующие два блока переводят ВМ в режим UEFI

[drive]
  file = "/usr/share/edk2-ovmf/OVMF_CODE.fd"
  if = "pflash"
  format = "raw"
  unit = "0"
  readonly = "on"

[drive]
  file = "/var/tmp/qemu/OVMF_VARS_tm72p.fd"
  if = "pflash"
  format = "raw"
  unit = "1"

Пример для ВМ c Windows:

# qemu config file
#MEM=3072
#CPU=2,3

# Следующие четыре блока настраивают два виртуальных привода DVD
# используются при установке ОС с виртуального привода DVD

[drive "cd0"]
  file = "/opt/distr/windows_server.iso"
  if = "none"
  readonly = "on"
  format = "raw"

[device]
  driver = "ide-cd"
  drive = "cd0"
  bus = "ide.0"
  unit = "0"

[drive "cd1"]

# Второй привод DVD нужен для предоставления установщику Windows драйверов устройств Virtio

  file = "/opt/distr/virtio-win-0.1.190.iso"
  if = "none"
  readonly = "on"
  format = "raw"

[device]
  driver = "ide-cd"
  drive = "cd1"
  bus = "ide.1"
  unit = "0"

[drive "vhd0"]
  file = "/dev/mapper/vg-windoze"
  if = "none"
  format = "raw"
  cache = "unsafe"
  cache.direct = "on"
  aio = "io_uring"

[device "virtio0"]
  driver = "virtio-blk-pci"
  scsi = "off"
  bus = "pcie.0"
  drive = "vhd0"

[chardev "charmonitor"]
  backend = "socket"
  path = "/var/tmp/qemu/windoze.monitor"
  server = "on"
  wait = "off"

[device "net0"]
  driver = "virtio-net-pci"
  netdev = "hostnet0"
  mac = "52:54:00:20:12:04"
  bus = "pcie.0"

[netdev "hostnet0"]
  type = "tap"
  ifname = "vnet8"
  vhost = "on"
  script = "/etc/qemu/ifup.sh"
  downscript = "no"

# Нужно для комфортной работы через VNC

[device "tablet"]
  driver = "usb-tablet"

[mon]
  chardev = "charmonitor"
  mode = "control"

[machine]
  type = "q35"
  accel = "kvm"
  usb = "on"

[smp-opts]
  cpus = "2"

Подготовка вспомогательных файлов

При работе ВМ в режиме UEFI для каждой ВМ нужно скопировать /usr/share/edk2-ovmf/OVMF_VARS.fd в отдельный файл в каталоге Qemu, в моём примере /var/tmp/qemu.
Для дополнительных настроек запуска ВМ предусмотрен ещё один конфигурационный файл: /etc/conf.d/qemu.Имя_ВМ (как наследство OpenRC). Содержит одну строку с переменной QEMU_OPTS. Пример для загрузки по сети:

QEMU_OPTS="-boot order=n"


Замечание, касающееся Systemd: до перехода с OpenRC я широко использовал каталог /var/tmp (в моём случае это отдельный раздел) для хранения разнородных данных, например, данных qemu. После перехода на Systemd выяснилось, что в нём имеется служба systemd-tmpfiles-clean, которая удалила, к моему сожалению, из /var/tmp много нужных файлов. Чтобы защитить файлы qemu, был создан настроечный файл /usr/lib/tmpfiles.d/qemu.conf:

d /var/tmp/qemu 0755 qemu qemu -

Запуск и остановка ВМ


Запуск ВМ по требованию выглядит так:

/usr/local/sbin/qemu start Имя_ВМ

Остановка соответственно:

/usr/local/sbin/qemu stop Имя_ВМ

Получение перечня запущенных ВМ:

/usr/local/sbin/qemu status

Подключение к ВМ хостовых устройств USB на постоянной основе


В настроечный файл достаточно добавить блок с указанием VID и PID устройства, например:

[device "usbaudio"]
  driver = "usb-host"
  vendorid = "0x041e"
  productid = "0x30e0"

Поскольку эти настройки, как я заметил, производятся программой qemu до сброса администраторских привилегий, менять владельца устройства на qemu:qemu необязательно.

Подключение к работающей ВМ виртуального съёмного устройства хранения («флешки»)


Предварительно должен быть создан образ виртуального съёмного устройства, например, размером в 1 ГБ, в каталоге $CHROOT (в моём случае это /var/tmp/qemu/empty):

truncate -s 1G /var/tmp/qemu/empty/usb.img
chown qemu:qemu /var/tmp/qemu/empty/usb.img

Поскольку в нашей установке графика у ВМ предоставляется только посредством VNC, то командная строка Qemu Monitor недоступна. Поэтому будем пользоваться протоколом QMP, подключаясь к сокету, указанному в настроечном файле:

socat - /var/tmp/qemu/windoze.monitor

После успешного подключения к сокету в первую очередь даём команду:

{ "execute": "qmp_capabilities" }

Затем следуют команды подключения устройства (отформатировано для удобства просмотра):

{
  "execute": "human-monitor-command",
  "arguments": {
    "command-line": "drive_add auto if=none,id=hd1,file=/usb.img,cache=writeback,cache.direct=on,aio=io_uring,format=raw"
  }
}
{
  "execute": "device_add",
  "arguments": {
    "driver": "usb-storage",
    "drive": "hd1",
    "id": "usb1"
  }
}

Команды отключения устройства будут, соответственно, такими:

{ "execute": "device_del", "arguments": { "id": "usb1" } }
{ "execute": "human-monitor-command", "arguments": { "command-line": "drive_del hd1" } }

Создание снимка (снапшота) выключенной ВМ


Создание снимка делается средствами LVM:

lvm lvcreate -s vg/centos -n centos_bak

Обратите внимание, что снимок помечается как не подлежащий автоматической активации LVM.

Перенос диска ВМ из одного пула в другой


На определённом этапе мне потребовалось создать новый тонкий пул и перенести на него логические тома нескольких использовавшихся тогда ВМ. Это делается предельно просто командой dd, предварительно необходимо создать в новом тонком пуле соответствующие логические тома:

dd if=/dev/vg/some_vm of=/dev/other_vg/some_vm bs=64M conv=sparse

Здесь ключевым является «conv=sparse», нужный для того, чтобы логический том занимал только фактически использованное ВМ место.

Автозапуск ВМ при запуске хоста


Иногда требуется, чтобы ВМ запускалась одновременно с остальными службами хоста. Для таких случаев подготовим другой сценарий, назовём его /usr/local/bin/guests:

#!/bin/bash
export LVM_SUPPRESS_FD_WARNINGS=1
CFGDIR=/etc/qemu

case $1 in
	start)
		CHROOT=/var/tmp/qemu/empty
		for CFG in $CFGDIR/*.cfg; do
			VM=${CFG##*/}
			VM=${VM%.cfg}
			[ -f /etc/conf.d/qemu.$VM ] && . /etc/conf.d/qemu.$VM
			CPU=`sed -ne '/#CPU/{s/.*=\(.*\)/\1/p;q}' $CFG`
			X[0]=`sed -ne '/#MEM/{s/.*=\(.*\)/\1/p;q}' $CFG`
			if grep '^#kernel=' $CFG &> /dev/null; then
				X[1]='-kernel'
				X[2]=`sed -ne '/#kernel/{s/[^=]*=\(.*\)/\1/p;q}' $CFG`
				X[3]='-append'
				X[4]=`sed -ne '/#append/{s/[^=]*=\(.*\)/\1/p;q}' $CFG`
				X[5]='-initrd'
				X[7]=`sed -ne '/#initrd/{s/[^=]*=\(.*\)/\1/p;q}' $CFG`
			fi
			N=`sed -ne '/= "vnet[0-9]/{s/.*"vnet\([0-9]\+\).*/\1/p;q}' $CFG`
			echo "Starting $VM"
			taskset -c $CPU qemu-system-x86_64 -name $VM -nodefaults -daemonize -runas qemu -cpu host -machine q35 -readconfig $CFG -m "${X[@]}" \
				-mem-prealloc -mem-path /dev/hugepages -rtc base=utc -chroot $CHROOT -vga virtio -audiodev id=none,driver=none \
				-object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0,id=vrng0,max-bytes=1024,period=200 -vnc 127.0.0.1:$N
		done
	;;
	stop)
		WAIT=60
		for CFG in $CFGDIR/*.cfg; do
			VM=${CFG##*/}
			VM=${VM%.cfg}
			PID=`pgrep -f "qemu-system-x86_64 -name $VM "`
			if [[ -n $PID ]]; then
				echo "Stopping $VM"
				MON=`sed -ne 's/^\s*path\s*=\s*"\(.*\)"/\1/p' $CFG`
				echo '{ "execute": "qmp_capabilities" } { "execute": "system_powerdown" }' | socat - $MON > /dev/null
				while ps -p $PID >/dev/null 2>&1; do
					[[ $((i++)) -ge $WAIT ]] && break || sleep 1
				done
				[[ $i -gt $WAIT ]] && kill $PID
			fi
			[[ $i -gt $WAIT ]] && sleep 2
			pgrep -f "qemu-system-x86_64 -name $VM " && exit 1
		done
	;;
	status)
		ps -eo args | sed -ne 's/.*[q]emu-system-x86_64 -name \([^ ]\+\) .* -vnc \([^ ]\+\)/\1\t\2/p'
	;;
	*)
		echo "Unrecognized command: $1"
		exit 2
	;;
esac

Собственно для запуска создаётся служба /usr/lib/systemd/system/guests.service:

[Unit]
Description=Qemu-KVM guests service.
After=network.target

[Service]
Type=forking
ExecStart=/bin/bash /usr/local/bin/guests start
ExecStop=/bin/bash /usr/local/bin/guests stop

[Install]
WantedBy=multi-user.target

Настройка сетевой установки гостевых ОС


Все файлы, нужные для сетевой установки, поместим в разделе /netboot.

DHCP и TFTP

В качестве основного приложения, обеспечивающего сетевую установку, будем использовать dnsmasq. Привожу часть его конфигурации, касающуюся DHCP и TFTP:

dhcp-range=192.168.120.225,192.168.120.252,3h
dhcp-option=option:netmask,255.255.255.0
dhcp-option=option:router,192.168.120.1
dhcp-option=option:dns-server,192.168.120.1
dhcp-option=option:domain-name,shadow.amn
dhcp-option-force=209,pxelinux.cfg/default
dhcp-authoritative
dhcp-vendorclass=set:uefi,"PXEClient:Arch:00007"
dhcp-match=set:uefi,option:client-arch,7
enable-tftp
tftp-root=/netboot/tftp
dhcp-boot=pxelinux.0,host.shadow.amn,192.168.120.1
dhcp-boot=tag:uefi,grubx64.efi,host.shadow.amn,192.168.120.1
pxe-prompt="Press F8 for PXE Network boot.",10
pxe-service=x86PC,"Boot from local disk"
pxe-service=x86PC,"Install OS via PXE",pxelinux

Корнем TFTP указан каталог /netboot/tftp. Поместим в него следующие файлы:

1. Для PXE в режиме BIOS:
  • pxelinux.0, ldlinux.c32, libcom32.c32, libutil.c32, vesamenu.c32 и memdisk из sys-boot/syslinux.
  • Каталог pxelinux.cfg с файлом default, представляющим собой меню загрузчика pxelinux.

2. Для PXE в режиме UEFI:
  • grubx64.efi с установочного DVD CentOS 7.9 (находится в EFI/BOOT).
  • grub.cfg, представляющим собой меню загрузчика Grub.
  • Символьную ссылку grub.cfg-00000000-0000-0000-0000-000000000000 на файл grub.cfg.

3. Для ВМ на CentOS: каталог centos, содержащий файлы initrd.img и vmlinuz с установочного DVD CentOS 7.9 (находятся в images/pxeboot).

4. Для ВМ на Astra Linux: каталог astra, содержащий файлы initrd.gz и linux с установочного DVD Astra Linux.

5. Для ВМ на Windows:
  • каталог windows, содержащий файлы BCD, bootmgr.exe, boot.sdi, pxeboot.0 и winpe.wim, подготовленные с помощью Windows Assessment and Deployment Kit (Windows ADK), пустые файлы boot.ini и hiberfil.sys, каталог Fonts с файлом wgl4_boot.ttf.
  • Символьную ссылку Boot на каталог windows.
  • Символьную ссылку boot.ini на windows/boot.ini.
  • Символьную ссылку bootmgr.exe на windows/bootmgr.exe.
  • Символьную ссылку hiberfil.sys на windows/hiberfil.sys.

Пример файла grub.cfg:

set timeout=60

menuentry 'Rescue installed system (CentOS)' {
    linuxefi centos/vmlinuz inst.stage2=nfs:nfsvers=4:192.168.120.1:/netboot/nfs rescue selinux=0
    initrdefi centos/initrd.img
}

menuentry 'Install CentOS' {
    linuxefi centos/vmlinuz rd.net.timeout.carrier=15 inst.repo=nfs:nfsvers=4:192.168.120.1:/netboot/nfs inst.ks=http://192.168.120.1/ks.cfg inst.sshd
    initrdefi centos/initrd.img
}

menuentry 'Install Astra Linux' {
    linuxefi astra/linux auto=true priority=critical debian-installer/locale=ru_RU console-keymaps-at/keymap=ru url=ftp://192.168.120.1/preseed.cfg interface=auto netcfg/dhcp_timeout=60
    initrdefi astra/initrd.gz
}

Пример файла pxelinux.cfg/default:

default vesamenu.c32

label CentOS
kernel centos/vmlinuz
append initrd=centos/initrd.img rd.net.timeout.carrier=15 inst.repo=nfs:nfsvers=4:192.168.120.1:/netboot/nfs inst.ks=http://192.168.120.1/ks.cfg inst.sshd 

label Astra
menu label Astra Linux
kernel astra/linux
append initrd=astra/initrd.gz auto=true priority=critical debian-installer/locale=ru_RU console-keymaps-at/keymap=ru url=ftp://192.168.120.1/preseed.cfg interface=auto netcfg/dhcp_timeout=60

label Windows
menu label Windows Server
kernel windows/pxeboot.0

NFS

Каталог /netboot/nfs будет точкой монтирования ISO-образа установочного DVD CentOS.

Настроечный файл /etc/exports.d/netboot.exports для сетевой установки CentOS через NFS выглядит так:
/netboot/nfs  -sec=sys,mp,async,no_root_squash,no_subtree_check  192.168.120.0/24

HTTP и FTP

В каталогe /netboot/http будет размещаться ks.cfg — кикстарт для CentOS, в каталоге /netboot/ftp будет размещаться preseed.cfg — аналог кикстарта для Astra Linux и пустой каталог astra — точка монтирования ISO-образа установочного DVD Astra Linux.

Я решил не усложнять задачу установкой полновесных веб- и FTP-серверов, воспользовался апплетами busybox’а. Для запуска их как служб очень хорошо подошёл Systemd.

Создадим службу /etc/systemd/system/httpd@.service:

[Unit]
Description=Busybox HTTPd
Requires=httpd.socket

[Service]
Type=simple
ExecStart=/sbin/httpd -h /netboot/http/ -vvfi
StandardInput=socket
StandardError=journal
TimeoutStopSec=60

[Install]
WantedBy=multi-user.target

И соответствующий ей сокет /etc/systemd/system/httpd.socket:

[Unit]
Description=Busybox HTTPd

[Socket]
ListenStream=192.168.120.1:80
Accept=yes

[Install]
WantedBy=sockets.target

Создадим службу /etc/systemd/system/ftpd@.service:

[Unit]
Description=Busybox FTPd
Requires=ftpd.socket

[Service]
Type=simple
ExecStart=/sbin/ftpd /netboot/ftp
StandardInput=socket
StandardError=journal
TimeoutStopSec=60

[Install]
WantedBy=multi-user.target

И соответствующий ей сокет /etc/systemd/system/ftpd.socket:

[Unit]
Description=Busybox FTPd

[Socket]
ListenStream=192.168.120.1:21
Accept=yes

[Install]
WantedBy=sockets.target

Теперь запуск httpd и ftpd становится просто запуском сокета Systemd:

systemctl start httpd.socket
systemctl start ftpd.socket

CIFS

В каталоге /netboot/smb создадим два пустых каталога (например, 2k16 и virtio), которые будут точками монтирования ISO-образов установочного DVD Windows Server и ISO-образа с драйверами Virtio для Windows. Правда, с установкой сетевых драйверов при сетевой установке Windows Server были сложности, решение, видимо, состоит в том, чтобы включить эти драйвера заранее в ISO-образ установочного DVD Windows Server.

Также в /netboot/smb может находиться файл autounattend.xml для автоматической установки Windows Server.

Часть настроечного файла /etc/samba/smb.conf, касающаяся сетевой установки Windows Server, выглядит так:

[global]
workgroup = AMN
netbios name = HOST
interfaces = 192.168.120.1/255.255.255.0
bind interfaces only = yes
load printers = no
show add printer wizard = no
server string = My notebook
disable netbios = yes

[public]
comment = Public
hosts allow = 192.168.120.0/255.255.255.0
path = /netboot/smb
force user = nobody
force group = nobody
create mask = 0644
directory mask = 0755
read only = yes
guest ok = yes
oplocks = no
level2 oplocks = no
locking = no
acl allow execute always = yes

Как уже было указано, файл winpe.wim был подготовлен с помощью ADK, в него были включены драйвера Virtio (обязательно viostor и netkvm), а также сценарий startnet.cmd:

@echo off
chcp 65001

echo.
echo Запускаю wpeinit.
echo.
wpeinit

echo На выбор доступно три режима работы WinPE:
echo 1) Монтирование сетевого ресурса и возврат в оболочку.
echo.
echo 2) Монтирование сетевого ресурса и запуск установки
echo Windows Server 2016 Standard.
echo.
echo 3) Монтирование сетевого ресурса и запуск автоматической
echo установки Windows Server 2016 Standard
echo.
echo Для выбора пункта меню введите соответствующую ему цифру,
echo а затем нажмите клавишу Enter (ошибочный ввод = 1 пункт).
set /p ID=
echo.

if %ID%==1 goto :first
if %ID%==2 goto :second
if %ID%==3 goto :third
if %ID% GTR 3 goto :failure
if %ID% LSS 3 goto :failure
exit /b

:second
echo Выбран пункт меню под номером 2
echo.
echo Монтирую сетевой ресурс.
net use S: \\192.168.120.1\public
echo Запускаю S:\2k16\setup.exe
S:\2k16\setup.exe
exit /b

:third
echo Выбран пункт меню под номером 3
echo.
echo Монтирую сетевой ресурс.
net use S: \\192.168.120.1\public
echo Запускаю автоматическую установку: S:\2k16\setup.exe /unattend:S:\autounattend.xml
S:\2k16\setup.exe /unattend:S:\autounattend.xml
exit /b

:first
echo Выбран пункт меню под номером 1
echo.

:failure
echo.
echo Монтирую сетевой ресурс.
net use S: \\192.168.120.1\public
exit /b

Небольшая автоматизация


При установке Windows Server некоторую часть настроек приходится выполнять через VNC. Хотелось бы автоматизировать ввод длинных строк или паролей в окне просмотра VNC, чтобы сократить вероятность опечатки. Для Wayland’а имеется утилита ydotool, которая, правда, давно не обновлялась. Ею особенно, на мой взгляд, удобно пользоваться в плиточном оконном менеджере sway, так как запущенное из командной строки приложение gvncviewer всегда оказывается в одном и том же месте экрана.

Ydotool требует модуль uinput, загрузим его и дадим права на чтение и запись группе wheel, в которую входит обычная учётная запись (далее УЗ):

modprobe uinput
chgrp wheel /dev/uinput
chmod g+rw /dev/uinput

Затем, уже от имени обычной УЗ запустим демон ydotoold:

ydotoold &

Готово. Теперь запускаем gvncviewer с указанием адреса и номера экрана нашей ВМ. Возвращаемся обратно в командную строку и даём одной строкой череду команд (добавлены переводы строк для удобства чтения):

ydotool mousemove 350 15;
ydotool click 1;
ydotool key Alt+s;
ydotool type d;
sleep 5;
ydotool type P@ssw0rd;
ydotool key Enter

Данные команды произведут следующие действия:

  1. переключение на окно gvncviewer,
  2. выбор пункта меню «Send Key → Ctrl+Alt+Del»,
  3. пятисекундное ожидание появления окна ввода пароля администратора,
  4. ввод пароля,
  5. нажатие клавиши Enter.

Ссылки


  1. pavsh.ru/official/non-koncerts/stemsite-frolov/public/prosp/legendy.htm — эпиграф.
  2. gpo.zugaina.org — поиск по оверлеям Gentoo, Funtoo и Zentoo.
  3. gitlab.com/Perfect_Gentleman/PG_Overlay — домашняя страница оверлея pg_overlay.
  4. qemu-project.gitlab.io/qemu — документация Qemu.
  5. habr.com/ru/company/infowatch/blog/492260 — описание настройки nft, уже немного устаревшее, так как создавалось до перехода на Systemd.
  6. fedorapeople.org/groups/virt/virtio-win/direct-downloads/latest-virtio — драйвера Virtio для Windows.
  7. docs.microsoft.com/ru-ru/windows-hardware/get-started/adk-install — средства для развёртывания и оценки Windows (Windows ADK).


Автор статьи: Шамиль Саитов Nikodim_Tychoblin
«В лето 7529»

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


  1. Kirikekeks
    28.08.2021 08:43

    Спасибо за статью. Гентушники всегда чему то научат, в данном случае nft без firewalld.

    А почему не предоставляете клиентов на gentoo? Если родительскую систему обслуживает мастер и разгребает все проблемы обнолений, то у меня нет причин волноваться за арендованную ВМ?

    Сегодня разглядывая танцы redhat установка centos выглядит очень сомнительной, даже в трехлетней перспективе. Та же виндовс только линукс. Из rpm based кого видите вокруг если вдруг на замену? Очень любопытно Ваше мнение.


    1. InfoWatch Автор
      30.08.2021 11:45

      Ответ автора: "Спасибо за доброжелательный комментарий. Ставлю виртуальные машины с CentOS и Astra Linux, так как они требуются для моей работы, оттого клиентов на Gentoo не было. Из rpm-based на замену CentOS пока плотно ничего не испытывал, пробовал установку CentOS Sream, первое впечатление обнадёживающее".


  1. mpa4b
    30.08.2021 19:24

    Подскажите, как именно qemu при запуске узнаёт, что ему надо подконнектиться именно вот к этому созданному бриджу?
    ip link add br0 type bridge
    Вот при помощи этого куска конфига?


      driver = "virtio-net-pci"
      netdev = "hostnet0"
      mac = "52:54:00:20:12:04"
      bus = "pcie.0"
    
    [netdev "hostnet0"]
      type = "tap"
      ifname = "vnet8"
      vhost = "on"
      script = "/etc/qemu/ifup.sh"
      downscript = "no"

    Или как-то ещё?


    1. InfoWatch Автор
      01.09.2021 12:40

      Ответ автора: "Всё верно, в разделе netdev указан сценарий поднятия интерфейса ifup.sh, в котором интерфейс ВМ, переданный как аргумент командной строки, подключается к явно указанному мосту br0" .


      1. mpa4b
        02.09.2021 18:20

        Спасибо, стало понятнее.


  1. GrgPlus93
    02.09.2021 22:22

    Может я невнимательно читал, но в чем проблема с сетью у systemd? systemd-networkd может создавать бридж по конфигу и маппить к нему через .network конфиги через [Match]


    1. InfoWatch Автор
      07.09.2021 11:09

      Ответ автора: "Сложность в настройке сети в systemd возникла при создании пустого моста с назначенным IP-адресом, но этом IP-адресе должен был работать dnsmasq. Дело в том, что systemd создавал мост, но вот IP-адрес ему не присваивал, видимо, пока мост пустой. А dnsmasq у меня используется не только для виртуальных машин, но и для Gentoo. Вполне вероятно, что можно было настроить, например, два экземпляра dnsmasq, или использовать systemd-resolved, но мне показалось, что это будет чуточку сложнее, чем добавить отдельную службу для поднятия пустого моста с IP-адресом".


      1. GrgPlus93
        07.09.2021 18:23

        Это правда, бридж не поднимется без единого подключенного устройства. Для поднятия в Вашем случае, я полагаю, можно добавить .netdev с типом dummy без .network параметров, кроме принадлежности к бриджу. Впрочем, естественно, конечная реализация - это на Ваше усмотрение. Я лишь рассуждаю вслух, исходя из позиции, что с сетью у systemd все в порядке.