Автоматизация работы с VirtualBox: Резервное копирование

VirtualBox — достаточно универсальная, простая и бесплатная платформа виртуализации, что и обеспечило ей широкую популярность, однако, часто не хватает её функциональности для того, чтоб решить ту или иную задачу. Этот недостаток можно частично компенсировать разработав собственные инструменты применяя SDK. Т.к. информации по этой теме в интернете немного в этой статье хочу рассказать о том, как реализовать не требующее остановки резервное копирование виртуальной машины с использованием Python API для VirtualBox. Люди обладающие серьёзным опытом работы с VirtualBox вряд ли найдут в здесь что-то новое, но тем кто недавно работает с VirtualBox и хоть немного умеет програмировать думаю будет интересно.


Задача

Есть сервер с установленным VirtualBox и несколькими работающими виртуальными машинами(далее ВМ). Требуется периодически копировать выбранные виртуальные машины на другой сервер, при этом должны быть соблюдены следующие условия:
  • Образ жесткого диска ВМ должен быть в одном файле.
  • Необходимо так же копировать текущее состояние (содержимое RAM) ВМ, файл конфигурации и логи.
  • Останавливать работу ВМ при этом нельзя.
  • Если что-то пошло не так на каком-то этапе — создать сообщение об ошибке и перейти к копированию следующей ВМ в списке.

Для этого напишем скрипт на Python который будет в качестве параметров принимать разделенные пробелом имена ВМ, которые необходимо подготовить к копированию, и по мере своей работы выводить в стандартный поток вывода STDOUT абсолютные пути к файлам, готовым к копированию, а в поток ошибок SDTERR диагностические сообщения о всех неудачах, которые имели место быть. Дальнейшая судьба полученного списка файлов зависит от конкретной задачи, об этом напишу ниже.
Прежде чем копировать файлы с образами дисков ВМ, нужно привести их в актуальное состояние, а так же сделать так, чтоб VirtualBox гарантированно ничего в них не писал в момент копирования. Для этого нужно поработать со снэпшотами. Остановимся на этом подробнее.

О снэпшотах

Для того чтоб сохранять и восстанавливать состояние ВМ используются так называемые снимки состояния системы (Snapshots). Работают они следующим образом: Когда еще не сделано ни одного снимка, образ виртуального диска ВМ хранится в одном большом файле с разрешением vdi и каким-то понятным человеку именем (по умолчанию это название виртуальной машины). ВМ по мере своей работы читает и пишет в этот файл. Когда создается снимок системы, VirtualBox создает два файла в расширениями vdi и sav и именами из букв цифр и чёрточек. Что-то вроде {4a34e84c-aad0-4486-85d2-2d7fbb472d70}.sav. {4a34e84c-aad0-4486-85d2-2d7fbb472d70} – это id снимка, он нам пригодится позже. Файл в разрешением sav – это сохраненное состояние системы, по сути содержимое оперативки. А в файл с разрешением vdi с этого момента начинают писаться все изменения, которые ВМ делает со своим диском. Это означает, что если у нас есть хотя бы один снэпшот, то файл с образом диска используется только для чтения, и можно его смело копировать.
Когда снимок удаляется – vdi-файл снимка сливается (в смысле merge) с vdi-файлом своего предка, если снимок было всего один, то непосредственно с образом диска. Надеюсь, понятно объяснил. Вообще, снимки можно выстраивать в длинные цепочки и в деревья, но пока нам этого делать не понадобится.

Алгоритм

Чтоб в итоге получить актуальную копию ВМ и ничего не поломать, поступим следующим образом:

Ищем нужные нам машины среди зарегистрированных на сервере ВМ, 
Если каких-то не находим:
        выводим в STDERR сообщения с названиями не найденных машин
        переходим к обработке найденных, если таковые имеются.

Далее для каждой из найденных ВМ:
        Проверяем есть ли снимки этой ВМ. 
                Если снимки есть, и они явно небыли оставлены ранее нашим же скриптом:
                        ругаемся в STDERR и больше эту машину не трогаем. 
                Если снимок есть, но он был создан при прошлом резервном копировании:
                        удаляем его, т.е. объединяем его с образом диска

        Создаём новый снимок, чтоб острановить запись в файл-образ диска и получить дамп памяти ВМ.
    
        Выводим в STDOUT абсолютные пути к
                только что созданному sav-файлу
                образу диска
                файлу конфигурации ВМ
                (опционально) логам.



