Skip to content

Latest commit

 

History

History
249 lines (180 loc) · 21.5 KB

2017-04-01-raspberry-pxe.md

File metadata and controls

249 lines (180 loc) · 21.5 KB

Загрузка Raspberry Pi по сети (через PXE)

Попалась мне в руки малинка (а именно, Raspberry Pi 3 Model B Rev 1.2, как она пишет в лог загрузки). Жёсткого диска у компьютера размером с кредитную карточку, конечно же, нет. Есть слот под MicroSD. Но я из тех, кто не любит все эти мелкие карточки, зато любит загрузить компьютер по сети, да так и работать. Благо, как пишут разработчики самой малинки, с третьей версии она поддерживает загрузку через PXE нативно1.

Дано: Raspberry Pi 3 Model B Rev 1.2 и MicroSD-карточка с raspbian, монитор, к которому это можно подключить и загрузить. Ну ещё мой ноутбук, с которого я давно всякие штуковины по сети гружу.

Требуется: загрузить малинку без карточки.

Теория

Загрузка через PXE как правило работает следующим образом:

  • Загрузчик на клиенте (например, BIOS на ноутбуке) делает DHCP-запрос.
  • Вместе с адресом DHCP-сервер возвращает несколько специальных полей про то, как и откуда скачать загрузчик операционной системы.
  • Клиент по TFTP скачивает то, что ему сказали, и запускает. Для x86/amd64 это может быть, например, стадия pxelinux или grub, для Raspberry используются те же bootcode.bin и start.elf, что и при «обычной» загрузке с карточки.
  • Загрузчик скачивает по тому же TFTP настройки, какие-нибудь свои модули, что ему там ещё надо — и в конце ядро с (опционально) initramfs.
  • Запускается ядро, в качестве root= ему дают, например, ссылку на сетевую файловую систему (как правило, NFS). Или не дают :)

Я не очень люблю NFS: работает непонятно как, шифрование там появилось только в четвёртой версии, в настройках экспортов легко напортачить… Зато я люблю sshfs и squashfs. SSH хорошо и просто шифрует, squashfs позволяет разнести по разным местам права на файлы, которые должны быть в загруженной системе, и права, которые используются для доступа по сети. Поэтому у меня свой init-скрипт в initramfs, который делает следующее:

  • Получает по DHCP адрес. Самый первый загрузчик это уже делал, но ядро-то не знает. При этом я использую udhcpc из busybox в качестве DHCP-клиента и не закладываюсь на CONFIG_IP_PNP (получение ip-адреса самим ядром).
  • Монтирует по sshfs директорию с файлом-образом системы.
  • Монтирует образ системы («распаковывает» squashfs).
  • Монтирует tmpfs в отдельную директорию.
  • Объединяет директории при помощи aufs или overlayfs.
  • Переходит в получившуюся систему через switch_root.

В результате все изменения хранятся в памяти, а софт подтягивается по сети. Очень удобно. При этом схему можно слегка модифицировать:

  • Если на клиенте достаточно памяти, а образ системы не слишком большой, то можно его целиком скопировать в tmpfs и не зависеть от ssh-подключения.
  • Вместо tmpfs можно, например, расшифровать и подмонтировать локальный раздел. Это позволяет хранить систему в одном месте (на сервере), но настройки и какие-нибудь данные сохранять на каждом клиенте свои.

Практика

Разработчики малинки написали статью о том, как же загрузить Raspberry без карточки. И ещё одна ценная статья попалась мне на глаза, пока я разбирался, почему как обычно ничего не работает.

Окей, поехали, обо всём по порядку.

Включить PXE в самой малинке

У этого недокомпьютера нету красочного сине-серого BIOS Setup'а, или ещё чего-нибудь2, где можно поставить галочку «Boot from PXE» и всё заработает. Вместо этого, как предлагает, инструкция, следует сделать следующее:

  • Загрузиться классическим способом, с карточки в какой-нибудь raspbian.
  • В файл /boot/config.txt добавить строчку program_usb_boot_mode=1.
  • Перезагрузиться. Как я понимаю, загрузчик, видя эту строчку, всё что надо в энергонезависимую память и прописывает.
  • Строчку можно удалить. Проверить, что нужные биты записались можно так: vcgencmd otp_dump | grep 17, должно вывести 3020000a.

Подготовить систему

В системе, которую мы будем загружать по сети, нам понадобится немного дополнительного софта. Самое время его поставить. Это sshfs и статически собранный busybox (пакет busybox-static в raspbian).

