Я много работаю с PXE загрузкой машин по сети. Для тестирования загрузочных образов я использую qemu и однажды я понял, что для установки ubuntu на виртуальную машину мне удобнее использовать PXE, чем загружаться с образа оптического диска. В данной статье я буду работать с ubuntu, однако подобный подход можно применять и для других дистрибутивов (у меня есть позитивный опыт работы со SLES).

Для генерации загрузочного образа нам понадобятся squashfs-tools и debootstrap, установим их:

apt-get update
apt-get install --no-install-recommends -y squashfs-tools debootstrap

Теперь можно установить минимальную систему при помощи debootstrap:

IMAGE_ROOT=/tmp/new_root
mkdir -p "$IMAGE_ROOT" && cd "$IMAGE_ROOT"
. /etc/os-release
debootstrap --arch amd64 --variant=minbase --components=main,restricted,universe \
    --include=live-boot,systemd-sysv,openssh-server,linux-image-virtual,curl \
    ${UBUNTU_CODENAME} .

Как видно из команды я выбираю минимальную установку и добавляю пакеты, которые мне нужны.

Создаем конфиг для apt

printf "deb http://archive.ubuntu.com/ubuntu/ %s main restricted universe multiverse\n" \
	$UBUNTU_CODENAME{,-backports,-updates,-security} > ./etc/apt/sources.list

И для сети

mkdir -p /etc/systemd/{network,system}
cat > ./etc/systemd/network/80-dhcp.network <<NETWORK
[Match]
Name=!lo* !docker*
[Network]
DHCP=yes
NETWORK

Кроме того я добавляю маленький сервис, при помощи которого я смогу после загрузки выполнять произвольные скрипты, которые будут загружаться по http. Так как для загрузки я использую ipxe, то http сервер у меня в любом случае задействован. Добавляя параметр вида http_hook=http://server/path/to/script.sh я могу дополнительно настраивать образ после загрузки. В построенном загрузочном образе ssh ключей нет, так что при помощи такого дополнительного скрипта я добавляю временный ssh ключ, сгеренированный спепциально для конкретного запуска.

cat > ./etc/systemd/system/http_hook.service <<HTTP_HOOK
[Unit]
Description=Fetch via http and run script provided via http_hook kernel parameter
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/bin/bash -c ". <(grep -oP 'http_hook=\K\S+' /proc/cmdline|xargs -r -L1 -P1 curl -sf)"
[Install]
WantedBy=multi-user.target
HTTP_HOOK

Надо активировать новый сервис, а также systemd-networkd

chroot . /bin/bash -c \
    "systemctl enable systemd-networkd.service && systemctl enable http_hook"

Разрешаем root ssh-доступ по ключу

sed -i -e '/^PermitRootLogin/d' \
    -e '$aPermitRootLogin without-password' ./etc/ssh/sshd_config

После удаления лишнего можно сгенерировать образ корневой файловой системы

rm -rf ./var/cache/apt ./etc/{hostname,hosts} ./var/log/*.log ./root/.cache
mksquashfs . ./boot/root.squashfs -b 1048576 -comp xz -Xdict-size 100% -regex \
    -e "proc/.*" -e "sys/.*" -e "run/.*" -e "var/lib/apt/lists/.*" -e "boot/.*"

Образ будет находиться в директории ./boot, так как там же находятся ядро и initrd, что облегчает последующее копирование. Лучше всего все команды выше выполнять в докере, вот скрипт.

Теперь, когда у нас есть необходимые артефакты, можно протестировать их при помощи qemu. Для этого я использую скрипт, который понимает команды start, stop, ssh и console.

Команда console нужна для отладки, когда доступ по ssh по какой-то причине не работает. Но для ее использования надо не забыть в раскомментировать в скрипте построения артефактов строку, которая устанавливает пароль для root.

Для запуска виртуальной машины надо выполнить команду ./qemu.sh start. Эта команда создаст временный ssh ключ для доступа и запустит http сервер (используя встроенный в python 3 модуль http.server). В опциях запуска виртуальной машины мы указываем параметры для использования ipxe, а также форвардим ssh порт с виртуальной машины для последующего доступа.

Как только виртуальная машина будет доступна по ssh скрипт сообщит вам об этом. В противном случае по истечении таймаута скрипт сообщит о неудаче. Для диагностирования проблем лог консоли сохраняется в ./output/qemu.log.

Если машина загрузилась без проблем вы можете зайти на нее по ssh при помощи команды ./qemu.sh ssh.

Для завершения работы машины вы можете либо выполнить команду shutdown -P now непосредственно в сеансе ssh или выполнить команду ./qemu.sh stop.

Теперь я покажу процесс установки ubuntu на диск виртуальной машины. Создаем образ диска

qemu-img create -f qcow2 /var/tmp/myimage.qcow2 16G

Когда диск будет готов надо запустить виртуальную машину вот таким образом

QEMU_OPTS="-drive file=/var/tmp/myimage.qcow2" ./qemu.sh start

и дождаться подтверждения, что машина готова. Теперь залогинимся на виртуальную машину при помощи команды ./qemu.sh ssh и выполним установку ubuntu на диск.

Будем устанавливать ubuntu 24.04, машину назовем ubuntu-dev

INSTALL_CODENAME=noble
INSTALL_HOSTNAME=ubuntu-dev

Установка будет идти через http прокси на хост системе

export http_proxy=http://10.0.2.2:3128/
export https_proxy=$http_proxy

Для установки нам будут нужны debootstrap и gdisk

apt-get update && apt-get install -y debootstrap gdisk

Разметим и отформатируем диск

sgdisk --zap-all /dev/sda
sgdisk -n 0:0:+1MiB -t 0:ef02 -c 0:grub /dev/sda
sgdisk -n 0:0:0 -t 0:8300 -c 0:root /dev/sda
mkfs.ext4 /dev/sda2

Смонтируем диск и установим минимальную систему

mount /dev/sda2 /mnt
debootstrap --arch amd64 --variant=minbase $INSTALL_CODENAME /mnt/

Создадим конфиг для apt

. /etc/os-release 
grep "^deb h" /etc/apt/sources.list \
| sed -e "s/$UBUNTU_CODENAME/$INSTALL_CODENAME/" >/mnt/etc/apt/sources.list

cat <<APT_CONF >/etc/apt/apt.conf.d/99_norecommends
APT::Install-Recommends "false";
APT::AutoRemove::RecommendsImportant "false";
APT::AutoRemove::SuggestsImportant "false";
APT_CONF

Смонтируем псевдо файловые системы для работы в chroot среде

mount --make-private --rbind /dev  /mnt/dev
mount --make-private --rbind /proc /mnt/proc
mount --make-private --rbind /sys  /mnt/sys

Сконфигурируем имя машины, /etc/fstab

echo "$INSTALL_HOSTNAME" >/mnt/etc/hostname
sed -e "s/\(127.0.1.1\).*/\1 $INSTALL_HOSTNAME/" /etc/hosts >/mnt/etc/hosts