Перейдем к практике

Для начала нам потребуется машина с установленным VirtualBox и Python. Я использовал Ubuntu 10.10, VirtualBox 4.0.4 и Python 2.6, так же тестировал на Windows 7 c Python 2.7. [C незначительными изменениями] всё должно работать и на других системах.
Чтобы API стало доступно нужно скачать и установить SDK с официального сайта www.virtualbox.org/wiki/Downloads. Для того, чтоб установить его на Ubuntu понадобилось прописать путь к каталогу с VirtualBox и запустить инсталятор. У меня это выглядело вот так:

# export VBOX_INSTALL_PATH=/usr/lib/virtualbox
# python vboxapisetup.py install


Под Windows устанавливаем аналогично

C:\vbpx\sdk\installer> c:\python27\python.exe vboxapisetup.py install

но при первом запуске скрипта на Windows 7 получил ошибку

ImportError: No module named win32com

Установил требуемый модуль — всё заработало. Скачать для своей версии его можно отсюда:
sourceforge.net/projects/pywin32/files/pywin32/Build216

В архиве с SDK лежит папка docs, а в ней в разных форматах документация по API. Это самый главный источник информации, поэтому советую пробежаться по ней глазами хотя бы для того, чтоб примерно представлять все наши возможности.

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

Disclaimer: возможно код не соответствует всем питоновым стилистическим правилам, т.к. обычно пишу на других ЯП. Если что-то режет глаз, пишите — поправлю.

  1. #!/usr/bin/python
  2. # -*- coding: utf8 -*-
  3.  
  4. import sys, datetime, time
  5. from vboxapi import VirtualBoxManager
  6.  
  7. # Название снэпшота. С помощью него будем отличать наши автоматически создааные снэпшоты от всех остальных, дабы не удалить чего лишнего
  8. SNAPSHOT_NAME = "autobackup"
  9. # Максимальное время ожидания завершения асинхронной операции. см. waitFor
  10. MAX_TIMEOUT = 300
  11. # Период проверки условия при ожидании завершения асинхронной операции. см. waitFor
  12. TIME_QUANT = 5
  13.  
  14. """
  15. Функция waitFor(cond, message)
  16. Используется, чтоб дожидаться завершения асинхронных методы из API.
  17. В нашем случае это takeSnapshot и deleteSnapshot.
  18. Чтоб узнать, что метод отработал проверяем условие cond в течении MAX_TIMEOUT секунд
  19. с интервалом TIME_QUANT. Условие представляет собой функцию без параметров
  20. (в нашем случае лямбда-выражение) возвращающую значение логического типа.
  21. """
  22. def waitFor(cond, message):
  23.     timeout = 0
  24.     while (not cond()):
  25.         #print("timeout = " + str(timeout))
  26.         if timeout >= MAX_TIMEOUT:
  27.             raise Exception(message) # TODO testing
  28.         timeout = timeout + TIME_QUANT
  29.         time.sleep(TIME_QUANT)
  30.  
  31. def prepareToCopy(vm):
  32.         if vm.snapshotCount > 1:
  33.             raise Exception("Найдено больше одного снэпшета.")
  34.         if vm.snapshotCount == 1:
  35.             curSnapshot = vm.currentSnapshot
  36.             if curSnapshot.name == SNAPSHOT_NAME:
  37.                 session = mgr.getSessionObject(vbox)
  38.                 vm.lockMachine(session, vbm.constants.LockType_Shared )
  39.                 session.console.deleteSnapshot(curSnapshot.id);
  40.             else:
  41.                 raise Exception("Название текущего снэпшота(\"" + curSnapshot.name.encode("utf-8") + "\") отличается от \"" + SNAPSHOT_NAME + "\", т.е. он не был создан автоматически. Что с ним делать не знаю.")
  42.     # Дождемся удаления снэпшета
  43.     waitFor(lambda: vm.snapshotCount == 0, "Не удалось дождаться завершения операции удаления снимка ВМ")
  44.  
  45.     # Сейчас образы дисков смёрджены в один файл.
  46.     # Прочитаем абсолютные пути ко всем дискам ВМ в массив vdiFiles
  47.     vdiFiles = []
  48.     # Примем, что у весех ВМ есть один и только один виртуальный IDE контроллер
  49.     storageController = vm.getStorageControllerByInstance(0)
  50.     for port in range(storageController.portCount):
  51.         for devPerPort in range(storageController.maxDevicesPerPortCount):
  52.             try:           
  53.                 dev = vm.getMedium(storageController.name,port, devPerPort)
  54.                 if dev != None:
  55.                     vdiFiles.append(dev.location)
  56.                     # Можно сразу выводить в STDOUT пути к образам дисков,
  57.                     # но тогда есть опасность, что образ начнет копироваться
  58.                     # раньше чем будет сделан снимок
  59.             except Exception, e:
  60.                 # Тут мы можем получить xpcom.Exception: 0x80bb0001
  61.                 # (No storage device attached to device slot 1 on port 0
  62.                 # of controller 'IDE контроллер').
  63.                 # Просто проигнорируем его.
  64.                 pass
  65.  
  66.         # Создаем снимок, чтоб сохранить моментальное состояние ВМ
  67.         session = mgr.getSessionObject(vbox)
  68.         vm.lockMachine(session, vbm.constants.LockType_Shared)
  69.         session.console.takeSnapshot(SNAPSHOT_NAME, "Automatically generated snapshot of virtual machines at " + str(datetime.datetime.now()) + ". It will be deleted when the next backup");
  70.  
  71.         # Дождемся создания снэпшета
  72.         waitFor(lambda: vm.snapshotCount == 1, "Не удалось дождаться завершения операции создания снимка ВМ")
  73.         # Все подготовительные действия выполнены,
  74.         # осталось вывести список файлов
  75.         print(vm.snapshotFolder+"/{"+vm.currentSnapshot.id+"}.sav")
  76.         print(vm.settingsFilePath)
  77.         for vdiFile in vdiFiles:
  78.             print(vdiFile);
  79.         # Если понадобятся еще и логи ВМ, то они тут:
  80.         #print(vm.logFolder)
  81.  
  82.          
  83.  
  84. if __name__ == "__main__":
  85.     if len(sys.argv) < 2:
  86.         sys.stderr.write("Файл " + sys.argv[0] + " запущен без параметров.\n"+
  87.           "Передайте в качестве параметра список имен виртуальных машин"+
  88.           " разделенных пробелами," +
  89.           "которые нужно подготовить к резервному копированию. "+
  90.           "Список не может быть пустым.\n")
  91.         exit(-1)
  92.  
  93.     # Создаем объекты для управления VirtualBox'ом
  94.     vbm = VirtualBoxManager(None, None)
  95.     vbox = vbm.vbox
  96.     mgr = vbm.mgr
  97.  
  98.      
  99.     # Ищем нужные нам машины среди зарегистрированных
  100.     allMachines = vbm.getArray(vbox, 'machines')
  101.     machines = []
  102.     for mname in sys.argv[1:]:
  103.         for m in allMachines:
  104.             if m.name==mname:
  105.                 machines.append(m)
  106.                 break
  107.         else:      
  108.             sys.stderr.write("На серевере не зарегистрирована виртуальная машина \""+mname+"\"\n")
  109.  
  110.     # Последовательно готовим машины к копировнию с обработкой ошибок.
  111.     for m in machines:
  112.         try:
  113.             prepareToCopy(m)
  114.         except Exception, e:
  115.             sys.stderr.write(("Ошибка при обработке ВМ \"" +
  116.                 m.name.encode("utf-8") + "\": " + str(e) + " \n"))
  117.              
  118.         
