devops
April 27, 2023

Управляем виртуальной машиной через QEMU Guest Agent

Имея гипервизор и запущенные на нём виртуальные машины, скорее всего, рано или поздно, возникнет необходимость что-то пнуть, взять или запустить внутри гостевой системы, не прибегая к доступу по SSH. Как? С помощью qemu-guest-agent. Посмотрим как всё это работает.

Собираем стенд

Окружение

Собственно, нам много не надо - сервер с qemu-kvm и какая-нибудь виртуалка с не самым маргинальным дистрибутивом. В моём случае qemu-kvm развёрнут прямо на лаптопе, в виртуальной машине крутится Ubuntu Jammy:

andrey@ionice:~$ virsh list

 Id   Name                      State
-----------------------------------------
 7    ubuntu22.04-guest-agent   running

Установка qemu-guest-agent

Установка агента тривиальна и скучна - всё есть в репозиториях, устанавливаем и стартуем службу

andrey@ionice:~$ virsh console ubuntu22.04-guest-agent

qga login: server
Password: 
Welcome to Ubuntu 22.04.1 LTS (GNU/Linux 5.15.0-71-generic x86_64)

server@qga:~$ sudo apt install qemu-guest-agent
server@qga:~$ sudo systemctl enable --now qemu-guest-agent
server@qga:~$ sudo systemctl status qemu-guest-agent
● qemu-guest-agent.service - QEMU Guest Agent
     Loaded: loaded (/lib/systemd/system/qemu-guest-agent.service; static)
     Active: active (running) since Thu 2023-04-27 18:03:54 UTC; 1min 19s ago
   Main PID: 1039 (qemu-ga)
      Tasks: 2 (limit: 4572)
     Memory: 380.0K
        CPU: 1ms
     CGroup: /system.slice/qemu-guest-agent.service
             └─1039 /usr/sbin/qemu-ga

Apr 27 18:03:54 qga systemd[1]: Started QEMU Guest Agent.

