LaurVas

Делаем загрузочный образ из контейнера

debian, linux

Пользовался я контейнерами и бед не знал. Но вот понадобилось как-то собрать пакет openjdk-6. А он при сборке ругается на ядро: uname -a ему не нравится. Говорит не буду такое поддерживать. Значит надо собирать на железе или виртуалке. Устанавливать систему с нуля не хотелось — слишком долго. И тогда я подумал, что можно развернуть бэкап контейнера на диск, добавить загрузчик и ядро и получить Debian, способный работать самостоятельно. А чтобы не выполнять одну и ту же работу дважды, я решил работать не на диске, а в образе, и сохранить его на будущее.

Эта инструкция подойдёт для всех релизов Debian и Ubuntu. Я буду переносить Debian Jessie.

Первым делом создадим образ диска. Простой, надёжный, и 100% рабочий вариант — использовать для выделения места утилиту dd. Она создаст файл заданного размера, заполнив пространство на диске нулями. Более быстрый способ — попросить файловую систему зарезервировать пустое место под файл командой fallocate. Она работает только на тех файловых системах, которые поддерживают разреженные (sparse) файлы: btrfs, ext4, ocfs2, xfs. Универсальная утилита — truncate. Судя по её поведению, она создаёт разреженный файл там, где это возможно, и заполняет нулями там, где нет.

Много места не нужно, лишь бы контейнер уместился с запасом. Я делаю 4 ГиБ.

# truncate -s 4096M jessie.img

Для лучшего понимания происходящего я подготовил схему. Это наш пустой образ:

Натягиваем jessie.img на первое свободное петлевое устройство:

# losetup -f jessie.img --show

Дальше работаем с петлевым устройством как с обычным диском. Создаём таблицу разделов и первый раздел:

# parted -s /dev/loopX mklabel msdos
# parted -s /dev/loopX -- mkpart primary ext2 1MiB -1s

Вместо loopX должно быть loop0, loop1, loop2 и т.д, в зависимости от того, куда натянулся jessie.img.

Первая команда создаёт таблицу разделов MBR (не GPT). Вторая команда создаёт раздел, смещённый относительно начала диска ровно на 1 МиБ (т.н. “выравнивание” раздела) и заканчивающийся последним сектором (-1s). Нумерация секторов начинается с нуля. Ключ -- означает, что дальше аргументов не будет и нужен для правильного восприятия -1s.

После разметки имеем следующую картину:

Информация о разделе записана в таблицу разделов, но файловой системы на нём ещё нет. ext2 во второй команде — это только тип раздела — однобайтный идентификатор, записываемый в таблицу разделов MBR. У файловых систем ext2/3/4 идентификатор одинаковый и равен 0x83.

Хорошая подсказка по идентификаторам есть в утилите fdisk:

# echo l | fdisk /dev/sdX

Проверяем что всё разметилось правильно:

# fdisk -lu /dev/loopX
Диск /dev/loopX: 4 GiB, 4294967296 байт, 8388608 секторов
Единицы: секторов по 1 * 512 = 512 байт
Размер сектора (логический/физический): 512 байт / 512 байт
Размер I/O (минимальный/оптимальный): 512 байт / 512 байт
Тип метки диска: dos
Идентификатор диска: 0x55242bd0

Устр-во      Загрузочный начало   Конец Секторы Размер Идентификатор Тип
/dev/loopXp1               2048 8388607 8386560     4G            83 Linux

Что тут надо увидеть:

  1. Общий размер диска в секторах — 8388608.
  2. Начальный сектор раздела — 2048. Нумерация секторов начинается с нуля (как в сишных массивах), значит перед разделом есть 2048 / 2 / 1024 = 1МиБ.
  3. Конечный сектор раздела — 8388607. Это на единицу меньше, чем общий размер диска, значит это действительно последний сектор.

После разметки должен появиться /dev/loopXp1, проверяем командой lsblk. Если не появился, то перенатягиваем образ:

# losetup -d /dev/loopX
# losetup -f -P --show jessie.img

Форматируем наш раздел. В качестве файловой системы будем использовать Ext4. Это хорошая, стабильная файловая система. Я вижу два существенных преимущества Ext4 по сравнению с Ext2 и Ext3:

  • возможность эффективного хранения разреженных (sparse) файлов,
  • поддержка ATA-команды TRIM.
# mkfs.ext4 -m 1 -O ^has_journal /dev/loopXp1
или
# mkfs.ext4 -m 1 /dev/loopXp1

-m 1 — зарезервировать 1% пространства под нужды суперпользователя (по умолчанию 5%).
-O ^has_journal отключает журналирование на ФС. Отсутствие журнала сокращает нагрузку на диск, но может привести к нарушению целостности ФС при внезапном отключении питания.