Подсветка исходного кода

Что дальше?

Итак, у нас есть список файлов из которых, в случае чего, можно быстро восстановить ВМ, дальше нам нужно позаботиться о том, чтоб они были скопированы на какое-то надежное хранилище. Для этого можно воспользоваться утилитой scp, ftp-клиентом или скопировать на примонтированный smb ресурс. В моём случае нужно было свести к минимуму количество сетевого трафика, поэтому выбор пал на Rsync. Для тех кто не в курсе кратко расскажу что это такое:
rsync — (англ. Remote Synchronization) программа для синхронизации файлов между удаленными серверами. Существую реализации демона и клиента как под UNIX-подобные ОС, так и под Windows. Особенностью rsync является то, что она не передает все данные, а делит файлы на куски, вычисляет контрольные суммы и передает только изменившиеся части. Дополнительный выигрыш в минимизации трафика достигается за счет использования сжатия.
Для того чтобы собирать резервные копии со всех физических серверов на один
бэкапный установим на него rsyncd и напишим такой вот скрипт:
  1. #!/bin/bash
  2.   
  3. # Параметры сервера на который сливаем бэкап
  4. RSYNC_HOST=backup.example.com
  5. RSYNC_USER=vm-backup
  6. RSYNC_PASSWORD=SomePassword
  7.   
  8. # Куда пишем лог
  9. REPORT_LOG=/var/log/vm-backup/`date +%s`.log
  10.   
  11. # Имя пользователя под которым подключаемся
  12. # к серверу с установленным VirtualBox'ом
  13. USER=backup
  14.   
  15. # Команда которая подготовит ВМ к копированию
  16. # и вернет список файлов. Параметром передадим ей имя ВМ
  17. # т.е. скрипт приведенный выше
  18. PREPARE_COMAND=prepare_to_sync.py
  19.   
  20. # Сервер с установленным VirtualBox'ом
  21. HOST=$1
  22. # Имя ВМ
  23. VM=$2
  24. echo "Отчет о резервном копировании виртуальной машины $2 с хоста $1" > $REPORT_LOG
  25. echo "Время начала: `date`" >> $REPORT_LOG
  26. ssh $USER@$HOST "
  27.     export RSYNC_PASSWORD=$RSYNC_PASSWORD
  28.     $PREPARE_COMAND $VM |
  29.      while read file_name; do
  30.     echo \"Синхронизирую файл \$file_name ...\"
  31.         rsync -av --delete \"\$file_name\" $RSYNC_USER@$RSYNC_HOST::vm/$VM/
  32.     done;
  33. " 2>>$REPORT_LOG 1>>$REPORT_LOG
  34. # Чтоб оставить только сообщения об ошибках уберите "2>>$REPORT_LOG"
  35. echo "Время завершения резервного копирования: `date`" >> $REPORT_LOG
