Продолжаем серию статей о виртуализации на базе KVM. В предыдущих статьях было рассказано об инструментарии, о настройке хост-машины и создании виртуальной машины. Сегодня мы поговорим о создании образа виртуальной машины и его клонировании.
Исследования вопроса дали удручающие результаты: информацию по созданию образов виртуальных машин в сети очень сложно найти, а та, что есть, качеством и полнотой не отличается.
Для получения образа виртуальной машины в минимальной системе в ней достаточно поменять всего пару файлов чтобы получить нормально работающую систему, но в случае Debian появляются небольшие сложности.
Для создания новой виртуальной машины на основе имеющейся системы нужно внести следующие изменения:
- изменить hostname
- поправить файл hosts
- изменить настройки DNS
- заменить хост-ключи SSH
- изменить пароль для root
Большой находкой для меня оказалась библиотека libguestfs — она позволяет управлять дисками и оперировать файлами виртуальных машин как в интерактивном режиме, так и по заранее составленному сценарию.
Эту библиотеку написал Richard Jones из небезызвестной компании Red Hat. Она позволяет работать с файловыми системами (начиная от ext2 и заканчивая NTFS в Windows, UFS в FreeBSD — в общем, со всеми файловыми системами, с которыми умеет работать ядро), образами систем, LVM-разделами, в случае установки гостевых ОС из семейства MS Windows — править системный реестр (через библиотеку hivex). В общем, утилита очень богатая возможностями и очень гибкая. И что самое главное — не требует административных (root) прав для ее использования.
Исследуем образ
Итак, приступим к работе.
Основным инструментом, с помощью которого мы будем работать с образом гостевой системы, является guestfish.
Попробуем произвести некоторые операции в интерактивном режиме:
$ guestfish
><fs> add-drive debian_5_i386.img
><fs> run
><fs> list-filesystems
/dev/vda1: ext3
><fs> mount-vfs rw ext3 /dev/vda1 /
><fs> cat /etc/fstab
# /etc/fstab: static file system information.
#
# <file system> <mount point> <type> <options> <dump> <pass>
proc /proc proc defaults 0 0
/dev/vda1 / ext3 errors=remount-ro 0 1
/dev/hdc /media/cdrom0 udf,iso9660 user,noauto 0 0
Что очень здорово — все необходимые операции можно производить и в неинтерактивном режиме (по заранее составленному сценарию). Приведу пример скрипта, который редактирует файлы hosts, hostname и interfaces в системе:
$ guestfish <<EOF
add-drive debian_guest.img
run
mount-vfs rw ext3 /dev/vda1 /
upload -<<END /etc/hosts
127.0.0.1 localhost.localdomain localhost debian_guest.local debian_guest
10.10.10.100 debian_guest.local
END
upload -<<END /etc/resolv.conf
nameserver 8.8.8.8
END
upload -<<END /etc/hostname
debian_guest.local
END
upload -<<END /etc/network/interfaces
auto lo
iface lo inet loopback
allow-hotplug eth0
iface eth0 inet static
address 10.10.10.100
gateway 10.10.10.10
netmask 255.255.255.0
network 10.10.10.0
broadcast 10.10.10.255
END
EOF
Использование heredoc оказалось очень удобным в данном контексте.
(К слову: если возникают какие-либо вопросы по библиотеке, на них сам автор очень быстро отвечает на IRC канале #libguestfs на irc.freenode.net. Да и вообще парень очень интересный.)
Secure Hell
Как видно из названия, я с этим вопросом достаточно долго промучился: в Debian/Ubuntu автоматической регенерации ключей при их удалении попросту нет. В других системах, которые я пробовал использовать, с этим всё в порядке, а для deb-based операционных систем с этим проблемы.
Я сделал вот так:
$ guestfish
><fs> add-drive debian_guest.img
><fs> run
><fs> mount-vfs rw ext3 /dev/vda1 /
><fs> download /etc/init.d/ssh /home/username/debian_5_etc_init_ssh
Далее были сделаны следующие изменения:
--- /home/username/debian_5_etc_init_ssh 2012-12-21 00:00:00.000000000 +0000
+++ /home/username/debian_5_etc_init_ssh_fixed 2012-12-21 00:00:00.000000000 +0000
@@ -32,6 +32,10 @@
([ "$previous" ] && [ "$runlevel" ]) || [ "$runlevel" = S ]
}
+check_ssh_host_key() {
+ if [ ! -e /etc/ssh/ssh_host_key ] ; then
+ echo "Generating Hostkey..."
+ /usr/bin/ssh-keygen -t rsa1 -f /etc/ssh/ssh_host_key -N '' || return 1
+ fi
+ if [ ! -e /etc/ssh/ssh_host_dsa_key ] ; then
+ echo "Generating DSA-Hostkey..."
+ /usr/bin/ssh-keygen -d -f /etc/ssh/ssh_host_dsa_key -N '' || return 1
+ fi
+ if [ ! -e /etc/ssh/ssh_host_rsa_key ] ; then
+ echo "Generating RSA-Hostkey..."
+ /usr/bin/ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N '' || return 1
+ fi
+}
+
check_for_no_start() {
# forget it if we're trying to start, and /etc/ssh/sshd_not_to_be_run exists
if [ -e /etc/ssh/sshd_not_to_be_run ]; then
@@ -75,6 +79,7 @@
case "$1" in
start)
+ check_ssh_host_key
check_privsep_dir
check_for_no_start
check_dev_null
@@ -106,6 +111,7 @@
;;
restart)
+ check_ssh_host_key
check_privsep_dir
check_config
log_daemon_msg "Restarting OpenBSD Secure Shell server" "sshd"
Внимание, патч нерабочий, он приведён как пример необходимых изменений.
И для двух версий Debian/Ubuntu я сделал аналогичный файл с уже изменённым файлом ssh. Далее его можно просто загрузить в виртуальную машину.
><fs> upload /home/username/debian_5_etc_init_ssh_fixed /etc/init.d/ssh
А теперь удалим ключи, чтобы они сгенерировались автоматически:
><fs> glob rm /etc/ssh_host_*_key*
Удаление по маске не работает. Поскольку в API данный метод не реализован, префикс glob позволяет развернуть маску в список файлов.
Для FreeBSD и CentOS достаточно просто удалить ключи, при старте они сами сгенерируются.
Идентификация пользователей
Для начала стоит рассказать о том, как представлено хранение информации о пользователях в Linux/FreeBSD. Это будет немного занудно, но необходимо для понимания того, что мы всё-таки делаем. Хотя по минимуму достаточно информации только о shadow-файле.
Вся необходимая для аутентификации пользователей хранится в файлах /etc/passwd и /etc/shadow(/etc/master.passwd в FreeBSD).
Рассмотрим структуру файла /etc/passwd
root:x:0:0:root:/root:/bin/bash
Процитирую из вики порядок использования полей:
- регистрационное имя или логин
- хеш пароля (сейчас не используется, используется скрытый в shadow пароль)
- идентификатор пользователя
- идентификатор группы по умолчанию
- информационное поле GECOS
- начальный (он же домашний) каталог
- регистрационная оболочка, или shell
Рассмотрим структуру /etc/shadow
root:$1$APv1HQOB$HJQhYFq9JSnhusQ.1Ql10.:14977:0:99999:7:::
Опять же из wiki:
- имя пользователя
- хэш пароля
- дата последнего изменения пароля
- через сколько дней можно будет поменять пароль
- через сколько дней пароль устареет
- за сколько дней до того, как пароль устареет, начать напоминать о необходимости смены пароля
- через сколько дней после того, как пароль устареет, заблокировать учётную запись пользователя
- дата, при достижении которой учётная запись блокируется
- зарезервированное поле
Нам нужно изменить конкретно второе поле (хэш пароля). Его можно разбить на три части:
- 1 — тип шифрования md5, 2 — SHA512 (поправьте меня если я не прав)
- APv1HQOB — соль, через которую шифруется пароль
- HJQhYFq9JSnhusQ.1Ql10. — непосредственно хэш пароля с солью.
Хэш генерируется командой:
$ mkpasswd --method=md5 --salt="APv1HQOB" "$password"
$1$APv1HQOB$HJQhYFq9JSnhusQ.1Ql10.
Его нам и нужно подставить в файл /etc/shadow.
Я написал небольшой скрипт, который будет генерировать случайный пароль и соль длиной 8 символов, выводить его, генерировать хэш и подставлять его в нужный файл:
#!/bin/bash
tempfile=`mktemp`
shadow="/etc/shadow"
salt=`pwdgen`
passwd=`pwdgen`
hash=`pwhash $salt $password`
hash_esc=`escape_hash $hash `
pwdgen() {
charspool=('a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j' 'k' 'l' 'm' 'n' 'o' 'p' 'q' 'r' 's' 't' 'u' 'v' 'w' 'x' 'y' 'z' '0' '1' '2' '3' '4' '5' '6' '7' '8' '9' '0' 'A' 'B' 'C' 'D' 'E' 'F' 'G' 'H' 'I' 'J' 'K' 'L' 'M' 'N' 'O' 'P' 'Q' 'R' 'S' 'T' 'U' 'V' 'W' 'X' 'Y' 'Z');
len=${#charspool[*]}
for c in $(seq 8); do
echo -n ${charspool[$((RANDOM % len))]}
done
}
pwhash(){
salt=$1
password=$2
hash=`mkpasswd --method=md5 --salt=$salt $password`
echo $hash
}
# Функция нужна, чтобы sed корректно отработал закрывающие слэши и знаки $
escape_hash() {
echo $1 | sed -e 's/\//\\\//g' -e 's/\$/\\\$/g'
}
guestfish <<EOF
add-drive debian_guest.img
run
mount-vfs rw ext3 /dev/vda1 /
download /etc/shadow $tempfile
! sed 's/^root:[^:]\+:/root:$hash_esc:/' $tempfile > $tempfile.new
upload $tempfile.new $shadow
EOF
Как вы наверняка заметили, мы использовали внешнюю команду внутри скрипта, в которой мы заменили содержимое первой секции на полученный в скрипте хэш. Для этого используется внешний оператор "! ": он очень удобен, когда нам нужно сделать какие-то небольшие операции, не прерывая процесс работы с guestfish (поскольку на запуск guestfish всё таки требуется некоторое время).
Подготовка мастер-образа
Поскольку образы требуется периодически обновлять (в случае выхода важных обновлений или исправления ошибки в образе), нам следует подготовить мастер-образы, в которых мы будет производить необходимые манипуляции. Для разворачивания мы будем готовить эти образы при помощи отдельного скрипта.
Что нам нужно убрать в нашем образе:
- Очистить логи
- Удалить следы пребывания в системе
- Удалить скачанные пакеты (актуально для Debian и Ubuntu, только они мусорят)
- Удалить файл с настройками сетевой карты
- Удалить ключи.
После этого нам нужно будет уменьшить размер файловой системы, уменьшить раздел и отрезать от образа лишнее.
Приложу небольшой участок кода, который выполняет первую часть необходимого действия:
guestfish <<EOF
add-drive debian_guest.img
run
mount-vfs rw ext3 /dev/vda1 /
upload /home/username/debian_5_etc_init_ssh_fixed /etc/init.d/ssh
-glob rm /etc/ssh/ssh_host_*
-glob rm /etc/udev/rules.d/70-persistent-net.rules
-glob rm /root/*
-glob rm /root/.*
-glob rm /var/log/*
-glob rm /var/cache/apt/archives/*deb
EOF
Флаг "-" перед командой означает, что мы не должны выходить, если какая-то из команд вернёт -1. Это сделано специально, чтобы отсутствие каких-либо файлов не прерывало выполнение остальных команд; таким образом, кастомизация данного скрипта для различных дистрибутивов становится не нужной, хотя она и возможна.
А теперь приступим к уменьшению образа:
$ guestfish <<EOF
add-drive add-drive ${images}/${os}_${version}_${arch}.img
run
e2fsck-f /dev/vda1
resize2fs-M /dev/vda1
tune2fs /dev/vda1 | grep "Block count:" | sed -e 's/Block\ count:\ //g' -e 's/$/*4+2144/g' | bc > /tmp/block_count
EOF
$ foo=`cat /tmp/block_count`
$ guestfish <<EOF
allocate debian_guest_minimal.img ${foo}k
EOF
Цифра 2144 — это размер загрузчика и таблицы разделов.
Вкратце суть проделанного в следующем: мы ужимаем файловую систему до минимального размера, вычисляем, сколько она стала занимать (минимальное количество блоков), и умножаем их на 4, поскольку размер блока 4 кбайта, после чего создаём образ полученной величины.
После этого нам необходимо будет воспользоваться утилитой virt-resize из комплекта утилит libguestfs, чтобы перенести получившуюся файловую систему в новый, меньший образ.
$ virt-resize --shrink /dev/vda1 debian_guestl.img debian_guest_minimal.img
Следует сразу обговорить ограничения данного метода: это применимо только для файловых систем ext2-4, поскольку resize2fs работает только с ними. Для чего-то нестандартного можно легко допилить нужный функционал (правда, как я уже упоминал ранее, libguestfs очень сложно собрать). Для образца можно посмотреть мой патч для реализации resize2fs-M.
К сожалению с FreeBSD всё сильно сложнее, и пока нет никаких вариантов решения проблемы с ней кроме добавления в конфиг виртуальной машины ещё одного диска и его монтирования.
Теперь же мы должны, по желанию, конечно, упаковать получившийся образ при помощи xz (это долго, но результат стоит того):
$ xz -9 debian_guest.img
$ ls -lsha debian_guest.img.xz
107M -rw-r--r-- 1 username username 107M Dec 21 00:00 debian_guest.img.xz
Разворачивание образа
Итак, образ виртуальной машины мы получили, но образы — это не готовые рабочие системы. Чтобы получить рабочую систему, нам нужно произвести несколько операций:
- Аллоцировать образ на диск
- Скопировать загрузчик и таблицу разделов
- Перенести информацию из шаблона в образ виртуальной машины
- Расширить файловую систему
- Сменить пароль root
- прописать сетевые настройки
Для Linux всё элементарно: в составе libguestfs есть замечательная утилита, написанная на OCaml — virt-resize, пункты с 2 по 4 выполняются ею без проблем.
По ряду причин на '''guestfish''' реализовать изменение размера диска невозможно (копирование mbr в guestfish невозможно), посему нужно использовать более функциональные средства.
$ guestfish <<EOF
allocate debian_guest_clone.img 10G
EOF
$ virt-resize --expand /dev/vda1 debian_guest.img debian_guest_clone.img
Собственно, это все, что минимально требуется знать для осуществления клонирования образов виртуальных машин.
Следующая статья расскажет про лимитирование ресурсов виртуальных машин.