Привет, Хабр! Статья будет посвящена любимому мной IaC. Чтобы ввести в курс дела, кратко расскажу про VMmanager и текущую реализацию продукта. Затронем варианты, как можно работать с VMmanager с подходом Infrastructure as Code, а основная часть — про развертывание платформы VMmanager и управление виртуальными машинами в ней с помощью Ansible.

О платформе VMmanager

Наверняка вы уже знаете, что такое VMmanager. Это платформа управления виртуализацией. Она имеет версии VMmanager Hosting и VMmanager Infrastructure — под разные цели использования.

Версия

Hosting

Infrastructure

Назначение

— ориентирована на потребности хостинг-провайдеров

— ориентирована на потребности владельцев IT-инфраструктур

Состав

— базовая функциональность платформы
— возможность брендирования

— базовая функциональность платформы
— дополнительные функции

Дополнительно

— регистрация в Едином реестре российских программ для электронных вычислительных машин и баз данных
— поддержка ОС Astra Linux, сертифицированной Федеральной службой по техническому и экспортному контролю (ФСТЭК России)

VMmanager — один из продуктов ISPsystem, он имеет интеграцию с другими продуктами, на схеме кратко и просто представлена их взаимосвязь.

Я буду работать с VMmanager Infrastructure.

IaC-решения

Для реализации подхода Infrastructure as Code в работе с VMmanager у нас есть сценарий развертывания платформы с помощью Ansible, примеры управления инфраструктурой через провайдер Terraform и работа с платформой с использованием VMmanager API. Все это представлено в документации.

По работе с Terraform уже была подробная статья. Поэтому сосредоточимся на Ansible. С помощью готового сценария мы развернем платформу, посмотрим, что из себя представляет сценарий (спойлер: набор плейбуков с задачами, использующими модуль ansible.builtin.uri). А для управления виртуальными машинами на платформе я погружусь в VMmanager API и напишу собственный модуль на Python.

Управление инфраструктурой через VMmanager API и Ansible

Развертывание платформы с помощью Ansible

Начнем с подготовки серверов для платформы VMmanager, понадобится Astra Linux версии 1.8.1 уровня защиты «Орел». У меня есть такой образ, его я и буду разворачивать в Yandex Cloud. Узел кластера для тестирования можно не разворачивать: в VMmanager для таких целей можно настроить кластер-заглушку, это нужно сделать уже после создания кластера.

Настройку серверов для платформы и узла кластера я не буду приводить в статье по двум причинам: во-первых, в документации очень подробно и точно описаны шаги — можно смело по ним идти, все получится; во-вторых, тема статьи о другом и будет неуместно сюда из документации репостить объемный материал. Но приведу несколько замечаний, которые при аналогичном развертывании на виртуальной машине Yandex Cloud могут быть полезны.

Примечание к настройке сервера

Из всех пунктов мне понадобилось только отредактировать /etc/apt/sources.list. Остальное я оставил по умолчанию.

Раздел «Настройка подключения к узлам кластера» можно пропустить, если настраиваем кластер-заглушку.

Переходим к развертыванию VMmanager. В документации есть step-by-step по установке VMmanager с помощью Ansible. Но, поскольку шагов не так много и это уже про IaC, я перепишу в статью все шаги, немного добавив пояснений.

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

Все продукты ISPsystem доступны для бесплатного тестирования. По запросу на сайте получите бесплатный триал или доступ к демостенду. Также можно заказать демонстрацию интересующих платформ на сайте: DCImanager, VMmanager, BILLmanager, DNSmanager.

Нас все еще интересует триал для VMmanager.

