Введение

Познакомившись с гипервизором oVirt полгода назад, я столкнулся с необходимостью управлять им вручную. Однако, такая рутина быстро наскучила, и я искал способ автоматизировать процесс. Решение я нашел в модуле oVirt для Ansible. В данной статье я поделюсь своим опытом использования этого инструмента и приведу несколько примеров его применения.

Кратко посмотрим, что там есть:

  • Управление группами пользователей
  • Аутентификация
  • Управление кластерами
  • Управление датацентрами с точки зрения oVirt
  • Управление дисками виртуальных машин
  • Управление виртуальными машинами
  • Управление сетями для виртуальных машин
  • Создание снапшотов
  • Управление пулами хранения
  • Управление шаблонами

Это не весь список, я взял основное, как мне кажется, что пригодиться почти всем. Так же почти для каждого модуля есть саб модуль, позволяющий забирать информацию из oVirt.

Можно предварительно, перед созданием большо количества виртуальных машин собрать информацию, по доступному месту на дисках в процентном соотношении. И исходя из количества свободного места, написать логику, которая позволит либо перейти к созданию виртуалок, либо завершить работу Ansible, если место в пуле осталось например 10% или 5%

Примеры плейбуков

Авторизация

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

Обычно я делал это примерно так

- block:
  - include_vars: ovirt_creds.yml # подключаем секреты

  - name: Login oVirt
  ovirt.ovirt.ovirt_auth:
    url: https://ovirt.example.com/ovirt-engine/api # адрес до API oVirt
    username: admin@internal # username
    ca_file: ca.pem # CA oVirt
    password: "{{ ovirt_password }}" # password
  no_log: true

  always:
  - name: Logout from oVirt
  ovirt.ovirt.ovirt_auth:
    state: absent
    ovirt_auth: "{{ ovirt_auth }}"
  no_log: true

В шаге Login oVirt мы передаем логин / пароль, адрес сервера. Для обычного логина, но есть еще один параметр ca_file. Который мы можем выгрузить из oVirt. На моей практике CA файл пригодился только для того, чтобы можно было загружать диски виртуальных машин.

Если Вы будете хранить CA файлы, например в секретах Vault. То вы не сможете использовать вот такую конструкцию:

- name: Login oVirt
 ovirt.ovirt.ovirt_auth:
   url: https://ovirt.example.com/ovirt-engine/api # адрес до API oVirt
   username: admin@internal # username
   ca_file: "{{ ca_info }}" # CA oVirt
   password: "{{ ovirt_password }}" # password
  no_log: true

В параметр ca_fie нельзя передавать содержимое файла CA, а нужно передать именно путь до файла, например так:

- name: "Set CA file"
copy:
  content: "{{ ovirt_rnd_ca }}"
  dest: /tmp/ca.pem

- name: "Login to oVirt"
ovirt_auth:
  ca_file: /tmp/ca.pem
  url: "{{ ovirt_url }}"
  username: "{{ ovirt_username }}"
  password: "{{ ovirt_password }}"
when: ovirt_auth is undefined or not ovirt_auth
register: loggedin

Так же, модуль поддерживает передачу кредов из ENV

OVIRT_URL = https://fqdn/ovirt-engine/api
OVIRT_USERNAME = admin@internal
OVIRT_PASSWORD = the_password
Сбор данных

Предположим, на вашем сервере есть 2 и более пулов ресурсов. У них установлена квота свободного места 5%. При достижении которого вам должен прийти алерт. что место на пуле заканчивается.

Мы можем предварительно собирать данные по свободному месту из пулов и выбирать самый свободный для создания на самом свободном пуле виртуальных машин.

Запишем имена пулов, паттерн поиска и процент квоты в файл role/create-vm/vars/main.yml

failed_low_space_indicator: 5
pttern_storage_domain: "iSCSI-L"

storage_domains:
- "{{ storage_domain_1 }}"
- "{{ storage_domain_2 }}"

storage_domain_1: "iSCSI-L1"
storage_domain_2: "iSCSI-L2"

role/create-vm/tasks/check_free_spase.yml будет иметь вид. Тут мы будем собирать данные из наших стореджей

---
- name: "Get data storage"
ovirt_storage_domain_info:
  auth: "{{ ovirt_auth }}"
  pattern: name="{{ item }}"
register: data_domain_storage

- name: "Extract storage domain information"
set_fact:
  storage_domain_info: "{{ storage_domain_info | default([]) + [{'name': item.name, 'free_space': item.available, 'total_size': item.storage.volume_group.logical_units[0].size}] }}"
loop: "{{ data_domain_storage.ovirt_storage_domains }}"
  loop_control:
  loop_var: item
no_log: true

В файле role/create-vm/tasks/main.yml подключим проверку

...
- name: "Include check free spase"
include_tasks: check_free_spase.yml
loop: "{{ storage_domains }}"

- name: "Find the least occupied storage domain"
set_fact:
  least_occupied_storage: "{{ storage_domain_info | sort(attribute='free_space') | last }}"

- name: "Check free space percentage"
set_fact:
  free_space_percentage: "{{ (least_occupied_storage.free_space / least_occupied_storage.total_size * 100) | float | round(2) }}"

- name: "Show free space percentage"
debug:
  var: free_space_percentage

