Правим QEMU железным кулаком
Туториал о том, как на Go программно управлять виртуальными машинами QEMU через libvirt — пишем небольшую утилиту с человеческим выводом в JSON вместо virsh.
Виртуализация, на мой взгляд, всё ещё остаётся одной из самых важных технологий в администрировании ЦОД. Да, конечно «все» будут рассказывать, что контейнеры намного более удобные, и всё надо запихивать в Кубер, и всё такое… Но после гигантского нагромождения никому не нужных конфигов, в какой-то момент ты начинаешь понимать, что зашёл слишком далеко.
И действительно. Мы пишем ПО для обслуживания целого ЦОДа. Изначально всё должно было быть контейнером, и всё должно было распространяться через CI/CD, но когда дело доходит до дела, ты начинаешь понимать, что нет ничего проще установленного линукса, на котором напрямую запускается твоя утилита, написанная на golang.
Но есть одна проблема. Виртуальными машинами не так легко управлять, как это можно делать с контейнерами. Ок, мы сами с усами, можем и вручную написать кое-чего.
Под катом давайте окунёмся в мир работы с QEMU и подёргаем сам эмулятор. Конечным результатом должна быть клонированная через golang Debian Linux.
Вступление
Итак, я думаю, все понимают разницу между виртуализацией и контейнерами. Если нет, то рекомендую ознакомиться. Материалов по этой теме — просто несчётное количество.
Основной плюс контейнеров в том, что ими легко управлять, и намного проще делить ресурсы между программами. А вот виртуальные машины не настолько удобные. Если у тебя есть что-то, что жрёт 2 гига памяти, то жрать оно эти 2 гига будет.
ТЗ
Что мы делаем здесь? Мы будем упрощать жизнь в управлении виртуальными машинами и напишем небольшую утилиту, которая позволит напрямую работать с QEMU через консоль, создавая эти машины на ходу. Исходники утилиты будут доступны в конце статьи.
Ещё раз, повторюсь — мы напишем простую утилиту, которая позволит создавать, стартовать, удалять и запускать виртуальные машины на QEMU. Цель написания этой утилиты — показать, как вы можете с помощью языка Golang программно создавать виртуальные машины.
Казалось бы, у нас в руках есть virsh, но, как показала практика, он не такой удобный и полезный, как мне бы того хотелось. Virsh умеет управлять QEMU из консоли, но по факту — это просто текстовый интерфейс, который не очень хорошо работает в скриптах и внешних программах. Для того чтобы мне было удобно управлять виртуальными машинами, мне надо будет написать свою утилиту, которая просто будет принимать параметры на вход, выполнять команды и завершаться.
Немного матчасти
Напомним, что мы смотрим на один из компонентов целого набора систем для управления виртуализацией.
У нас есть сам гипервизор — подсистема в ядре вашей ОС, которая позволяет создавать виртуальное пространство в процессоре и памяти физического компьютера. В данном случае мы будем использовать гипервизор KVM.
Далее — сам эмулятор. Это обёртка, которая превращает гипервизор в то, что выглядит как компьютер. К этому компьютеру добавляются виртуальные порты, устройства, системы ввода и вывода, и в итоге у нас появляется то, что выглядит как компьютер, нарисованный на экране. QEMU — это наш эмулятор. Он умеет работать поверх KVM, HVF или уже богом забытого проприетарного кода.
Система управления всем этим добром. Для того чтобы отправлять команды туда и обратно, вам нужна библиотека, которая эти команды может дёргать. В данном случае мы будем использовать libvirt.
Сам UI для управления виртуальными машинами. Мы можем воспользоваться консольным virsh или более гуманным virtual machine manager, который идёт в комплекте с QEMU. Но в данной статье мы будем переписывать именно этот компонент. Поскольку с ним не так-то просто работать, как хотелось бы.
Приступим
Писать будем на golang, потому что на нём только ленивый не пишет. Хотя работать с libvirt можно и на других языках, выбирайте что хотите.
Самый неприятный момент в разработке утилит на libvirt — это зубодробительная документация. К сожалению, ребята руководствовались принципом, что если это писалось сложно, то и пониматься должно сложно.
Для наших целей давайте возьмём обёртку на golang вот отсюда.
Ребята проделали хорошую работу, написав скрипт, который автоматически оборачивает все вызовы к libvirt в готовые для использования в golang библиотеки. При этом никто не позаботился о документации, и даже примеры в их репозитории компилируются с предупреждениями о том, что код устарел.
Что же, давайте разбираться
Для начала — давайте серьёзно упростим подключение к нашему эмулятору и уберём всё сложное и ненужное. Это — пример того, как написать код для подключения к libvirt без каких-либо ворнингов об устаревании кода.
var v *libvirt.Libvirt
func virtinit() {
v = libvirt.NewWithDialer(dialers.NewLocal(dialers.WithLocalTimeout(time.Second)))
if err := v.Connect(); err != nil {
log.Fatalf("failed to connect: %v", err)
}
}
Эта функция подключит вас к QEMU, запущенному на локальной машине. С использованием v вы сможете вызывать любые нужные функции libvirt для управления вашими машинами.
Вопрос только в том, какие функции вам надо запускать?
К сожалению, вот тут вас ждёт подвох. По-хорошему единственная вменяемая документация к libvirt живёт на сайте самого libvirt.
Если вы пройдёте по этому адресу и посмотрите на доки, то вы увидите, что они всеобъемлющи и написаны на C++. Что не удивительно. Наша обёртка на Golang позволяет запускать все эти команды без необходимости перевода параметров в непонятные структуры и тому подобное. Пользоваться этим удобно.
Проблема заключается в том, что документация написана людьми, которые писали libvirt, и для того, чтобы найти нужную функцию, вам нужно будет быть о-о-очень смышлёным. В основном, потому что названия переменных и функций не соответствуют ничему, к чему вы могли бы привыкнуть. Вы — либо разработчик libvirt, либо вам придётся страдать, перечитывая ВСЮ документацию о том, как и что делать с машиной, для того чтобы её перезагрузить.
Иногда знание команды virsh может помочь найти нужную функцию в libvirt, но такое бывает не всегда.
Давайте начнём с того, что перезагрузим машину
// VirtualMachineSoftReboot reboots a machine gracefully, as chosen by hypervisor.
func VirtualMachineSoftReboot(id string) {
d, err := v.DomainLookupByName(id)
herr(err)
err = v.DomainReboot(d, libvirt.DomainRebootDefault)
herr(err)
hok(fmt.Sprintf("%v was soft-rebooted successfully", id))
}
Код достаточно прост. Нам нужно сначала найти указатель на машину, с которой мы работаем, потом вызвать int virDomainReboot(virDomainPtr domain, unsigned int flags). Вызываем мы это на экземпляре нашего соединения, которое мы установили в самом начале.
В имплементации, в golang мы получаем указатель на виртуальную машину по её имени, и вызываем функцию virDomainReboot, которая в golang называется просто DomainReboot.
Плюс, как я говорил, у libvirt есть свой «язык». Например, domain — это то, что в данной статье я буду называть «виртуальной машиной». Возможно, это не будет самым хорошим переводом с точки зрения номенклатуры libvirt, но вот для наших целей подходит как нельзя лучше.
Давайте быстро посмотрим на функции обработки ошибок.
func herr(e error) {
if e != nil {
fmt.Printf(`{"error":"%v"}`, strings.ReplaceAll(e.Error(), "\"", ""))
os.Exit(1)
}
}
func hok(message string) {
fmt.Printf(`{"ok":"%v"}`, strings.ReplaceAll(message, "\"", ""))
os.Exit(0)
}
func hret(i interface{}) {
ret, err := json.Marshal(i)
herr(err)
fmt.Print(string(ret))
os.Exit(0)
}
Здесь всё просто, мы выходим из программы через os.Exit и возвращаем код ошибки. С кодами заморачиваться не будем. 1 — есть ошибка, 0 — нет ошибки.
Как вы видите, сам вывод программы отформатирован в JSON. Правильно, потому что мне надо будет дёргать этот код через web, и в конце концов вывод этой утилиты будет скормлен большому брату, который управляет серверами.
Посему я решил, что json-formatted вывод будет наиболее удобным в данном случае.
Итак, проверяем нашу программу, запускаем её на локальном компьютере, всё работает, виртуальная машина перезагружается! Отлично!
Почитав документации, вы быстро разберётесь, как выполнять такие простые вещи, как выключение, включение, перезагрузка и остановка виртуальной машины.
Поехали дальше.
Создадим виртуальную машину
// VirtualMachineCreate creates a new VM from an xml template file
func VirtualMachineCreate(xmlTemplate string) {
xml, err := ioutil.ReadFile(xmlTemplate)
herr(err)
d, err := v.DomainDefineXML(string(xml))
herr(err)
hret(d)
}
Тут всё просто. Libvirt и QEMU описывают виртуальные машины в XML-формате. Создав такой файл, вы описываете все настройки необходимой виртуальной машины и после этого можете клепать их направо и налево.
Для того чтобы получить такой файл, я рекомендую воспользоваться virsh dumpxml VM1 > VM1.xml.
Этот код позволит вам записать все данные о текущей виртуальной машине в xml-файл. Прочитав файл, вы запросто разберётесь в том, что и как в нём надо менять. Единственное что, прошу убедиться, что вы изменили ID и GUID виртуальной машины.
А теперь давайте перейдём к тому, почему я взялся за написание своей утилиты. Virsh не позволяет по-роботски получить данные о виртуальных машинах. Для того чтобы узнать количество процессоров, памяти и того подобных вещей, мне надо было парсить вывод командной строки virsh.
// VirtualMachineState returns current state of a virtual machine.
func VirtualMachineState(id string) {
var ret tylibvirt.VirtualMachineState
d, err := v.DomainLookupByName(id)
herr(err)
state, maxmem, mem, ncpu, cputime, err := v.DomainGetInfo(d)
herr(err)
ret.CPUCount = ncpu
ret.CPUTime = cputime
// God only knows why they return memory in kilobytes.
ret.MemoryBytes = mem * 1024
ret.MaxMemoryBytes = maxmem * 1024
temp := libvirt.DomainState(state)
herr(err)
switch temp {
case libvirt.DomainNostate:
ret.State = tylibvirt.VirtStatePending
case libvirt.DomainRunning:
ret.State = tylibvirt.VirtStateRunning
case libvirt.DomainBlocked:
ret.State = tylibvirt.VirtStateBlocked
case libvirt.DomainPaused:
ret.State = tylibvirt.VirtStatePaused
case libvirt.DomainShutdown:
ret.State = tylibvirt.VirtStateShutdown
case libvirt.DomainShutoff:
ret.State = tylibvirt.VirtStateShutoff
case libvirt.DomainCrashed:
ret.State = tylibvirt.VirtStateCrashed
case libvirt.DomainPmsuspended:
ret.State = tylibvirt.VirtStateHybernating
}
hret(ret)
}
А вот в данном случае мы получаем информацию о состоянии определённой машины прямо в консоли в виде отличного JSON. Теперь наша маленькая утилита становится очень полезной. Она позволяет быстро и удобно сканировать все виртуалки на сервере.
В добавку к этому коду приведу очень полезный набор констант.
// VirState represents current lifecycle state of a machine
// Pending = VM was just created and there is no state yet
// Running = VM is running
// Blocked = Blocked on resource
// Paused = VM is paused
// Shutdown = VM is being shut down
// Shutoff = VM is shut off
// Crashed = Most likely VM crashed on startup cause something is missing.
// Hybernating = Virtual Machine is hybernating usually due to guest machine request
// TODO:
type VirtState string
const (
VirtStatePending = VirtState("Pending") // VM was just created and there
VirtStateRunning = VirtState("Running") // VM is running
VirtStateBlocked = VirtState("Blocked") // VM Blocked on resource
VirtStatePaused = VirtState("Paused") // VM is paused
VirtStateShutdown = VirtState("Shutdown") // VM is being shut down
VirtStateShutoff = VirtState("Shutoff") // VM is shut off
VirtStateCrashed = VirtState("Crashed") // Most likely VM crashed on sta
VirtStateHybernating = VirtState("Hybernating") // VM is hybernating usually due
)
Как я уже говорил, читать коды возврата libvirt — не очень удобно и понятно. После получаса сидения в гугле и stackoverflow я собрал данные о том, что же означают коды остановки виртуальных машин.
Всё! Всё готово
Теперь вас может остановить только ваше воображение.
Давайте попробуем и посмотрим, что у нас получилось. Вам нужно будет добавить функции обработки параметров командной строки и все остальные мелочи, с которыми вы запросто разберётесь сами. В конце статьи я привожу полный исходный код компонента.
Запускаем нашу утилиту и пытаемся включить одну из виртуальных машин на моём компьютере:
./tarsvirt --id debian11 --virtual-machine-start
{"error":"Cannot access storage file '/dev/tars_storage/vol2': No such file or directory"}
Итак, система попыталась запустить виртуальную машину и завершила работу с ошибкой. Об ошибке мы узнали в приятном JSON-формате, и нам не придётся парсить вывод virsh для того, чтобы понять, что что-то не так.
В сообщении об ошибке говорится, что у нас не хватает какого-то жёсткого диска, чтобы запустить VM. Давайте поправим ошибку и попробуем ещё раз.
./tarsvirt --id debian11 --virtual-machine-start
{"ok":"debian11 was started"}
Красота. Всё запустилось. Проверяем через VM Manager:
(скриншот: VM Manager показывает запущенную виртуальную машину debian11)
Так и есть, машина работает.
Теперь, просто из интереса, проверяем статус виртуальной машины:
./tarsvirt --id debian11 --virtual-machine-state
{"State":"Running","MaxMemoryBytes":1073741824,"MemoryBytes":1073741824,"CPUCount":2}
Отлично, мы знаем, что мы запустились на одном гигабайте памяти с двумя процессорами. И машина работает исправно.
Вам осталось сделать только одну вещь — пойти и начать читать остальные части документации к libvirt, и вы можете программно создавать, удалять и управлять виртуальными машинами на вашем линуксе.
Это было не так-то сложно, и вы можете запросто внедрить это в любой из ваших бинарников. Это занимает не больше пары часов и позволяет вам управлять виртуальными машинами практически напрямую, без необходимости установки сторонних инструментов.
Именно с помощью этой небольшой программки мы можем создать новую виртуальную машину, склонировать новый жёсткий диск, переписать состояние линукса в этой машине и запустить её меньше чем за 2 минуты.
Удобно, надёжно, а главное — очень просто. Если вам интересно как — могу поделиться данными в будущих статьях. А пока — удачного ломания libvirt.
PS. Утилита настолько простая, что вот вам программный код:
(как в старые добрые ламповые времена, когда код распространялся напечатанным в журналах)
Читать дальше
Похожие посты
Клонируем сами, своими руками
Как клонировать Debian Linux вручную без сторонних утилит: dd, partprobe, sgdisk, e2fsck, resize2fs и немного Go — на случай, если вас занесло на необитаемый остров.
Девочка, балансирующая на NVMe-over-TCP 2.0
Продолжаем издеваться над NVMe-over-TCP. Настоящий тестовый стенд из двух Dell PowerEdge, 10-гигабитная сеть, новое ядро 5.16 и ответы на вопросы из первой части.
А все ли врут? Продолжаем издеваться над NVMe
NVMe — это не только жёсткий диск, но и протокол. Подключаем NVMe-диск по сети через TCP средствами обычного ядра Linux, без танцев с бубном и проприетарных решений.