Переходим к установке платформы.

  1. Установим Ansible на ПК, с которого будет запускаться установка платформы. Порядок установки в официальной документации Ansible.

  2. На ПК с Ansible:

    Если на ПК не установлена утилита curl, установите ее: 

    dnf install curl || apt install curl

    Скачайте сценарии установки: 

    curl https://download.ispsystem.com/extras/ansible/vmmanager6_common.tar.gz

    Создайте SSH-ключ и скопируйте его на сервер платформы. Подробнее — в статье про SSH-протокол.

  3. Создадим директорию для сценариев установки: 

    mkdir vm6_ansible

  4. Распакуем в директорию архив со сценариями: 

    tar xzf vmmanager6_common.tar.gz -C vm6_ansible/

  5. Перейдем в созданную директорию:

    cd vm6_ansible

  6. Укажем параметры установки в секции vars файла vmmanager6.yml:

  vars:
    vmi_first_username: "admin@example.com"
    vmi_first_password: "q1w2e3r4"
    vmmanager6_license_token: "..........:..................."
    vmi_domain: "{{ ansible_ssh_host }}"
# Поскольку я разворачивал виртуальную машину в Yandex Cloud, 
# параметры сети берем по факту, в которой разворачиваем. 
    vmi_network: "10.177.91.0/24"
    vmi_network_gateway: "10.177.91.1"
    vmi_network_note: "some network notes"
    vmi_pool_name: "some_pool"
    vmi_pool_note: "testing pool"
    vmi_cluster_name: "new_cluster"
    vmi_time_zone: "UTC"
    vmi_cluster_note: "some cluster note"
    vmi_domain_template: ".example.com"
# Я указал для check ip из настроек dns, но он нам не понадобится в рамках теста.
    vmi_node_check_ip: "10.177.181.142"
# Мы далее закомментируем все, что касается ssl и бэкапа, поэтому следующие
# настройки можно не трогать.
    vmi_certificate: "-----BEGIN CERTIFICATE-----\nMIIDkT……7s=\n-----END CERTIFICATE-----\n"
    vmi_certificate_key: "-----BEGIN PRIVATE KEY-----\nMI….BlXDeTd\n-----END PRIVATE KEY-----\n"
    vmi_certificate_ca: ""
    vmi_backup_ip: "10.177.91.71"
    vmi_backup_user: "root"
    vmi_backup_password: "q1w2e3p4"
    vmi_backup_path: "/backup"
  1. Мы не планируем подключать SSL-сертификат, закомментируем в файле vmmanager6.yml строку:

    #    - include_tasks: cert.yml

  2. Резервное копирование платформы мы также не планируем, закомментируем в файле vmmanager6.yml строку:

    #    - include_tasks: backup.yml

  3. Запустим установку:

    ansible-playbook -i <IP>, -u root vmmanager6.yml

    Вместо <IP> укажите ip своей виртуальной машины.

Посмотрим немного глубже в Ansible-сценарий установки VMmanager. В распакованном архиве мы видим набор плейбуков.

Основной плейбук vmmanager6.yml, в который подключаются по порядку остальные плейбуки для установки.

---
- name: Install VMmanager 6 playbook
  hosts: all
  debugger: on_failed
  vars:
    vmi_first_username: "admin@example.com"
    vmi_first_password: "q1w2e3r4"
    vmmanager6_license_token: ".............:................"
    vmi_domain: "{{ ansible_ssh_host }}"
    vmi_network: "10.177.91.0/24"
    vmi_network_gateway: "10.177.91.1"
    vmi_network_note: "some network notes"
    vmi_pool_name: "some_pool"
    vmi_pool_note: "testing pool"
    vmi_cluster_name: "new_cluster"
    vmi_time_zone: "UTC"
    vmi_cluster_note: "some cluster note"
    vmi_domain_template: ".example.com"
    vmi_node_check_ip: "10.177.181.142"
    vmi_certificate: "-----BEGIN CERTIFICATE-----\nMIIDkT……7s=\n-----END CERTIFICATE-----\n"
    vmi_certificate_key: "-----BEGIN PRIVATE KEY-----\nMI….BlXDeTd\n-----END PRIVATE KEY-----\n"    vmi_certificate_ca: ""
    vmi_backup_ip: "10.177.91.71"
    vmi_backup_user: "root"
    vmi_backup_password: "q1w2e3p4"
    vmi_backup_path: "/backup"
  tasks:
    - include_tasks: remove_and_install.yml

    - name: First time token
      uri:
        url: "https://{{ vmi_domain }}/auth/v4/public/token"
        method: POST
        body_format: json
        status_code: [200,201,503]
        return_content: yes
        body:
          email: "{{ vmi_first_username }}"
          password: "{{ vmi_first_password }}"
        validate_certs: no
      register: first_token
      retries: 3
      delay: 30
      until: first_token.status == 201

    - name: make ses6 a fact
      set_fact:
        ses6: "{{ (first_token.content|from_json).token }}"

    - include_tasks: first_run.yml