- name: "Fail if free space is less than 5%"
fail:
  msg: "Not enough free space in storage domain"
when: "free_space_percentage | float < {{failed_low_space_indicator}}"
...

least_occupied_storage - данная переменная будет хранить в себе самый наименее занятый пул из всех доступных

Шаг Check free space percentage будет проверять уже самый наименее занятый пул на свободное место в процентном соотношении. И если вы словите Fail if free space is less than 5%, то получается на всех пулах вашего oVirt меньше 5%

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

Загрузка образа cloud-init

Добавить образ cloud-init достаточно просто. Нужно его загрузить в доступный пулл ресурсов

- name: "Upload image to oVirt"
ovirt_disk:
  auth: "{{ ovirt_auth }}"
  name: "ubuntu-2204-cloud-init"
  upload_image_path: "/tmp/ubuntu-2204-cloud-init.img"
  storage_domain: "{{ least_occupied_storage.name }}"
  wait: true
  bootable: true
  sparse: true
  size: "40 GiB"
  format: cow
  content_type: data

Думаю тут все довольно просто для понимания.

upload_image_path - указываем локальный файл для загрузки на oVirt sparse - создание тонкого диска size - увеличит диск до 40 GiB, после загрузки

Создание виртуальной машины

- name: "Create VM"
ovirt_vm:
  auth: "{{ ovirt_auth }}"
  wait: true
  state: "present"
  name: "New VM"
  cluster: "RnD Cluster"
  operating_system: "Linux"
  bios_type: "UEFI"
  disks:
  - id: "{{ image_data.disk.id }}"
    bootable: True
  nics:
  - name: "nic1"
  memory: "4 GB"
  memory_guaranteed: "4 GB"
  memory_max: "4 GB"
  cpu_sockets: 1
  cpu_cores: "4"
  cpu_threads: 1
  timezone: "UTC"
  graphical_console:
  protocol:
  - spice
  - vnc
  cloud_init_persist: true
  cloud_init:
    host_name: "new-host"
	timezone: "UTC"
	user_name: "user"
	user_password: "123123"
	dns_servers: "1.1.1.1 1.0.0.1"
	nic_boot_protocol_v6: "none"
	nic_boot_protocol: static
	nic_ip_address: "192.168.0.42"
	nic_netmask: "255.255.255.0"
	nic_gateway: "192.168.0.1"
	nic_name: enp1s0
	custom_script: |
        users:
        - name: user
        shell: /bin/bash
        sudo: true
        sudo: ALL=(ALL) NOPASSWD:ALL
        package_update: true
        packages:
        - qemu-guest-agent
        runcmd:
        - [ systemctl, enable, --now, qemu-guest-agent ]

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

image_data.disk.id - в этой переменной у меня была заложен ID диска cloud-init. который загружается каждый раз при создании машины. Можно конечно залить образ один раз и захардкодидь его в репе, но у меня создан плейбук с расчетом на динамичную среду

operating_system и bios_type - данные о типах биоса и операционных систем я рекомендую предварительно выгрузить и посмотреть, что выдаст Вам oVirt. Сделать это можно с помощью модуля ovirt_vm_os_info

- ovirt.ovirt.ovirt_vm_os_info:
    auth: "{{ ovirt_auth }}"
  register: result
- ansible.builtin.debug:
    msg: "{{ result.ovirt_operating_systems }}"

- ovirt.ovirt.ovirt_vm_os_info:
    auth: "{{ ovirt_auth }}"
    filter_keys: name,architecture
  register: result
- ansible.builtin.debug:
    msg: "{{ result.ovirt_operating_systems }}"

Зачем это делать? Тут история такая, что наименование типа биоса и ОС может несколько отличаться от того, что нужно передать в Ansible плейбук. От суда могут быть проблемы с созданием виртуальных машин или с их запуском. На понимание этой простой мысли я потратил относительно много времени, чтобы понять причину проблем.

Ну и кратко по cloud-init параметрам пройдемся. Тут явно видно, что можно задать имя хоста, IP адрес и т.д., но есть нюансы:

  • nic_name - должен иметь то же самое имя, какое ему дает операционная система. Если в Ubuntu 22.04 в nic_name передать eth0, то правила для интерфейса не применятся. Нужно указывать именно enp1s0
  • package_upgrade: true - если вы добавите этот параметр и под обновление попадет ядро, то после первого запуска виртуалки, эту же виртуалку потребуется перезапустить один раз. Потому, что новое ядро не будет использоваться системой, понимание этого тоже пришло не сразу, когда аффектило создание k8s кластера
  • oVirt умеет “очищать” cloud-init образ при первом запуске, который позволяет создавать из одного образа сотни виртуалок с разными ID системы (machine-id), что решает проблему с получением одинакового IP при использовании DHCP

Если у Вас machine-id не генерируется новый, то добавьте эти 2 команды в cloud-init custom script

rm /etc/machine-id ; systemd-machine-id-setup

Заключение

oVirt плагин для Ansible оказался довольно мощным инструментом. Перед его использованием рекомендую изучить подробно те механизмы, где вам требуется идемпотентность, плагин не всегда ее может предоставить, подразумеваю это из-за особенностей oVirt’a. Но в будущем думаю идемпотентность будет реализована еще в больших количествах механизмов плагина.

Анонсы и еще больше информации в Telegram-канале