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

    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


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

    Следующая статья расскажет про лимитирование ресурсов виртуальных машин.
    Поделиться публикацией
    Комментарии 23
      +2
      Спасибо за статьи. Ждем продолжения :)
        +3
        Всегда пожалуйста.
        0
        а зачем эта страшная либа, если есть kpartx?
          +1
          Для kpartx нужны рутовые права + у kpartx нет развесистого API для работы с кучей языков программирования. И по большому счёту libguestfs предоставляет возможность использовать её в том числе и для массовых манипуляций с конфигами виртуалок.

          + В составе libguestfs есть демон guestfsd, который можно запускать внутри системы и через канал в libvirt общаться с живой операционкой.
            +1
            это всё понятно, но извращения со сборкой сводят на нет все преимущества библиотеки.
              +1
              Какие извращения? Берёте с сайта библиотеку, ставите и получаете результат.
              Извращения со сборкой начинаются, когда нужно добавить какой-то функционал, да и то проблемы в основном при сборке из git. Из уже готовых архивов всё намного проще.
              0
              кстати по поводу рутовых прав. а как вы склонируете систему, у которой диск это LVM раздел без рутовых прав? они все равно нужны.
              да и API не сильно то и нужно, если всё равно основной код будет на шеле.
                +1
                Я с lvm и libguestfs особо не работал, но вообще действительно, для изменения раздела lvm нужны рутовые права.
                Я для частных нужд советую использовать именно LVM, поскольку тогда не будет лишней прослойки в виде файловой системы и администратор может более гибко распределять ресурсы диска, а для нужд хостинга разумно использовать raw или qcow2 образы.

                На bash реализовать клонирование невозможно, потому что при клонировании нужно копировать загрузочную запись. Нужно обязательно использовать более мощные языки.
                + API необходимо когда вы делаете некоторую более серьёзную работу с виртуалками, нежели обычное клонирование.
                  +1
                  хехе
                  я бы на вашем месте не говорил слово невозможно.
                  извените а зачем копировать загрузочную запись? dd не устраивает?
                  и не обязательно использовать более мощные языки, это если вы плохо знаете простые програмки UNIX — тогда да.
                  что вы подразумеваете под понятием более серьёзную работу с виртуалками?
                    +1
                    Ок.
                    Представим ситуацию, у нас есть N серверов, к которым у нас есть доступ через некоторое API, допустим SOAP, через который мы управляем конфигурацией хост машин, занимаемся деплойментом виртуальных машин, изменением их конфигураций, управлением этимим виртуалками. + Ко всему этому сбор статистики, нотификации и прочее.
                    И в самом низком уровне у вас лежит набор bash скриптов, которые вы дёргаете через exec с какими-либо параметрами?
                    Как то это не очень хорошо.

                    С известными извращениями на bash я даже делал генератор статического сайта(и один раз делал не очень статического), но зачем если всё тоже самое можно сделать намного проще, на тех языках, которые предназначены для этого.
                      +1
                      вот видите, Вы уже говорите что это не «не возможно», а «не очень хорошо».
                      Кстати по поводу такого API советую посмотреть в сторону OpenStack.
                        +1
                        Про «невозможно» я говорил касаясь конкретно libguestfs, там можно скопировать какие то участки с начала диска, но конкретно нужную часть вырезать у диска без примеренения нормальных языков, как раз невозможно.

                        «Не очень хорошо» касаясь того, что для собственных нужд bash очень хорошо подходит, а когда нужно сделать что то целостное, то лучше перенести всё это в какую-то одну сущность.

                        Про OpenStack знаю, спасибо, но в ближайшее время мы API открывать не будем. Позже — вполне возможно, но не прямо сейчас.
                          +1
                          как это невозможно? там что вычисления не используют "+", "-", "/" и "*"?
                          в принципе на баше можно написать всё, было бы желание и опыт. и это как раз одна сущность. быстро, дешево и сердито.

                          а зря, в том-то и проблема что все хостеры делают свои велосипеды, почему бы не взять то что уже работает? кстати OpenStack использует API совместимое с RackSpace и Amazon EC2, что очень удобно как для тех кто не работал с ним и в будущем все таки прийдется и для тех кто уже работал и не надо изучать новое.
                            +1
                            На bash можно написать почти всё.

                            Во многих случаях велосипеды вполне обоснованы. Хотя бы тем что нужно встраивать что то новое в уже существующую инфраструктуру. Если понадобится, можно вполне мигрировать на другую инфраструктуру, но на это нужно время, деньги и люди.
                              0
                              я Вас достаю не потому что Вы написали что-то не нужное или неинтересное, но потому что Вы слишком категоричны в высказываниях.
                                0
                                Бывает, вот что слишком крепкий кофе делает с людьми. Я обычно мягкий и шелковистый, пока не побрился налысо :)
                                  0
                                  попробуйте зеленый чай, там кофеина больше, а вреда для организма меньше ;)
                                    +1
                                    Я кофе обычно как некоторый деликатес использую. По праздникам, так сказать.
                                    А зелёный чай у меня обычно настолько крепкий, что лучше уж кофе :)
              0
              Кстати про kpartx, статья появилась про изменение размера образов виртуальных машин.
              +1
              Хотелось бы аналогичную статью для win виртуальных машин
                +1
                Когда будем предоставлять услуги Windows виртуалок, тогда напишу. Но в принципе в msdn должно быть написано что нужно менять, а вообще windows как мне кажется лучше ставить с установочного диска, а изменение размера вполне возможно для ntfs из linux.
              • НЛО прилетело и опубликовало эту надпись здесь
                  0
                  Окей, ваш способ тоже работает. Но в условии когда у нас есть Nцать образов, с разными операционными системами, и тут у нас приехало на все(или на часть) обновление безопасности. Запустили все базовые виртуалки, к которым подключены образы, обновили всё, запустили подготовленный скрипт и идём пить чай.

                  И когда у нас много образов, экономия места внезапно становится очень существенной.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое