Pull to refresh

Работа с виртуальными машинами KVM. Клонирование виртуальных машин

Reading time 9 min
Views 18K
Clone

Продолжаем серию статей о виртуализации на базе 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 всё таки требуется некоторое время).

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



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

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

  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


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

Следующая статья расскажет про лимитирование ресурсов виртуальных машин.
Tags:
Hubs:
+24
Comments 23
Comments Comments 23

Articles