#   - include_tasks: cert.yml
    - include_tasks: setup_ip.yml
    - include_tasks: cluster.yml
    - include_tasks: storage.yml
#   - include_tasks: backup.yml

Посмотрим на один из плейбуков cluster.yml, чтобы разобраться, что происходит при установке. Мы видим, что сценарий построен на использовании модуля ansible.builtin.uri.

---
- name: Set up cluster
  uri:
# В документации API можем посмотреть url для создания кластера, и другие url,
# а также параметры, которые мы можем передать.
    url: "https://{{ vmi_domain }}/vm/v3/cluster"
    method: POST
    body_format: json
    status_code: [200, 401, 409, 500, 503]
    headers:
      Cookie: "ses6={{ ses6 }}"
      x-xsrf-token: "{{ ses6 }}"
    body:
      name: "{{ vmi_cluster_name }}"
      virtualization_type: "kvm"
      time_zone: "{{ vmi_time_zone }}"
      dns_servers:
        - "1.1.1.1"
      comment: "{{ vmi_cluster_note }}"
      os:
        - 1
      iso_enabled: false
      manage_disk_enabled: false
      domain_template: "{{ vmi_domain_template }}"
      domain_change_allowed: false
      overselling: 1
      host_per_node_limit: -1
      host_distribution_policy: "spread"
      host_filter: []
      backup_locations: []
      image_storage_path: "/image"
      os_storage_path: "/share"
      datacenter_type: "common"
      interfaces:
        - interface: 0
          ippool: [ 1 ]
      node_network:
        gateway: "{{ vmi_node_check_ip }}"
        timeout: 300
      vxlan_mode: "disabled"
    validate_certs: no
  register: cluster
  retries: 3
  delay: 10
  until: cluster.status == 200

С помощью переменных мы ранее задали все нужные значения, которые подтягиваются при выполнении задач. ansible.builtin.uri отправляет запросы для скачивания установочных файлов VMmanager API и инициализации установки. А далее происходит магия… Обращение к VMmanager API для создания кластера и его настройки. 

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

Далее нужно создать кластер в VMmanager — в документации пошагово представлено, как это сделать через веб-интерфейс, а затем настроить заглушку для кластера. Эти шаги я тоже оставляю за пределами статьи — по тем же причинам, что и настройка серверов.

По заглушке кластера есть небольшое примечание

Команда должна быть:

update vm_cluster set virtualization_type = 'dummy' where id = <cluster_id>; 

Где ‘dummy’ в одинарных кавычках.

Управление ВМ с помощью модуля ansible.builtin.uri

У меня готова платформа VMmanager, кластер настроен и подключен, самое время попробовать в IaC — будем создавать виртуальные машины. Научимся с простого варианта — с использованием модуля ansible.builtin.uri. Поскольку VMmanager API позволяет довольно гибко работать с платформой, будет полезно отработать быстрый вариант работы с API, если захочется использовать разные возможности управления. 

Отмечу, что данный способ имеет определенный нюанс: мы не получаем контроль идемпотентности нашего Infrastructure as Code. VMmanager API обеспечивает идемпотентность только по id, что может быть удобно, когда мы не хотим иметь ограничение в именах виртуальных машин, но модуль ansible.builtin.uri это не обеспечит. В POST-запросе нельзя указать id, то есть создать виртуальную машину с конкретным id. 

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

Для управления виртуальными машинами нам будет достаточно переиспользовать файл с переменными и создать сценарий. Сеть и пул IP-адресов уже созданы при развертывании платформы, учетную запись рекомендую создать отличную от админской, пользователя создадим из веб-интерфейса. А далее обратимся к документации API.

