Я грешен: во мне есть дух соперничества. Когда я услышал, что мой друг заставил Linux загружаться с NFS, мне обязательно нужно было его превзойти. Я обязан был доказать, что могу сделать что-то сложнее, лучше, быстрее, сильнее [прим. пер.: в оригинале отсылка к композиции Daft Punk «Harder, Better, Faster, Stronger»].
Как и все хорошие проекты, этот начался с идеи.
Мой
На грани безумия мой утомлённый
▍ Но как?
Я хотел обеспечить автономность системы, поэтому не мог использовать в качестве «помощника» вторую машину. Мой разум сразу же вспомнил FUSE — программу, работающую драйвером файловой системы в пользовательском пространстве (с поддержкой со стороны ядра).
Мне достаточно было установить программы FUSE в initramfs ядра Linux и сконфигурировать сеть. В этом ведь не должно быть ничего сложного, так?
▍ Процесс запуска Linux
Процесс запуска Linux очень забавен. Позвольте мне на секунду сделать вид, что я его понимаю1:
- Запускается прошивка (BIOS/UEFI) и загружает bootloader.
- Bootloader загружает ядро.
- Ядро распаковывает в ОЗУ временную файловую систему, у которой есть инструменты для монтирования реальной файловой системы.
- Ядро монтирует реальную файловую систему и переключает процесс на систему инициализации в новой файловой системе.
Несмотря на странность третьего этапа, на самом деле он очень полезен! Мы можем смонтировать на этом этапе файловую систему FUSE и выполнить обычный запуск.
1. Главным образом я понимаю его, потому что прочитал эту статью с Archwiki.
▍ Proof of Concept
Файловой системе initramfs требуется и поддержка сети, и двоичные файлы FUSE. К счастью, благодаря Dracut можно достаточно просто собирать собственную initramfs.
Я решил создать её поверх Arch Linux, потому что он относительно легковесен, и я знаком с его работой, в отличие от чего-то наподобие Alpine.
$ git clone https://github.com/dracutdevs/dracut
$ podman run -it --name arch -v ./dracut:/dracut docker.io/archlinux:latest bash
В контейнер я установил несколько пакетов (в том числе и пакет linux
, потому что мне нужно работающее ядро), скомпилированный из исходников dracut
, а также написал простой скрипт модуля в modules.d/90fuse/module-setup.sh
:
#!/bin/bash
check() {
require_binaries fusermount fuseiso mkisofs || return 1
return 0
}
depends() {
return 0
}
install() {
inst_multiple fusermount fuseiso mkisofs
return 0
}
Вот и всё. Это весь код, который мне нужно было написать. Окрылённый уверенностью, я рванул вперёд и собрал образ EFI.
$ ./dracut.sh --kver 6.9.6-arch1-1
--uefi efi_firmware/EFI/BOOT/BOOTX64.efi
--force -l -N --no-hostonly-cmdline
--modules "base bash fuse shutdown network"
--add-drivers "target_core_mod target_core_file e1000"
--kernel-cmdline "ip=dhcp rd.shell=1 console=ttyS0"
$ qemu-kvm -bios ./FV/OVMF.fd -m 4G
-drive format=raw,file=fat:rw:./efi_firmware
-netdev user,id=network0 -device e1000,netdev=network0 -nographic
...
...
dracut Warning: dracut: FATAL: No or empty root= argument
dracut Warning: dracut: Refusing to continue
Generating "/run/initramfs/rdsosreport.txt"
You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot
after mounting them and attach it to a bug report.
To get more debug information in the report,
reboot with "rd.debug" added to the kernel command line.
Dropping to debug shell.
dracut:/#
Голосом хакера: мы в системе. Теперь нужно включить сеть и смонтировать тестовый рут. Я уже извлёк рут Arch Linux в локально работающий бакет S3, так что вроде особых проблем быть не должно. Достаточно вручную настроить сетевые маршруты и загрузить драйверы.
dracut:/# modprobe fuse
dracut:/# modprobe e1000
dracut:/# ip link set lo up
dracut:/# ip link set eth0 up
dracut:/# dhclient eth0
dhcp: PREINIT eth0 up
dhcp: BOUND setting up eth0
dracut:/# ip route add default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15
dracut:/# s3fs -o url=http://192.168.2.209:9000 -o use_path_request_style fuse /sysroot
dracut:/# ls /sysroot
bin dev home lib64 opt root sbin sys usr
boot etc lib mnt proc run srv tmp var
dracut:/# switch_root /sysroot /sbin/init
switch_root: failed to execute /lib/systemd/systemd: Input/output error
dracut:/# ls
sh: ls: command not found
Честно говоря, не знаю, на что я надеялся. Похоже, что всё просто… пропало. Я зашёл в тупик и понятия не имел, что делать. Я потратил несколько дней на изучение, исследование исходного кода switch_root
, ничего не добившись. Но потом я вспомнил о ссылке, которую мне отправил Энтони: How to shrink root filesystem without booting a livecd. По ней была команда pivot_root
, которую switch_root
, похоже, вызывает внутренним образом. Давайте попробуем её.
dracut:/# logout
...
[ 430.817269] ---[ end Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000100 ]---
...
dracut:/# cd /sysroot
dracut:/sysroot# mkdir oldroot
dracut:/sysroot# pivot_root . oldroot
pivot_root: failed to change root from `.' to `oldroot': Invalid argument
Очевидно, pivot_root
не разрешается изменять руты, если переключаемый рут находится в initramfs. Не повезло. В ответе на Stack Exchange рекомендуется использовать switch_root
, что тоже не сработало. Однако моё внимание привлекла часть ответа:
initramfs — это rootfs: для rootfs нельзя ни выполнить pivot_root, ни размонтировать её. Вместо этого удалите всё из rootfs, чтобы освободить пространств (
find -xdev / -exec rm '{}' ';'
), перемонтируйте rootfs с новым рутом (cd /newmount; mount --move . /; chroot .
), подключите stdin/stdout/stderr к новому /dev/console и выполните новый init.
Возможно ли вручную переключить рут без специализированного системного вызова? Что, если я просто выполню chroot?
...
dracut:/# mount --rbind /sys /sysroot/sys
dracut:/# mount --rbind /dev /sysroot/dev
dracut:/# mount -t proc /proc /sysroot/proc
dracut:/# chroot /sysroot /sbin/init
Explicit --user argument required to run as user manager.
Ага, чтобы Systemd запустился нормально, мне нужно выполнять команду chroot
как PID 1. Можно изменить скрипт инициализации initramfs, просто поместить в него мои команды запуска и заменить вызов switch_root
на exec chroot /sbin/init
.
Я поместил это в modules.d/99base/init.sh
в исходниках Dracut после загрузки правил udev и обхода предыдущих проверок переменной root
.
modprobe fuse
modprobe e1000
ip link set lo up
ip link set eth0 up
dhclient eth0
ip route add default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15
s3fs -o url=http://192.168.2.209:9000 -o use_path_request_style fuse /sysroot
mount --rbind /sys /sysroot/sys
mount --rbind /dev /sysroot/dev
mount -t proc /proc /sysroot/proc
Ещё я добавил в конец exec chroot /sysroot /sbin/init
вместо команды switch_root
.
Пересобрал образ EFI, и…
Я сидел перед экраном, не веря своим глазам. Не может же всё быть так просто? Наверняка это богохульство, и дух Денниса Ритчи снизойдёт сейчас, чтобы остановить меня.
Но никто меня не остановил, поэтому я двинулся дальше.
Я залогинился как root
с совершенно безопасным паролем root
и без церемоний попал в шелл.
[root@archlinux ~]# mount
s3fs on / type fuse.s3fs (rw,nosuid,nodev,relatime,user_id=0,group_id=0)
...
[root@archlinux ~]#
Наконец-то Linux загрузился из бакета S3. Меня уже подмывало поделиться своим достижением с остальными, достаточно было лишь запустить программу fetch, чтобы показать её на скриншоте:
[root@archlinux ~]# pacman -Sy fastfetch
:: Synchronizing package databases...
core.db failed to download
error: failed retrieving file 'core.db' from geo.mirror.pkgbuild.com : Could not resolve host: geo.mirror.pkgbuild.com
warning: fatal error from geo.mirror.pkgbuild.com, skipping for the remainder of this transaction
error: failed retrieving file 'core.db' from mirror.rackspace.com : Could not resolve host: mirror.rackspace.com
warning: fatal error from mirror.rackspace.com, skipping for the remainder of this transaction
error: failed retrieving file 'core.db' from mirror.leaseweb.net : Could not resolve host: mirror.leaseweb.net
warning: fatal error from mirror.leaseweb.net, skipping for the remainder of this transaction
error: failed to synchronize all databases (invalid url for server)
[root@archlinux ~]#
Ой, похоже, DNS не работает, а у меня нет dig
и других инструментов отладки.
Постойте-ка! Файловая система рута находится на S3! Я могу просто смонтировать её куда-то ещё, где работает сеть, выполнить chroot
и установить все нужные утилиты!
Немного позанимавшись отладкой, я выяснил, что systemd-resolved отказывается запускаться, потому что Failed to connect stdout to the journal socket, ignoring: Permission denied
. Я не собираюсь пытаться отладить systemd, потому что это слишком сложно, а я ленив, поэтому вместо этого я воспользуюсь DNS Cloudfare.
[root@archlinux ~]# echo "nameserver 1.1.1.1" > /etc/resolv.conf
[root@archlinux ~]# pacman -Sy fastfetch
:: Synchronizing package databases...
core is up to date
extra is up to date
...
[root@archlinux ~]# fastfetch
Меня по-прежнему никто не останавливал. Никто не вламывался в окно, и сигнализация не сработала. Можно было спокойно продолжать.
Я был готов запустить Linux с Google Drive.
▍ Дело дошло до Google
Уже существует готовый проект, который позволяет пользоваться Google Drive через FUSE: google-drive-ocamlfuse. К счастью, у меня есть аккаунт Google, которым я не пользовался многие годы! Я выполнил инструкции, не читая принял условия использования, создал все секреты oauth2, включил APIs, установил google-drive-ocamlfuse
из AUR в мою Arch Linux VM, пропатчил несколько PKGBUILD
(давно этого не делал), и на этом всё! Я примонтировал Google Drive! Смонтировав Drive и выполнив несколько очень долгих rsync
, я получил Arch Linux на Google Drive.
Шучу, конечно, никогда не бывает всё так просто. Вот неполный список проблем, с которыми я столкнулся:
- Не работают симлинки на симлинки (очень важно для того, что находится в
/usr/lib
). - Не работают жёсткие ссылки.
- Всё о-о-очень медленно.
- Относительные симлинки вообще не работают.
- Отсутствуют повисшие симлинки (важно для того, что ссылается
/proc
и не примонтировано, или для того, что пока ещё не скопировано). - Не работают симлинки за пределами Google Drive.
- Не работают разрешения (как и атрибуты).
- Я ведь уже говорил, что всё медленное?
Учитывая количество проблем с симлинками, я уже почти был готов изменить код драйвера FUSE, чтобы он просто создавал файл, заканчивающийся на .internalsymlink
, чтобы всё это исправить (будь ты проклята, совместимость с Google Drive).
Но я поставил перед собой задачу сделать это, не исправляя ничего важного (никаких изменений в ядре и в драйвере FUSE), так что мне придётся просто смириться с этим и вручную создать все симлинки, которые не получается создать у rsync
, применив команду sed
к логам ошибок rsync
.
Тем временем я добавил в initramfs файлы токенов, сгенерированные на моём ноутбуке, двоичный файл FUSE Google Drive и сертификаты SSL, а также настроил несколько параметров2, чтобы сильно упростить себе жизнь.
2. Я задал acknowledge_abuse=true
и root_folder=fuse-root
.
...
inst ./gdfuse-config /.gdfuse/default/config
inst ./gdfuse-state /.gdfuse/default/state
find /etc/ssl -type f -or -type l | while read file; do inst "$file"; done
find /etc/ca-certificates -type f -or -type l | while read file; do inst "$file"; done
...
Здорово видеть, что, по крайней мере, работают метки времени. Теперь осталось лишь подождать мучительно медленного запуска!
chroot: /sbin/init: File not found
Вероятно, меня никто не останавливал, потому что я всё равно потерплю неудачу.
Я знаю, что файл существует, но почему же он не найден? Всё просто: Linux — это довольно странная система: если вызываемый двоичный файл зависит от библиотеки, которая не найдена, то вы получите «File not found».
dracut:/# ldd /sysroot/bin/bash
linux-vdso.so.1 (0x00007e122b196000)
libreadline.so.8 => /usr/lib/libreadline.so.8 (0x00007e122b01a000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007e122ae2e000)
libncursesw.so.6 => /usr/lib/libncursesw.so.6 (0x00007e122adbf000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007e122b198000)
Однако на самом деле этих симлинков не существует! Помните, выше мы говорили, что относительные симлинки не работают? Так вот, они нанесли ответный удар. Ядро ищет файлы в /sysroot
внутри /sysroot/sysroot
. К счастью, исправить это достаточно легко: нужно всего лишь связать /sysroot
с /sysroot/sysroot
без линков:
dracut:/# mkdir /sysroot/sysroot
dracut:/# mount --rbind /sysroot /sysroot/sysroot
Настало время запуска!
Arch потребовалось пять минут на пересоздание кэша динамического компоновщика, ещё минута на systemd unit, а потом ничего не произошло. Запуск прекратился.
[ TIME ] Timed out waiting for device /dev/ttyS0.
[DEPEND] Dependency failed for Serial Getty on ttyS0.
Наверно, нужно увеличить таймаут и перезапуститься. В /etc/systemd/system/dev-ttyS0.device
я указал следующее:
[Unit]
Description=Serial device ttyS0
DefaultDependencies=no
Before=sysinit.target
JobTimeoutSec=infinity
К счастью, запуск занял не бесконечное количество времени.
Я уже близок к победе! Осталось увеличить ещё один таймаут. Я присвоил LOGIN_TIMEOUT
значение 0
в /etc/login.defs
Google Drive и снова попробовал выполнить вход.
К счастью, существует кэш, так что последующие операции чтения файлов стали намного быстрее.
Ура, можно надевать лавровый венок: моя химера из Linux и Google Drive ожила.
Но я пока не закончил. Никто меня не остановил, потому что все хотели, чтобы я одержал победу. Мне нужно её развить. Нужно, чтобы это заработало на реальном оборудовании.
▍ А теперь сделаем это на реальном оборудовании
К счастью, я поменял серверы и теперь у меня есть лишний ноутбук без накопителя! Идеальная жертва3 для экспериментов!
3. При работе над этим проектом ни один компьютер не пострадал (физически).
Мне пришлось внести некоторые изменения:
- Использовать подходящий драйвер Ethernet, а не стандартный
e1000
. - Не использовать последовательней дисплей.
- Изменить параметры сети, чтобы они соответствовали топологии моей домашней сети.
Мне достаточно драйвера r8169
для Ethernet-порта, а ещё добавим сюда Powerline, потому что это существенно не повлияет на производительность, а у меня нет Ethernet-кабеля, который бы можно было дотянуть до моей комнаты.
Я собрал единый файл EFI, закинул его в /BOOT/EFI
USB-накопителя, а затем подключил его в свой старый сервер. Несмотря на все усилия, я так и не разобрался, какая директива modprobe нужна для встроенной клавиатуры ноутбука, поэтому просто выполнил hid_usb
modprobe и подключил внешнюю клавиатуру для настройки сети.
Вот мой magnum opus. Моё великое творение. Этот след надолго останется на Земле после моего ухода: нативный облачный компьютер.
Здорово то, что я могу просто взять скриншот4 с Google Drive и выложить его сюда!
4. Я сделал скриншот с помощью fbgrab.
▍ Узрите — нативный облачный компьютер!
Несмотря на несерьёзность моего проекта, можно придумать ему достаточно серьёзные применения, например, запуск Linux с SSH, а может, запуск Linux из репозитория Git и отслеживание всех изменений в Git при помощи gitfs. Несмотря на посредственную полезность, его возможности бесконечны.
Если я что-то и знаю о технологиях, так это то, что современный тренд — это перенос всего в Облако. Поэтому я уже готов коммерциализировать этот проект для любой компании, желающей отказаться от своего ненадёжного аппаратного накопителя и полностью перейти в Облако. Если вас интересует Истинный Нативный Облачный Компьютинг, оставьте заявку.
К сожалению, не знаю, что делать с этим дальше. Возможно, стоит установить Nix?
Автор: ru_vds