Правим QEMU железным кулаком

Kate

Administrator
Команда форума
Виртуализация, на мой взгляд, всё ещё остаётся одной из самых важных технологий в администрировании ЦОД. Да, конечно “все” будут рассказывать, что контейнеры намного более удобные, и всё надо запихивать в Кубер, и всё такое… Но после гигантского нагромождения никому не нужных конфигов, в какой-то момент ты начинаешь понимать, что зашёл слишком далеко.

И действительно. Мы пишем ПО для обслуживания целого ЦОДа. Изначально всё должно было быть контейнером, и всё должно было распространяться через 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 * 2)))
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 is no state yet
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 startup cause something is missing.
VirtStateHybernating = VirtState("Hybernating") // VM is hybernating usually due to guest machine request
)


Как я уже говорил, читать коды возврата libvirt — не очень удобно и понятно. После получаса сидения в гугле и stakoverflow я собрал данные о том, что же означают коды остановки виртуальных машин.

▍ Всё! Всё готово​


Теперь вас может остановить только ваше воображение.

Давайте попробуем и посмотрим, что у нас получилось. Вам нужно будет добавить функции обработки параметров командной строки и все остальные мелочи, с которыми вы запросто разберётесь сами. В конце статьи я привожу полный исходный код компонента.

Запускаем нашу утилиту и пытаемся включить одну из виртуальных машин на моём компьютере:

./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:

8pladazxiek7v3tb-jiorb1y_pi.png


Так и есть, машина работает.

Теперь, просто из интереса, проверяем статус виртуальной машины:

./tarsvirt --id debian11 --virtual-machine-state
{"State":"Running","MaxMemoryBytes":1073741824,"MemoryBytes":1073741824,"CPUCount":2,"CPUTime":27560000000,"StateDate":null}


Отлично, мы знаем, что мы запустились на одном гигабайте памяти с двумя процессорами. И машина работает исправно.

Вам осталось сделать только одну вещь – пойти и начать читать остальные части документации к libvirt и вы можете программно создавать, удалять и управлять виртуальными машинами на вашем линуксе.

Это было не так-то сложно, и вы можете запросто внедрить это в любой из ваших бинарников. Это занимает не больше пары часов и позволяет вам управлять виртуальными машинами практически напрямую, без необходимости установки сторонних инструментов.

Именно с помощью этой небольшой программки мы можем создать новую виртуальную машину, склонировать новый жёсткий диск, переписать состояние линукса в этой машине и запустить её меньше чем за 2 минуты.

Удобно, надёжно, а главное – очень просто. Если вам интересно как — могу поделиться данными в будущих статьях. А пока — удачного ломания libvirt.

PS. Утилита настолько простая, что вот вам программный код:


Как в старые, добрые, ламповые времена, когда код распространялся напечатаным в журналах

package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"time"

"github.com/spf13/pflag"

"uncloudzone/libtars/tylibvirt"

"github.com/digitalocean/go-libvirt"
"github.com/digitalocean/go-libvirt/socket/dialers"
)

type VirtualMachineStatus string

const (
VirtualMachineStatusDeleted = VirtualMachineStatus("deleted")
VirtualMachineStatusCreated = VirtualMachineStatus("created")
VirtualMachineStatusReady = VirtualMachineStatus("ready")
VirtualMachineStatusStarting = VirtualMachineStatus("starting")
VirtualMachineStatusImaging = VirtualMachineStatus("imaging")
VirtualMachineStatusRunning = VirtualMachineStatus("running")
VirtualMachineStatusOff = VirtualMachineStatus("off")
VirtualMachineStatusShuttingDown = VirtualMachineStatus("shutting_down")
)

// Versions - originally created for testing purposes, not actually something we would need.
// var libvirtVersion = *pflag.Bool("libvirt-version", false, "Returns result with version of libvirt populated")
// var virshVersion = *pflag.Bool("virsh-version", false, "Returns result with version of virsh populated")
// var tarsvirtVersion = *pflag.Bool("tarsvirt-version", false, "Returns result with version of tarsvirt populated")

// VirtualMachine commands
var virtualMachineState = pflag.Bool("virtual-machine-state", false, "Returns result with a current machine state")
var virtualMachineSoftReboot = pflag.Bool("virtual-machine-soft-reboot", false, "reboots a machine gracefully, as chosen by hypervisor. Returns result with a current machine state")
var virtualMachineHardReboot = pflag.Bool("virtual-machine-hard-reboot", false, "sends a VM into hard-reset mode. This is damaging to all ongoing file operations. Returns result with a current machine state")
var virtualMachineShutdown = pflag.Bool("virtual-machine-shutdown", false, "gracefully shuts down the VM. Returns result with a current machine state")
var virtualMachineShutoff = pflag.Bool("virtual-machine-shutoff", false, "kills running VM. Equivalent to pulling a plug out of a computer. Returns result with a current machine state")
var virtualMachineStart = pflag.Bool("virtual-machine-start", false, "starts up a VM. Returns result with a current machine state")
var virtualMachinePause = pflag.Bool("virtual-machine-pause", false, "stops the execution of the VM. CPU is not used, but memory is still occupied. Returns result with a current machine state")
var virtualMachineResume = pflag.Bool("virtual-machine-resume", false, "called after Pause, to resume the invocation of the VM. Returns result with a current machine state")
var virtualMachineCreate = pflag.Bool("virtual-machine-create", false, "creates a new machine. Requires --xml-template parameter. Returns result with a current machine state")
var virtualMachineDelete = pflag.Bool("virtual-machine-delete", false, "deletes an existing machine.")