Заполним основной файл сценария — с переменными, авторизацией и вызовом сценария создания виртуальной машины.

---
- name: Install VMmanager 6 playbook
  hosts: all
  debugger: on_failed
  vars:
    vmi_first_username: "vm@example.com"
    vmi_first_password: "uA2sW2nT9czV"
    vmi_domain: "{{ ansible_ssh_host }}"
  tasks:

   - name: First time token
      uri:
        url: "https://{{ vmi_domain }}/auth/v4/public/token"
        method: POST
        body_format: json
        status_code: [200,201,503]
        return_content: yes
        body:
          email: "{{ vmi_first_username }}"
          password: "{{ vmi_first_password }}"
        validate_certs: no
      register: first_token
      retries: 3
      delay: 30
      until: first_token.status == 201

    - name: make ses6 a fact
      set_fact:
        ses6: "{{ (first_token.content|from_json).token }}"

    - include_tasks: vm.yml

И напишем сам сценарий работы с виртуальными машинами.

–
# Для начала посмотрим, какие же виртуальные машины уже развернуты, 
# указанный url выдаст все, без фильтрации.
- name: Get up vm
  uri:
    url: "https://{{ vmi_domain }}/vm/v3/host"
    method: GET
    body_format: json
    status_code: [200, 401, 409, 500, 503]
    headers:
      Cookie: "ses6={{ ses6 }}"
      x-xsrf-token: "{{ ses6 }}"
    validate_certs: no


# Для тестирования мы передаем минимальный набор параметров.
- name: Set up vm
  uri:
    url: "https://{{ vmi_domain }}/vm/v3/host"
    method: POST
    body_format: json
    status_code: [200, 401, 409, 500, 503]
    headers:
      Cookie: "ses6={{ ses6 }}"
      x-xsrf-token: "{{ ses6 }}"
    body:
      name: My_vm
      password: vmsecret
# Для cluster, ipv4_pool, os передаем id, в веб-интерфейсе все они указаны, 
# можно посмотреть. 
      cluster: 1
      os: 1
      ram_mib: 512
      hdd_mib: 5000
      cpu_number: 1
      ipv4_pool: [1]
    validate_certs: no

Управление ВМ с помощью своего модуля Ansible

Настало время перейти к технически интересному варианту, полностью соответствующему подходу Infrastructure as Code. Я напишу собственный модуль на Python для создания виртуальных машин в VMmanager. Грамотно написанный модуль обеспечит на стороне Ansible идемпотентность нашего инфраструктурного кода.

Если быть точными, то мы напишем два модуля vm_info, vm и два вспомогательных модуля auth_utils и info_utils. Это будет полностью соответствовать рекомендациям Ansible, а именно выносить сбор информации, не вносящий изменения в инфраструктуру, в модуль info, общий код модулей — во вспомогательные модули.

Начнем с модуля auth_utils. Он нужен для авторизации, при вызове других модулей мы получаем токен и возвращаем его в вызвавший авторизацию модуль.

# -*- coding: utf-8 -*-

import json
import time
import urllib3
from ansible.module_utils.urls import open_url, SSLValidationError

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def get_auth_token(base_url, email, password, retries=3, delay=30, validate_certs_param=None, module=None):
    """
    Получение токена авторизации через API VMI.

    :param base_url: Базовый URL (например, https://vmi.example.com)
    :param email: Логин пользователя
    :param password: Пароль
    :param retries: Количество попыток
    :param delay: Задержка между попытками
    :param validate_certs_param: строка 'no' или None (если не передан параметр)
    :param module: ссылка на AnsibleModule для вывода ошибок
    :return: токен сессии
    """
    url = f"{base_url}/auth/v4/public/token"
    headers = {
        'Content-Type': 'application/json'
    }
    body = json.dumps({
        "email": email,
        "password": password
    })

    last_exception = None
    for attempt in range(1, retries + 1):
        try:
            kwargs = {
                "url": url,
                "method": "POST",
                "headers": headers,
                "data": body,
            }

            if validate_certs_param == "no":
                kwargs["validate_certs"] = False

            response = open_url(**kwargs)

            if response.getcode() in (200, 201):
                result = json.loads(response.read())
                return result.get("token")

        except (SSLValidationError) as e:
            last_exception = e
            if module:
                module.warn(f"Попытка {attempt} не удалась: {e}")
        time.sleep(delay)

    if module:
        module.fail_json(msg="Не удалось получить токен после нескольких попыток", exception=str(last_exception))
    else:
        raise Exception(f"Ошибка получения токена: {last_exception}")

