Привет, Хабр! Статья будет посвящена любимому мной IaC. Чтобы ввести в курс дела, кратко расскажу про VMmanager и текущую реализацию продукта. Затронем варианты, как можно работать с VMmanager с подходом Infrastructure as Code, а основная часть — про развертывание платформы VMmanager и управление виртуальными машинами в ней с помощью Ansible.
О платформе VMmanager
Наверняка вы уже знаете, что такое VMmanager. Это платформа управления виртуализацией. Она имеет версии VMmanager Hosting и VMmanager Infrastructure — под разные цели использования.
Версия | Hosting | Infrastructure |
Назначение | — ориентирована на потребности хостинг-провайдеров | — ориентирована на потребности владельцев IT-инфраструктур |
Состав | — базовая функциональность платформы | — базовая функциональность платформы |
Дополнительно | — | — регистрация в Едином реестре российских программ для электронных вычислительных машин и баз данных |
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.
Переходим к установке платформы.
Установим Ansible на ПК, с которого будет запускаться установка платформы. Порядок установки в официальной документации Ansible.
На ПК с Ansible:
Если на ПК не установлена утилита curl, установите ее:
dnf install curl || apt install curlСкачайте сценарии установки:
curlhttps://download.ispsystem.com/extras/ansible/vmmanager6_common.tar.gzСоздайте SSH-ключ и скопируйте его на сервер платформы. Подробнее — в статье про SSH-протокол.
Создадим директорию для сценариев установки:
mkdir vm6_ansibleРаспакуем в директорию архив со сценариями:
tar xzf vmmanager6_common.tar.gz -C vm6_ansible/Перейд��м в созданную директорию:
cd vm6_ansibleУкажем параметры установки в секции 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"
Мы не планируем подключать SSL-сертификат, закомментируем в файле vmmanager6.yml строку:
# - include_tasks: cert.ymlРезервное копирование платформы мы также не планируем, закомментируем в файле vmmanager6.yml строку:
# - include_tasks: backup.ymlЗапустим установку:
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 завершены. Желаю успехов в экспериментах, а также освоении новых инструментов и технологий.