^[

Всё, основную часть задачи выполнили. Теперь убедимся что наша виртуалка имеет устройство qemu-ga:

andrey@ionice:~$ virsh dumpxml ubuntu22.04-guest-agent
...
    <channel type='unix'>
      <target type='virtio' name='org.qemu.guest_agent.0'/>
      <address type='virtio-serial' controller='0' bus='0' port='1'/>
    </channel>
...

Если такой секции нет, можем дописать с помощью команды virsh edit ubuntu22.04-guest-agent. Желательно перезагрузить виртуалку после таких манипуляций командой virsh reboot ubuntu22.04-guest-agent.

Проверка связи

Теперь самое интересное - обратимся к виртуальной машине через агента:

andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-ping"}'
{"return":{}}

Ну... как-то не впечатляем. Дубль два:

andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-get-time"}' --pretty
{
  "return": 1682620143771430000
}

Сделаем поправку на ветер:

andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-get-time"}' --pretty | jq .return | date -d -
Чт 27 апр 2023 00:00:00 UTC

Более-менее информация о дате соответствует действительности, но почему-то таймштамп не вернул нам время. Где-то нас обворовали. Тем не менее, самое главное - информацию мы получили не прибегая к протоколам удалённого доступа. Разбираемся дальше.

Обзор протокола QMP

Общение с виртаульной машиной осуществляется по протоколу QMP (QEMU Machine Protocol) через локальный сокет на гипервизоре (то самое виртуальное устройство, которое мы настраивали ранее). Протокол поддерживает весьма обширный набор команд как GET, так и POST. Формат данных протокола - JSON.

Посмотрим, какие команды поддерживает наш qemu-guest-agent в виртуалке:

andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-info"}' --pretty | jq -r '.return.supported_commands[] | select(.enabled==true) | .name'

guest-ssh-remove-authorized-keys
guest-ssh-add-authorized-keys
guest-ssh-get-authorized-keys
guest-get-osinfo
guest-get-timezone
guest-get-users
guest-get-host-name
guest-exec
guest-exec-status
guest-get-memory-block-info
guest-set-memory-blocks
guest-get-memory-blocks
guest-set-user-password
guest-get-fsinfo
guest-get-disks
guest-set-vcpus
guest-get-vcpus
guest-network-get-interfaces
guest-suspend-hybrid
guest-suspend-ram
guest-suspend-disk
guest-fstrim
guest-fsfreeze-thaw
guest-fsfreeze-freeze-list
guest-fsfreeze-freeze
guest-fsfreeze-status
guest-file-flush
guest-file-seek
guest-file-write
guest-file-read
guest-file-close
guest-file-open
guest-shutdown
guest-info
guest-set-time
guest-get-time
guest-ping
guest-sync
guest-sync-delimited

Впечатляет? Ваш внутренний безопасник уже истерично лупит по кнопке "Запретить"? Мозг рисует безграничные возможности управления инфраструктурой? Чтобы понимать что к чему, сначала рекомендую почитать референс протокола и двигаться дальше.

Примеры выполнения команд

  • Данные об операционной системе:
andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-get-osinfo"}' --pretty | jq

{
  "return": {
    "name": "Ubuntu",
    "kernel-release": "5.15.0-71-generic",
    "version": "22.04.1 LTS (Jammy Jellyfish)",
    "pretty-name": "Ubuntu 22.04.1 LTS",
    "version-id": "22.04",
    "kernel-version": "#78-Ubuntu SMP Tue Apr 18 09:00:29 UTC 2023",
    "machine": "x86_64",
    "id": "ubuntu"
  }
}
  • Список IPv4 адресов:
andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-network-get-interfaces"}' --pretty | jq -r '.return[]."ip-addresses"[] | select(."ip-address-type"=="ipv4" and ."ip-address"!="127.0.0.1") | ."ip-address"'

192.168.122.17
  • Список несистемных пользователей
andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-get-users"}' --pretty | jq -r .return[].user

server
  • Добавим пользователю server публичный ключ для доступа по SSH
andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-ssh-add-authorized-keys", "arguments": {"username": "server", "keys": [ "ssh-rsa xxxxx" ] }}' --pretty
  • Убедимся, что ключ был добавлен
andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-ssh-get-authorized-keys", "arguments": {"username": "server" }}' --pretty
{
  "return": {
    "keys": [
      "ssh-rsa xxxxx"
    ]
  }
}
  • Выполним какую-нибудь shell-команду и получим её выхлоп. Для этого нам потребуются две команды - guest-exec, чтобы запустить процесс и получить его PID, и guest-exec-status, чтобы узнать чем завершилось выполнение процесса. Выхлоп будет в формате base64
andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-exec", "arguments": { "path": "/usr/bin/lsb_release", "arg": [ "-a" ], "capture-output": true }}' --pretty | jq ".return.pid"
832
andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-exec-status", "arguments": { "pid": 832 }}' --pretty | jq
{
  "return": {
    "exitcode": 0,
    "err-data": "Tm8gTFNCIG1vZHVsZXMgYXJlIGF2YWlsYWJsZS4K",
    "out-data": "RGlzdHJpYnV0b3IgSUQ6CVVidW50dQpEZXNjcmlwdGlvbjoJVWJ1bnR1IDIyLjA0LjEgTFRTClJlbGVhc2U6CTIyLjA0CkNvZGVuYW1lOglqYW1teQo=",
    "exited": true
  }
}

andrey@ionice:~$ echo RGlzdHJpYnV0b3IgSUQ6CVVidW50dQpEZXNjcmlwdGlvbjoJVWJ1bnR1IDIyLjA0LjEgTFRTClJlbGVhc2U6CTIyLjA0CkNvZGVuYW1lOglqYW1teQo= | base64 --decode 
Distributor ID:	Ubuntu
Description:	Ubuntu 22.04.1 LTS
Release:	22.04
Codename:	jammy

andrey@ionice:~$ echo Tm8gTFNCIG1vZHVsZXMgYXJlIGF2YWlsYWJsZS4K | base64 --decode 
No LSB modules are available.
  • Прочитаем что-нибудь из файла. Для этого сначала откроем его на чтение, получив его ID, затем прочитаем данные из файла (они будут в base64), а потом закроем файл, чтобы не держать лишние процессы и дескрипторы:
andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-file-open", "arguments": { "path": "/etc/hosts", "mode" : "r" }}' --pretty
{
  "return": 1000
}

andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-file-read", "arguments": { "handle": 1000 }}' --pretty
{
  "return": {
    "count": 218,
    "buf-b64": "MTI3LjAuMC4xIGxvY2FsaG9zdAoxMjcuMC4xLjEgcWdhCgojIFRoZSBmb2xsb3dpbmcgbGluZXMgYXJlIGRlc2lyYWJsZSBmb3IgSVB2NiBjYXBhYmxlIGhvc3RzCjo6MSAgICAgaXA2LWxvY2FsaG9zdCBpcDYtbG9vcGJhY2sKZmUwMDo6MCBpcDYtbG9jYWxuZXQKZmYwMDo6MCBpcDYtbWNhc3RwcmVmaXgKZmYwMjo6MSBpcDYtYWxsbm9kZXMKZmYwMjo6MiBpcDYtYWxscm91dGVycwo=",
    "eof": true
  }
}

andrey@ionice:~$ echo MTI3LjAuMC4xIGxvY2FsaG9zdAoxMjcuMC4xLjEgcWdhCgojIFRoZSBmb2xsb3dpbmcgbGluZXMgYXJlIGRlc2lyYWJsZSBmb3IgSVB2NiBjYXBhYmxlIGhvc3RzCjo6MSAgICAgaXA2LWxvY2FsaG9zdCBpcDYtbG9vcGJhY2sKZmUwMDo6MCBpcDYtbG9jYWxuZXQKZmYwMDo6MCBpcDYtbWNhc3RwcmVmaXgKZmYwMjo6MSBpcDYtYWxsbm9kZXMKZmYwMjo6MiBpcDYtYWxscm91dGVycwo= | base64 --decode 
127.0.0.1 localhost
127.0.1.1 qga

# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters

andrey@ionice:~$ virsh qemu-agent-command ubuntu22.04-guest-agent '{"execute": "guest-file-close", "arguments": { "handle": 1000 }}' --pretty
{
  "return": {

  }
}

Следы

Хоть мой внутренний автоматизатор ликует, параноик всё равно немного параноит. Как минимум, ему хотелось бы понимать как пользователю гостевой ОС контролировать происходящее с его системой. Посмотрим логи с виртуалки:

root@qga:~# journalctl -u qemu-guest-agent

Apr 27 18:22:54 qga systemd[1]: Started QEMU Guest Agent.
Apr 27 18:28:40 qga qemu-ga[624]: info: guest-ping called
Apr 27 18:29:17 qga qemu-ga[624]: info: guest-ping called
Apr 27 19:55:41 qga qemu-ga[624]: info: guest-exec called: "/usr/bin/lsb_release -a"
Apr 27 19:56:45 qga qemu-ga[624]: info: guest-exec-status called, pid: 832
Apr 27 20:07:06 qga qemu-ga[624]: info: guest-file-open called, filepath: /etc/hosts, mode: r
Apr 27 20:07:06 qga qemu-ga[624]: info: guest-file-open, handle: 1000
Apr 27 20:14:12 qga qemu-ga[624]: info: guest-file-close called, handle: 1000

Как видим, ключевые команды в логе отображаются. Можно выдохнуть.

Выводы

QEMU Guest Agent - очень интересный способ контроля и управления виртуальными машинами, который уже давно используется в продуктовых средах. Если вы заранее подготовите образы ОС, напишете скрипты или сервисы управления, то сможете облегчить себе обслуживание инфраструктуры "без рук". Следует однако помнить, что применение инструмента потребует пересмотра политик безопасности. Ну и классическое - with great power comes great responsibility