Также понадобится информация, которую следует из запущенной системы получить и сохранить.

  • Настройки ядра: подгрузить модуль командой modprobe configs, появится файл /proc/config.gz.
  • Список библиотек, которые будут нужны в initramfs: вывод ldd на /usr/bin/ssh и /usr/bin/sshfs для работы, для отладки ещё могут понадобиться /usr/bin/strace и сам /usr/bin/ldd.
  • Список доступных видеорежимов: вывод /opt/vc/bin/tvservice -m CEA и /opt/vc/bin/tvservice -m DMT, как рекомендует eLinux wiki.
  • Серийный номер малинки и её mac-адрес. Впрочем, их можно выцепить из сети или логов запросов к DHCP-серверу и TFTP-серверу.

Вот теперь можно вынуть карточку из Raspberry. Система с карточки нам пригодится (собственно, её и будет загружать), но только уже на загрузочном сервере (то есть, моём ноутбуке).

DHCP-сервер

Статья предлагает не перенастраивать DHCP-сервер, а поднять некий [dnsmasq] (https://en.wikipedia.org/wiki/Dnsmasq) в качестве DHCP-прокси, а заодно и TFTP-сервера. Я же больше привык к обычному (в моём случае, net-misc/dhcpcd из gentoo) и поправить конфиги единственного сервера в моей домашней сети не боюсь. Собственно, вот кусок /etc/dhcp/dhcpd.conf:

option option-43 code 43 = text;
host raspberry {
  hardware ethernet <мак-адрес малинки>;
  # fixed-address <адрес для малинки>;
  option option-43 "Raspberry Pi Boot";
  option tftp-server-name "<адрес моего ноутбука>";
} 

Опция 43 называется «Vendor-Option» и её значение зависит от устройства. Так, если Raspberry Pi получает там строчку «Raspberry Pi Boot», то как раз и активируется загрузка по сети. Опция же tftp-server-name просто указывает адрес сервера, у которого по TFTP следует просить файлы.

Обратите внимание на то, что опция fixed-address закомментирована. С ней, как ни странно, не работает. Оказывается, в случае фиксированного адреса DHCP-сервер ведёт себя слегка иначе, нежели при выборе адреса из пула, и у Raspberry Pi из-за этого что-то не срастается. К счастью, я не первый на это напоролся, подробнее можно почитать, например, на форуме.

TFTP-сервер

Загрузчик Raspberry Pi первым делом запрашивает файл bootcode.bin. Его можно взять с карточки, с загрузочного раздела. Инструкция вообще предлагает все файлы из старого /boot скопировать в корень TFTP. Но это как-то грубо, нужны оттуда далеко не все.

Да и копировать их можно не в корень. Следующий файл, start.elf Raspberry сначала пытается взять из директории по названию серийного номера устройства (например, у меня запрашивает e407808b/start.elf) — и если удаётся, все остальные файлы тоже качает из директории. Серийный номер можно посмотреть честными методами, а можно просто подглядеть (через tcpdump или логи TFTP-сервера), какие запросы нам шлют.

Следующий важный файл: fixup.dat. Без него, что характерно, малинка прекрасно загрузится, но доступно будет только 256 мегабайт RAM. И 128 из них уйдут на GPU. Довольно обидно для устройства с гигабайтом на борту.

Настройки: config.txt и cmdline.txt. В первом настройки для загрузчика, во втором — параметры для ядра. Их нам ещё придётся редактировать, но об этом позже.

Ядро. Почему-то называется kernel7.img (семёрка, видимо, означает ARMv7, но моему эстетическому чувству не хватает в названии файла каких-нибудь, не знаю, версий что-ли).

Некий «Device Tree Blob», файлы *.dtp. Для моей достаточно bcm2710-rpi-3-b.dtb.

Пожалуй, хватит. Можно все в корень, а можно в корень только bootcode.bin, остальные — в директорию, соответствующую серийному номеру. Ещё в TFTP надо будет положить архив initramfs, но об этом в следующей части.

initramfs

Следующий шаг — собрать initramfs, который выполнит действия из части «Теория», в результате подмонтирует образ «настоящей» системы и передаст ему управление. Традиционно он создаётся специальными скриптами, но нам придётся это делать самим. Нужен cpio-архив (возможно, сжатый, например, gzip'ом), состоящий из трёх частей:

  • Бинарники, модули ядра и библиотеки, скопированные из (образа) системы.
  • Всякие настройки (ssh-ключи, например).
  • Скрипт инициализации. В моём случае он называется init.sh, о чём надо будет явно сказать ядру (добавить опцию rdinit=/init.sh в cmdline.txt).

Скрипт инициализации я рекомендую прочитать целиком: вот он. Там остались куски, которые мне нужны были для загрузки других систем — например, поиск сетевых карточек среди pci-устройств или копирование sqfs-снимка в tmpfs. Снимок raspbian весит два гигабайта и в гигабайтную память малинки, увы, никак не влезет. Скрипт умеет парсить аргументы для ядра, и там есть один обязательный: netboot_server, адрес SSH-сервера с образом системы.

Отдельный момент — ключи для ssh. Для этого на сервере есть специальный пользователь с минимальными правами, чей приватный ssh-ключ лежит в initramfs (то есть, фактически, раздаётся всем желающим по TFTP). Ну а публичный ключ от сервера следует положить в known_hosts в initramfs.

Также рекомендую почитать скрипт для паковки этого всего: вот. Он у меня тоже рассчитан не только на малинку, а ещё на x86 и amd64 — системы, которые мне доводилось загружать по сети ранее. Скрипт какие-то файлы копирует из снимка (snapshot), какие-то создаёт сам, потом сжимает это всё и кладёт в /var/netboot/tftp/initramfs-raspberry.

В директории конкретного устройства (где лежат всякие start.sh и config.txt) у меня симлинк на получившийся файл, называется initramfs.cpio.gz. Соответственно, в config.txt надо указать, что подгружать: добавить строчку initramfs initramfs.cpio.gz followkernel.

В процессе настройки я наткнулся на обсуждение 2012-го года, в котором жалуются, что загрузчик пусть и подтягивает архив, но не говорит об этом ядру (не проставляет ATAG_INITRD2). К счастью, это, похоже, в прошлом.

Дополнительные настройки

Кроме добавленных опций, я ещё удалил некоторые. Так, из опций запуска (cmdline.txt) я убрал всё, связанное с root: при моём подходе, это не задача ядра что-то куда-то монтировать. Ещё я убрал splash и quiet, потому что они мешают отладке.

Параметры, которые можно указывать в config.txt, довольно интересны. Рекомендую прочитать документацию на эту тему. Например, в какой-то момент мне пришлось установить видеорежим 1920×1080 с поворотом на 90° — иначе стектрейс от очередного kernel panic занимал весь экран и скрывал, собственно, ценное сообщение об ошибке.

Образ системы

Тот самый, который надо сжать в squashfs (снова можно глянуть в pack.sh, хотя там на эту тему только запуск mksquashfs с нужными параметрами и бесполезный гипнокод). Сам снимок можно получить копированием с карточки. Вот только надо поправить /etc/fstab: убрать монтирование разделов с карточки, иначе загрузка на этом и сломается.

Подводные камни

В initramfs должен быть файл /init, как считает switch_root, иначе падает. Я мог бы переименовать свой init.sh в init, но вместо этого сделал в pack.sh фейковый init.

Для Raspberry Pi недоступна netconsole: без подключенного к малинке монитора отлаживать загрузку практически невозможно. Дело в том, что сетевая карта определяется как USB-устройство, а netconsole требует некий Netpoll API, который USB-устройства (пока что?) не поддерживают. В целом, конечно, я обнаглел: хочу чтобы при kernel panic мне через кучу уровней абстракции, используя USB и сеть, исправно посылали логи.

Без файла fixup.dat, как я уже отмечал, малинка загружается, но даёт использовать только 128 мегабайт памяти.

Скопированный из raspbian busybox показался мне каким-то урезанным: там, например, нет lspci, какие привычные мне конструкции в скрипте не работали и пришлось переделывать.

С помощью ldd можно получить список библиотек, которые прилинкованы к бинарнику. Если для статически собранного busybox этот список пустой, то, скажем, ssh требует около дюжины всяких разных so'шников. И дело не ограничивается выводом ldd: во время работы он ещё подгружает libnss_files.so.2 (библиотеку, которая читает всякий /etc/passwd), который тоже придётся копировать в initramfs.

Спасибо за внимание

Буду рад любым вопросам, замечаниям или предложениям. Меня можно достать следующими способами:

1 Для старых версий малинки есть компромиссный вариант: оставить на карточке небольшой загрузчик, который всё остальное стянет по сети.

2 На каких-то desktop'ах я встречал утилиту настройки BIOS с графическим интерфейсом, поддержкой мыши и схлопывающимися списками. До чего техника дошла.