Далее vm_info. Мы могли бы ограничиться только вспомогательным модулем, но будет более показательно реализовать модуль info полностью, тем более что он будет содержать минимальное количество базового кода.

#!/usr/bin/python

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.auth_utils import get_auth_token
from ansible.module_utils.info_utils import get_vmi_info

DOCUMENTATION = r'''
---
module: vmi_info
short_description: Получение информации о хосте по имени из VMI API
description:
  - Получает токен через auth_utils.get_auth_token, затем делает GET запрос через info_utils.get_vmi_info
options:
  vmi_domain:
    description: Домен VMI без https://
    required: true
    type: str
  vmi_first_username:
    description: Email пользователя
    required: true
    type: str
  vmi_first_password:
    description: Пароль пользователя
    required: true
    type: str
  name:
    description: Имя хоста для фильтрации
    required: true
    type: str
  validate_certs:
    description:
      - Отключает проверку SSL-сертификатов ('no' для отключения).
    required: false
    type: str
'''

EXAMPLES = r'''
- name: Получить информацию о хосте
  vm_info:
    vmi_domain: vmi.example.com
    vmi_first_username: admin@example.com
    vmi_first_password: secret
    name: my-host
    validate_certs: no
  register: result
'''

def main():
    module = AnsibleModule(
        argument_spec=dict(
            vmi_domain=dict(required=True, type='str'),
            vmi_first_username=dict(required=True, type='str'),
            vmi_first_password=dict(required=True, type='str'),
            name=dict(required=True, type='str'),
            validate_certs=dict(required=False, type='str'),
        )
    )

    base_url = f"https://{module.params['vmi_domain']}"
    email = module.params['vmi_first_username']
    password = module.params['vmi_first_password']
    name = module.params['name']
    validate_certs = module.params.get('validate_certs')

    # Получаем токен
    token = get_auth_token(
        base_url=base_url,
        email=email,
        password=password,
        validate_certs_param=validate_certs,
        module=module
    )

    # Формируем endpoint
    endpoint = f"/vm/v3/host?where=(name+EQ+'{name}')"

    # Получаем информацию
    data = get_vmi_info(base_url, token, endpoint, validate_certs, module)

    module.exit_json(changed=False, finder=data.get('size', []))


if __name__ == '__main__':
    main()

Теперь перейдем к вспомогательному модулю info_utils. Он нужен для реализации идемпотентности на стороне Ansible-модуля создания виртуальных машин. Через этот модуль мы будем получать информацию о текущей инфраструктуре.

# -*- coding: utf-8 -*-

from ansible.module_utils.urls import open_url
import json

def get_vmi_info(base_url, token, endpoint, validate_certs, module):
    """
    Делает GET-запрос к API по endpoint с использованием токена.

    :param base_url: базовый URL с https
    :param token: токен авторизации ses6
    :param endpoint: путь API начиная с /, например /vm/v3/host?where=(name+...
    :param validate_certs: 'no' или None
    :param module: объект AnsibleModule для fail_json
    :return: распарсенный JSON из ответа
    """
    url = f"{base_url}{endpoint}"
    headers = {
        'Accept': 'application/json',
        'Cookie': f'ses6={token}',
        'x-xsrf-token': token,
    }

    kwargs = {
        'url': url,
        'method': 'GET',
        'headers': headers,
    }
    if validate_certs == 'no':
        kwargs['validate_certs'] = False

    try:
        response = open_url(**kwargs)
        return json.loads(response.read())
    except Exception as e:
        module.fail_json(msg=f"Ошибка при GET-запросе к {url}", error=str(e))