Подсветка исходного кода


Первым параметром он принимает имя физического сервера, а вторым имя виртуальной машины. Если вместо rsync используем что-то другое, то заменяем строчку rsync -av --delete \"\$file_name\" $RSYNC_USER@$RSYNC_HOST::vm/$VM/ на что-то своё, к примеру, scp. И для полного счастья напишем скрипт который будет собирать все ВМ со одного сервера и делать ротацию резервных копий. Его и будем запускать по cron'у
  1. #!/bin/bash
  2. PATH_TO_VMs="/путь/к каталогу/с копиями ВМ/"
  3. PATH_TO_VMs_ARCHIVES="/путь/к каталогу/с архивными копиями ВМ/"
  4. # Имена виртуальных машин
  5. VMs="webServer asterisk mail dns testUbuntu testWin7 testWinXP"
  6.    
  7. for vm in $VMs; do
  8.     cp $PATH_TO_VMs$vm $PATH_TO_VMs_ARCHIVES$mv`date +-%Y_%m_%d-%H-%M` -r
  9.     ./backup.sh $HOST $vm;
  10. done;
Подсветка исходного кода

На этом всё.

Полезные ссылки:

www.virtualbox.org/wiki/Downloads — VirtuaBox, SDK и оф. документация:
www.xakep.ru/magazine/xa/132/040/1.asp — Статья статья в XAKEP'е о VirtualBox
srackham.wordpress.com/2010/06/17/merging-and-compacting-virtualbox-snapshots — Merging and Compacting VirtualBox Snapshots
gui-at.blogspot.com/2009/09/how-to-clone-virtualbox-virtual-machine.html — How to clone VirtualBox virtual machine
www.itnotes.org.ua/administration/cwRsync/synchronization_disks_on_windows_servers.html — Хорошая статья про Rsync
Tags:
virtualbox, virtualization, python, backup, automation

You can't comment this post because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author's username will be hidden by an alias.