cat >/mnt/etc/fstab <<FSTAB
/dev/sda2 / ext4 errors=remount-ro 0 1
tmpfs /tmp tmpfs nosuid,nodev 0 0
FSTAB

Переключаемся в chroot среду

chroot /mnt /bin/bash

Устанавливаем нужные нам пакеты

apt-get update
apt-get install -y grub2 linux-image-generic openssh-server systemd-sysv \
    initramfs-tools sudo

Правим конфиг grub, чтобы можно было пользоваться консолью

 sed -i -e 's/^\(GRUB_CMDLINE_LINUX_DEFAULT\).*/\1="console=ttyS0,115200n8"/' \
    /etc/default/grub

Правим конфиг сети, чтобы получать адрес по dhcp

cat > /etc/systemd/network/80-dhcp.network <<NETWORK
[Match]
Name=en*
[Network]
DHCP=yes
NETWORK

Активируем systemd-networkd

systemctl enable systemd-networkd.service

Конфигурируем пользователя

USERNAME=toor
SSH_PUB=public_ssh_key_you_want_to_use
useradd -m -s /bin/bash $USERNAME
usermod -p '*' $USERNAME
echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/$USERNAME
chmod 0440 /etc/sudoers.d/$USERNAME
mkdir /home/$USERNAME/.ssh
echo "$SSH_PUB" >/home/$USERNAME/.ssh/authorized_keys
chown -R $USERNAME:$USERNAME /home/$USERNAME
usermod -a -G adm,cdrom,dip,plugdev,sudo $USERNAME

Обновляем initrd, устанавливаем grub

update-initramfs -c -k all
grub-install /dev/sda
update-grub

Все готово. Можно выключать машину командой shutdown -P now.

Теперь вы можете загрузить ее с образа диска вот такой командой

qemu-system-x86_64 -drive file=/var/tmp/myimage.qcow2 \
    -device virtio-net-pci,netdev=n1 \
    -netdev user,id=n1,hostfwd=tcp:127.0.0.1:2222-:22 \
    -nographic -enable-kvm -cpu max -m 4096

И получить доступ по ssh

ssh -p 2222 127.0.0.1

Команды выше можно автоматизировать при помощи простого скрипта. Тогда для установки ubuntu надо выполнить вот такую команду

./vm-debootstrap-ubuntu.sh |./qemu.sh ssh

Отдельно стоит отметить, что поскольку машина доступна по ssh можно использовать ansible или другие системы управления конфигурациями.

Обратите внимание, что вся установка в моем случае идет через http прокси, который работает на хост системе. Если вы захотите переиспользовать мой скрипт пожалуйста убедитесь, что у вас работает http прокси. В противном случае вам надо убрать конфигурацию прокси из скрипта и убедиться, что у вас нормально резолвится dns.

Все скрипты из этого поста можно найти на github. Также я настроил в этом репозитории workflow для построения артефактов непосредственно на github. Готовые артефакты можно найти в релизах.

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


  1. gumanzoy
    09.07.2024 09:19

    Не совсем понятно зачем в такой схеме PXE загрузка. Вместо HOST>QEMU>Chroot

    Можно сделать образ, смонтировать его на хосте, выполнить chroot туда, установить все, а затем образ уже запускать в QEMU.


    1. kt97679 Автор
      09.07.2024 09:19

      Безусловно так тоже можно делать. Но для этого вам нужен рутовый доступ на хосте. Плюс во время отладки скриптов есть шанс повредить хост систему. В моем подходе рутовый доступ не нужен и риск повредить хост систему отсутствует.


      1. gumanzoy
        09.07.2024 09:19

        Не думал про возможное отсутствие root доступа.

        А как насчет systemd-nspawn вместо обычного chroot. Вроде есть поддержка Unprivileged Operation. Но это уже теоретически, я не проверял как без root доступа работает.


        1. kt97679 Автор
          09.07.2024 09:19

          увы, опыта работы с systemd-nspawn у меня нет