var id = pflag.String("id", "", "id of the machine to work with")
var xmlTemplate = pflag.String("xml-template", "", "path to an xml template file that describes a machine. See qemu docs on xml templates.")

var v *libvirt.Libvirt

// TODO: cool things you can do with Domain, but do not know how to:
// virDomainInterfaceAddresses - gets data about an IP addresses on a current interfaces. Mega-tool.
// virDomainGetGuestInfo - full data about a config of the guest OS
// virDomainGetState - provides the data about an actual domain state. Why is it shutoff or hybernating. Requires copious amount of magic fuckery to find out the actual reason with multiplication and matrix transforms, but can be translated into a redable form.
func main() {

pflag.Parse()

virtinit()

switch {
case *virtualMachineState:
VirtualMachineState(*id)
case *virtualMachineSoftReboot:
VirtualMachineSoftReboot(*id)
case *virtualMachineHardReboot:
VirtualMachineHardReboot(*id)
case *virtualMachineShutdown:
VirtualMachineShutdown(*id)
case *virtualMachineShutoff:
VirtualMachineShutoff(*id)
case *virtualMachineStart:
VirtualMachineStart(*id)
case *virtualMachinePause:
VirtualMachinePause(*id)
case *virtualMachineResume:
VirtualMachineResume(*id)
case *virtualMachineCreate:
VirtualMachineCreate(*xmlTemplate)
case *virtualMachineDelete:
VirtualMachineDelete(*id)
}

}

// 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
}
v.DomainGetState(d, 0)
hret(ret)
}

// 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)
}

// VirtualMachineDelete deletes a new VM from an xml template file
func VirtualMachineDelete(id string) {
d, err := v.DomainLookupByName(id)
herr(err)
err = v.DomainUndefineFlags(d, libvirt.DomainUndefineKeepNvram)
herr(err)
hok(fmt.Sprintf("%v was deleted", id))
}

// 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))
}

// VirtualMachineHardReboot sends a VM into hard-reset mode. This is damaging to all ongoing file operations.
func VirtualMachineHardReboot(id string) {
d, err := v.DomainLookupByName(id)
herr(err)

err = v.DomainReset(d, 0)
herr(err)

hok(fmt.Sprintf("%v was hard-rebooted successfully", id))
}

// VirtualMachineShutdown gracefully shuts down the VM.
func VirtualMachineShutdown(id string) {
d, err := v.DomainLookupByName(id)
herr(err)

err = v.DomainShutdown(d)
herr(err)

hok(fmt.Sprintf("%v was shutdown successfully", id))
}

// VirtualMachineShutoff kills running VM. Equivalent to pulling a plug out of a computer.
func VirtualMachineShutoff(id string) {
d, err := v.DomainLookupByName(id)
herr(err)

err = v.DomainDestroy(d)
herr(err)

hok(fmt.Sprintf("%v was shutoff successfully", id))
}

// VirtualMachineStart starts up a VM.
func VirtualMachineStart(id string) {
d, err := v.DomainLookupByName(id)
herr(err)

//v.DomainRestore()
//_, err = v.DomainCreateWithFlags(d, uint32(libvirt.DomainStartBypassCache))
err = v.DomainCreate(d)

herr(err)

hok(fmt.Sprintf("%v was started", id))
}

// VirtualMachinePause stops the execution of the VM. CPU is not used, but memory is still occupied.
func VirtualMachinePause(id string) {
d, err := v.DomainLookupByName(id)
herr(err)

err = v.DomainSuspend(d)
herr(err)

hok(fmt.Sprintf("%v is paused", id))
}

// VirtualMachineResume can be called after Pause, to resume the invocation of the VM.
func VirtualMachineResume(id string) {
d, err := v.DomainLookupByName(id)
herr(err)

err = v.DomainResume(d)
herr(err)

hok(fmt.Sprintf("%v was resumed", id))
}

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)
}

func virtinit() {
v = libvirt.NewWithDialer(dialers.NewLocal(dialers.WithLocalTimeout(time.Second * 2)))
if err := v.Connect(); err != nil {
log.Fatalf("failed to connect: %v", err)
}
}


 
Сверху