Клонирование виртуальных машин

[TOC]

Clone

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

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

Для получения образа виртуальной машины в минимальной системе в ней достаточно поменять всего пару файлов чтобы получить нормально работающую систему, но в случае Debian появляются небольшие сложности.

Для создания новой виртуальной машины на основе имеющейся системы нужно внести следующие изменения:

Большой находкой для меня оказалась библиотека 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

Процитирую из вики порядок использования полей:

Рассмотрим структуру /etc/shadow

root:$1$APv1HQOB$HJQhYFq9JSnhusQ.1Ql10.:14977:0:99999:7:::

Опять же из вики:

Нам нужно изменить конкретно второе поле (хэш пароля). Его можно разбить на три части:

Хэш генерируется командой:

$ mkpasswd --method=md5 --salt="APv1HQOB" "$password"
$1$APv1HQOB$HJQhYFq9JSnhusQ.1Ql10.

Его нам и нужно подставить в файл /etc/shadow.

Я написал небольшой скрипт, который будет генерировать случайный пароль и соль длиной 8 символов, выводить его, генерировать хэш и подставлять его в нужный файл:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/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 всё таки требуется некоторое время).

Подготовка мастер-образа

Поскольку образы требуется периодически обновлять (в случае выхода важных обновлений или исправления ошибки в образе), нам следует подготовить мастер-образы, в которых мы будет производить необходимые манипуляции. Для разворачивания мы будем готовить эти образы при помощи отдельного скрипта.

Что нам нужно убрать в нашем образе:

  1. Очистить логи
  2. Удалить следы пребывания в системе
  3. Удалить скачанные пакеты (актуально для Debian и Ubuntu, только они мусорят)
  4. Удалить файл с настройками сетевой карты
  5. Удалить ключи.

После этого нам нужно будет уменьшить размер файловой системы, уменьшить раздел и отрезать от образа лишнее.

Приложу небольшой участок кода, который выполняет первую часть необходимого действия:

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

Разворачивание образа

Итак, образ виртуальной машины мы получили, но образы — это не готовые рабочие системы. Чтобы получить рабочую систему, нам нужно произвести несколько операций:

  1. Аллоцировать образ на диск
  2. Скопировать загрузчик и таблицу разделов
  3. Перенести информацию из шаблона в образ виртуальной машины
  4. Расширить файловую систему
  5. Сменить пароль root
  6. Прописать сетевые настройки

Для 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

Собственно, это все, что минимально требуется знать для осуществления клонирования образов виртуальных машин.

Следующая статья расскажет про лимитирование ресурсов виртуальных машин.

Если у вас остались вопросы, напишите мне письмо.