Я грешен: во мне есть дух соперничества. Когда я услышал, что мой друг заставил Linux загружаться с NFS, мне обязательно нужно было его превзойти. Я обязан был доказать, что могу сделать что-то сложнее, лучше, быстрее, сильнее [прим. пер.: в оригинале отсылка к композиции Daft Punk «Harder, Better, Faster, Stronger»].

Как и все хорошие проекты, этот начался с идеи.

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

На грани безумия мой утомлённый мозг придумал мой magnum opus: запуск Linux с рута Google Drive.

▍ Но как?


Я хотел обеспечить автономность системы, поэтому не мог использовать в качестве «помощника» вторую машину. Мой разум сразу же вспомнил FUSE — программу, работающую драйвером файловой системы в пользовательском пространстве (с поддержкой со стороны ядра).

Мне достаточно было установить программы FUSE в initramfs ядра Linux и сконфигурировать сеть. В этом ведь не должно быть ничего сложного, так?

▍ Процесс запуска Linux


Процесс запуска Linux очень забавен. Позвольте мне на секунду сделать вид, что я его понимаю1:

  1. Запускается прошивка (BIOS/UEFI) и загружает bootloader.
  2. Bootloader загружает ядро.
  3. Ядро распаковывает в ОЗУ временную файловую систему, у которой есть инструменты для монтирования реальной файловой системы.
  4. Ядро монтирует реальную файловую систему и переключает процесс на систему инициализации в новой файловой системе.

Несмотря на странность третьего этапа, на самом деле он очень полезен! Мы можем смонтировать на этом этапе файловую систему 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, потому что это слишком сложно, а я ленив, поэтому вместо этого я воспользуюсь systemd Cloudflare.

[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.

Шучу, конечно, никогда не бывает всё так просто. Вот неполный список проблем, с которыми я столкнулся:

  1. Не работают симлинки на симлинки (очень важно для того, что находится в /usr/lib).
  2. Не работают жёсткие ссылки.
  3. Всё о-о-очень медленно.
  4. Относительнче симлинки вообще не работают.
  5. Отсутствуют повисшие симлинки (важно для того, что ссылается /proc и не примонтировано, или для того, что пока ещё не скопировано).
  6. Не работают симлинки за пределами Google Drive.
  7. Не работают разрешения (как и атрибуты).
  8. Я ведь уже говорил, что всё медленное?

Учитывая количество проблем с симлинками, я уже почти был готов изменить код драйвера 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. При работе над этим проектом ни один компьютер не пострадал (физически).

Мне пришлось внести некоторые изменения:

  1. Использовать подходящий драйвер Ethernet, а не стандартный e1000.
  2. Не использовать последовательней дисплей.
  3. Изменить параметры сети, чтобы они соответствовали топологии моей домашней сети.

Мне достаточно драйвера 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?

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

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