Попалась мне в руки малинка (а именно, 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 без карточки. И ещё одна ценная статья попалась мне на глаза, пока я разбирался, почему как обычно ничего не работает.
Окей, поехали, обо всём по порядку.
У этого недокомпьютера нету красочного сине-серого 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-сервер, а поднять некий [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 из-за этого что-то не срастается. К счастью,
я не первый на это напоролся, подробнее можно почитать, например,
на форуме.
Загрузчик 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
, который выполнит действия из части «Теория», в результате
подмонтирует образ «настоящей» системы и передаст ему управление. Традиционно он создаётся
специальными скриптами, но нам придётся это делать самим. Нужен 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.
Буду рад любым вопросам, замечаниям или предложениям. Меня можно достать следующими способами:
- Telegram: https://t.me/burunduk3
- ВКонтакте: https://vk.com/burunduk3
- e-mail: burunduk3@gmail.com
1 Для старых версий малинки есть компромиссный вариант: оставить на карточке небольшой загрузчик, который всё остальное стянет по сети.
2 На каких-то desktop'ах я встречал утилиту настройки BIOS с графическим интерфейсом, поддержкой мыши и схлопывающимися списками. До чего техника дошла.