Последним модулем будет vm. Запрашивая информацию о текущей инфраструктуре, мы будем сравнивать ее параметры со входными параметрами при вызове модуля и в случае совпадения текущего состояния и желаемого — не производить изменения, отдавая changed False.

#!/usr/bin/python

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.auth_utils import get_auth_token
from ansible.module_utils.info_utils import get_vmi_info
from ansible.module_utils.urls import open_url
import ssl
import http.client
import json
from urllib.parse import urlparse

DOCUMENTATION = r'''
---
module: vm
short_description: Управление виртуальной машиной в VM
description:
  - Получает токен через get_auth_token.
  - Проверяет наличие VM по имени через get_vmi_info.
  - Создает VM если state=present и VM не существует.
options:
  vmi_domain:
    description: Домен VMI без https://
    required: true
    type: str
  vmi_first_username:
    description: Email пользователя
    required: true
    type: str
  vmi_first_password:
    description: Пароль пользователя
    required: true
    type: str
  state:
    description: Состояние VM
    required: false
    default: present
    choices: [present, absent]
    type: str
  name:
    description: Имя виртуальной машины
    required: true
    type: str
  password:
    description: Пароль для VM
    required: true
    type: str
  cluster:
    description: ID кластера
    required: true
    type: int
  os:
    description: ID операционной системы
    required: true
    type: int
  ram_mib:
    description: RAM в Мибибайтах
    required: true
    type: int
  hdd_mib:
    description: HDD в Мибибайтах
    required: true
    type: int
  cpu_number:
    description: Количество CPU
    required: true
    type: int
  ipv4_pool:
    description: Список ID пулов IPv4
    required: true
    type: list
    elements: int
  validate_certs:
    description:
      - Отключает проверку SSL-сертификатов ('no' для отключения).
    required: false
    type: str
'''

EXAMPLES = r'''
- name: Создать VM если отсутствует
  vm:
    vmi_domain: vmi.example.com
    vmi_first_username: admin@example.com
    vmi_first_password: secret
    name: My_vm
    password: vmsecret
    cluster: 1
    os: 1
    ram_mib: 512
    hdd_mib: 5000
    cpu_number: 1
    ipv4_pool: [1]
    validate_certs: no
    state: present
'''

def create_vm(base_url, token, vm_spec, validate_certs, module):
    url = f"{base_url}/vm/v3/host"
    body = json.dumps(vm_spec)
    headers = {
        'Content-Type': 'application/json',
        'Cookie': f'ses6={token}',
        'x-xsrf-token': token,
    }
    body = json.dumps(vm_spec)
    kwargs = {
        'url': url,
        'method': 'POST',
        'headers': headers,
        'data': body,
    }
    if validate_certs == 'no':
        kwargs['validate_certs'] = False

    try:
        response = open_url(**kwargs)
        response_body = response.read().decode('utf-8')
        if response.getcode() in (200, 201):
            return json.loads(response_body)
        else:
            module.fail_json(msg=f"Ошибка создания VM. HTTP {response.getcode()}", response_body=response_body)
    except Exception as e:
        module.fail_json(msg="Ошибка при создании VM, проверьте статус узла кластера", kwargs=kwargs, error=str(e))

def run(base_url, token, state, validate_certs, module):

    vm_name = module.params['name']
    vm_password = module.params['password']
    cluster = module.params['cluster']
    os_id = module.params['os']
    ram_mib = module.params['ram_mib']
    hdd_mib = module.params['hdd_mib']
    cpu_number = module.params['cpu_number']
    ipv4_pool = module.params['ipv4_pool']

    endpoint = f"/vm/v3/host?where=(name+EQ+'{vm_name}')"
    vmi_info = get_vmi_info(base_url, token, endpoint, validate_certs, module).get('size', 0)

    if state == 'present':
        if vmi_info != 0:
            # VM существует, ничего не меняем
            module.exit_json(changed=False, finder=vmi_info)
        else:
            # Создаем VM
            vm_spec = {
                "name": vm_name,
                "password": vm_password,
                "cluster": cluster,
                "os": os_id,
                "ram_mib": ram_mib,
                "hdd_mib": hdd_mib,
                "cpu_number": cpu_number,
                "ipv4_pool": ipv4_pool,
            }
            created_vm = create_vm(base_url, token, vm_spec, validate_certs, module)
            module.exit_json(changed=True, vm=created_vm)
    else:
        # Поддержка state=absent можно сделать позже
        module.exit_json(changed=False, msg="state=absent пока не реализован")