Наш образ после форматирования:

Монтируем файловую систему, чтобы получить к ней доступ. Каталог /mnt/t выбран только лишь потому, что его быстро набирать на клавиатуре.

# mkdir /mnt/t && mount /dev/loopXp1 /mnt/t

Распаковываем имеющийся бэкап контейнера:

# tar xpf jessie-vas.tar.xz -C /mnt/t --numeric-owner

Монтируем всё необходимое и чрутаемся внутрь:

# cd /mnt/t
# mount proc -t proc ./proc
# mount sys -t sysfs ./sys
# mount --bind /dev ./dev
# mount --bind /dev/pts ./dev/pts
# chroot ./ /usr/bin/env -i HOME=/root TERM="$TERM" /bin/bash --login

Дальше по инструкции устанавливаем часовой пояс, задаём имя хоста, создаём fstab, устанавливаем ядро и загрузчик. Отличие только в том, что вместо физического диска /dev/sdX у нас /dev/loopX, а первый раздел обозначается не /dev/sdX1, а /dev/loopXp1.

Устанавливаем пакеты для настройки сети и ssh-сервер для удобства:

(chroot)# apt-get install dhcpcd iproute openssh-server

Удаляем лишнее:

(chroot)# rm /etc/mtab /etc/resolv.conf /usr/sbin/policy-rc.d

Размонтируем всё:

(chroot)# exit
# cd
# umount /mnt/t/{proc,sys,dev/pts,dev,}

Наш образ после переноса контейнера:

Важное замечание для 32-битных систем. Если у файловой системы включена опция 64bit, то 32-битное ядро не подружится с ней. Проверяем:

# tune2fs -l /dev/loopXp1 | grep features

Отключаем при необходимости:

# e2fsck -f /dev/loopXp1
# resize2fs -s /dev/loopXp1

Теперь маленький трюк для экономии места. Уменьшаем (shrink) файловую систему:

# resize2fs -M /dev/loopXp1
resize2fs 1.43.3 (04-Sep-2016)
Resizing the filesystem on /dev/loopXp1 to 96809 (4k) blocks.
The filesystem on /dev/loopXp1 is now 96809 (4k) blocks long.

Новый размер ФС (96809 блоков по 4 КиБ) надо запомнить. Нам предстоит уменьшить раздел до этого размера. Для этого надо правильно вычислить номер последнего сектора: 2048 + 96809*8 - 1 = 776519.

# parted /dev/loopX resizepart 1 776519s

После уменьшения раздела из 8388608 секторов диска осталось занято 776520 (примерно 380 МиБ). Место в конце никак не используется.

# fdisk -lu /dev/loopX
Диск /dev/loopX: 4 GiB, 4294967296 байт, 8388608 секторов
Единицы: секторов по 1 * 512 = 512 байт
Размер сектора (логический/физический): 512 байт / 512 байт
Размер I/O (минимальный/оптимальный): 512 байт / 512 байт
Тип метки диска: dos
Идентификатор диска: 0x55242bd0

Устр-во      Загрузочный начало   Конец Секторы Размер Идентификатор Тип
/dev/loopXp1               2048  776519  774472 380.3M            83 Linux

Отключаем образ от петлевого устройства и уменьшаем до нужного размера:

# losetup -d /dev/loopX
# truncate -s $((774473 * 512)) jessie.img

И напоследок сжимаем архиватором:

# xz jessie.img

На этом основная работа закончена. Мы получили сжатый образ jessie.img.xz, который в любой момент можно развернуть на флэшку или виртуальный диск и загрузить настроенный Debian. Благодаря уменьшению ФС образ занимает минимум места и влезет даже на флэшку в 512 MB.

Записываем образ на флэшку или диск /dev/sdX, предварительно убедившись, что там нет ничего ценного!

# xzcat jessie.img.xz | dd of=/dev/sdX bs=1M

Растягиваем ранее сжатый раздел на весь размер диска:

# parted -s /dev/sdX -- resizepart 1 -1s
# e2fsck -f /dev/sdX1
# resize2fs /dev/sdX1

Всё, можно загружаться с /dev/sdX.

Если нужен виртуальный диск для виртуальной машины, то сначала увеличиваем образ до нужного размера, затем преобразуем его в один из форматов: vdi, vmdk или vhd:

# xz --decompress --keep jessie.img.xz
# truncate -s 1G jessie.img
# losetup -f -P --show jessie.img
# parted -s /dev/loopX -- resizepart 1 -1s
# e2fsck -f /dev/loopXp1
# resize2fs /dev/loopXp1
# losetup -d /dev/loopX
# vboxmanage convertfromraw --format VDI jessie.img jessie.vdi

Да, openjdk6 я в итоге успешно собрал.