def main():
    module = AnsibleModule(
        argument_spec=dict(
            vmi_domain=dict(required=True, type='str'),
            vmi_first_username=dict(required=True, type='str'),
            vmi_first_password=dict(required=True, type='str'),
            state=dict(required=False, default='present', choices=['present', 'absent']),
            name=dict(required=True, type='str'),
            password=dict(required=True, type='str'),
            cluster=dict(required=True, type='int'),
            os=dict(required=True, type='int'),
            ram_mib=dict(required=True, type='int'),
            hdd_mib=dict(required=True, type='int'),
            cpu_number=dict(required=True, type='int'),
            ipv4_pool=dict(required=True, type='list', elements='int'),
            validate_certs=dict(required=False, type='str'),
        ),
        supports_check_mode=False,
    )

    base_url = f"https://{module.params['vmi_domain']}"
    email = module.params['vmi_first_username']
    password_user = module.params['vmi_first_password']
    state = module.params['state']
    validate_certs = module.params.get('validate_certs')

    # Получаем токен
    token = get_auth_token(
        base_url=base_url,
        email=email,
        password=password_user,
        validate_certs_param=validate_certs,
        module=module
    )
    # Вызов основной функции
    run(base_url, token, state, validate_certs, module)

if __name__ == '__main__':
    main()

Завершаем работу переработкой созданного ранее сценария управления виртуальными машинами vm.yml, заменим ansible.builtin.uri на свой модуль. Прошу не ругать за FQCN: все-таки модули мы не упаковывали в коллекцию и подгрузим их через указанные пути в переменных.

---
- name: Install VMmanager 6 playbook
  hosts: all
  debugger: on_failed
  tasks:
    - name: Get up vm
      vm_info:
        vmi_domain: "{{ ansible_ssh_host }}"
        vmi_first_username: vm@example.com
        vmi_first_password: uA2sW2nT9czV
        name: My_vm
        validate_certs: no

    - name: Создать VM если отсутствует
      vm:
        vmi_domain: "{{ ansible_ssh_host }}"
        vmi_first_username: vm@example.com
        vmi_first_password: uA2sW2nT9czV
        name: My_vm
        password: vmsecret
        cluster: 1
        os: 1
        ram_mib: 512
        hdd_mib: 5000
        cpu_number: 1
        ipv4_pool: [1]
        validate_certs: no
        state: present

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

export ANSIBLE_LIBRARY=/home/my_user/vm6_ansible/plugins/modules
export ANSIBLE_MODULE_UTILS=/home/my_user/vm6_ansible/plugins/module_utils

И протестируем — запустим сценарий дважды, чтобы убедиться, что модуль действительно идемпотентен. Запускаем с помощью команды:

ansible-playbook --private-key /root/.ssh/private_key -i IP, -u root vm.yml

Смотрим результат:

В веб-интерфейсе платформы проверяем — действительно, модуль идемпотентен, создана одна виртуальная машина.

На этом наша работа с Infrastructure as Code, знакомство с платформой VMmanager и VMmanager API завершены. Желаю успехов в экспериментах, а также освоении новых инструментов и технологий.

Комментарии (1)


  1. AntonSurikov
    11.06.2025 14:21

    Интересное решение проблемы с автообновлением при кастомизации конфигов — часто сталкивался с тем, что при ручных правках всё ломается после следующего обновления. Systemd-сервисы для кастомной логики выглядят надёжнее, чем городить свои демоны. Единственное, хотелось бы подробнее про проверку целостности и откат. Например, что именно сравнивается и как хендлится частичное несоответствие. В остальном респект, vmmanager выглядит всё более зрелым решением.