Синопсис 📜

Начальной мотивацией для статьи было собрать лайки было зафиксировать определённые эмпирические моменты создания шаблона виртуальной машины. Хабр кажется для этого надёжнее, чем внутренний репозиторий, о котором редко вспоминают. Но что-то пошло не так, и получилось небольшое руководство по обживанию облака VMware Cloud Director. VMware идёт своим путём, в чём-то задавая моду, а в чём-то заставляя рвать волосы, и рассматриваемый продукт совсем не хотелось бы популизировать, однако «если жизнь даёт тебе лимоны»....

Если вы — линуксоид, впервые столкнувшийся с необходимостью работать с упомянутой платформой, это этот текст — для вас. Возможно он будет полезен, даже если вы сталкиваетесь с подходом «Инфраструктура как код» (IaC) впервые. Всем остальным могут быть полезны и интересны какие-то отдельные моменты постольку-поскольку.

Текст старается балансировать между туториалом и повествованием «как дошло до того, до чего дошло». За кадром осталось стремление решать задачи средствами Свободного ПО (не считая самого VMware Cloud Director как данности).

Собственно инфраструктурный код (Terraform и иже с ним) здесь не рассматривается, иначе текст совсем вышел бы из под контроля.

Сведения актуальны для платформы VMware Cloud Director условно версии 10.3+.

Вводная 📽️

Внезапно оказалось, что платформа облачной инфраструктуры VMware Cloud Director (в дальнейшем равнозначно VCD) используется некоторыми российскими вендорами, и предстоит на какое-то время пополнить ряды её пользователей. Веб-интерфейс платформы довольно минималистичен, и потыкавшись вечерок, можно вручную создать небольшую подсеть хостов. Но VMware так же предлагает для VCD одноимённый Terraform-провайдер с открытым кодом, чем хочется воспользоваться. Для подхода «Инфраструктура как код» хорошо бы иметь подготовленные для облака образы необходимых дистрибутивов ОС. И здесь начинается нисхождение во тьму.

В паре слов о веб-интерфейсе «облачного директора», если вы тоже сталкиваетесь с ним впервые.

Главная страница VCD
Главная страница VCD

С большой вероятностью после входа в свою новенькую учётку пользователь попадает на «приборную доску» своей организации (тэнанта). Верхн��е серое навигационное меню сопровождает пользователя на всех экранах. Кстати, в выпадающем списке при клике на имя пользователя с большой вероятностью будет ссылка на документацию. Ниже находятся карточки имеющихся в распоряжении организации виртуальных датацентров — VDC (не путать с VCD). В нашем случае VDC создаёт и настраивает вендор облачных услуг.

Внутри виртуального дата-центра
Внутри виртуального дата-центра

После проваливания по клику на карточке открывается список виртуальных машин и древовидное меню, дающее доступ к различным виртуальным сущностям. Из сущностей, помимо ВМ, в первую очередь интересны Networks (сети) и Edges (шлюзы), заключающие в себе бо́льшую часть конфигурации сетевой топологии. Так же бросается в глаза непонятный пункт vApps в разделе Compute. Это «вирутальные приложения» — за маркетинговым названием стоит понятная и полезная концепция: возможность обособлять несколько ВМ в группу, имеющую отдельную сеть.

ℹ️ На странице ВМ есть кнопка «LAUNCH WEB CONSOLE», открывающая в отдельном окошке экран хоста, обычно с tty. Это худо-бедный способ локального доступа при отсутствии возможности залогиниться по сети. Машина должна быть включена, чтобы кнопка стала доступна.

Последний экран, на который хочется обратить внимание — «Content Hub».

Подвезли контент
Подвезли контент

«Центр содержимого» — это собрание каталогов 🗁, которые могут объединять несколько видов сущностей. Статья касается следующих из них:

  • Media — загрузочные диски в формате ISO-образов 🖬

  • VM App — шаблоны виртуальных машин 🖳

  • vApp Templates — шаблоны тех «виртуальных приложений» 🖧

Причём vApp Templates — это не только заготовки подсетей из нескольких машин. Шаблон отдельной ординарной ВМ привязывается к шаблону виртуального приложения.

ℹ️ Шаблон отдельной ординарной ВМ привязывается к шаблону виртуального приложения.

В поисках образа 📀

На пути к IaC разумным шагом кажется взять готовый образ из библиотеки Content Hub. В моём случае облако содержит несколько вендорских каталогов, судя по содержимому дублирующих друг друга в разных геолокациях. Каталоги зарыблены шаблонами разнообразных дистрибутивов ОС. Однако в массе представлены в лучшем случае предпоследние версии дистрибутивов (причём с пометкой Eng, видимо каталоги — наследие капиталистического Запада).

Был опробован вендорский шаблон AlmaLinux OS 9 версии. И... После загрузки корневой раздел не растёкся на весь выделенный диск. После этого возобладало желание использовать свои базовые образы, в первую очередь свежего излюбленного коллективом Debian и посконного ALT Linux.

Путь образозаводчика начинае��ся с создания каталожика, в веб-интерфейсе это делается кнопкой «NEW» в разделе Catalogs древовидного меню экрана «Content Hub». Этот каталожик, в отличие от вендорских, будет привязан к текущей организации и по-умолчанию доступен только в её области видимости.

ℹ️ В дальнейшем для просмотра содержимого каталога удобнее открывать каталог из раздела Catalogs в левом древовидном меню, чем настраивать фильтры на странице Content.

Дело за малым — наполнить новый приватный каталог свежайшими и сверкающими официальными образами дистрибутивов. К счастью, в эти дни разработчики большинства крупных дистрибутивов GNU/Linux подготавливают образы дисков для облаков.

Немного про облачные образы 🌩️

В чём отличие образа дистрибутива для облака от обычного установочного ISO-образа? Пусть лучше ответит песнятаблица.

Установочный ISO-образ

Облачный образ

Формат файла

Образ оптического накопителя

Образ жёсткого диска

Файловая система

Типичная для оптических дисков (ISO 9660, UDF)

Обычная таблица разделов (GPT, MBR) с ФС общего назначения (Ext4, XFS, возможно LVM)

Содержимое

Программа установки, локальный репозиторий пакетов

Установленная ОС

Кроме того, как известно, компьютерные облака не на небе — это лишь чьи-то компьютеры. Их много, и они разные, поэтому обычно дистростроители публикуют образы для нескольких облаков + некую generic-версию. Большинство таких образов распространяются в формате Qcow2 либо виде «сырого» слепка (RAW, побайтовая копия) установленной операционной системы (+ иногда какие-то вендор-специфичные метаданные в текстовых файлах).

Что объединяет самые разные образы операционок для облаков — им требуется некий механизм инициализации. Некое универсальное API, чтобы облачная хост-система могла выполнить первоначальную настройку. А именно:

  • Создать и настроить пользователей (хотя бы одного начального) и способы авторизации.

  • Поднять сеть, настроить имя хоста.

  • Увеличить размер основного раздела диска до размера виртуального диска.

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

Чуть меньше, чем все облачные образы GNU/Linux содержат для этого настроенный cloud-init.

Немного про cloud-init 😶‍🌫️

cloud-init появился в конце нулевых годов вскоре после рождения подхода «инфраструктура как код». Он представляет собой набор служб и утилит, написанных на Python, и служащих цели однократно настроить в конце процесса загрузки хост, на котором они выполняются.

Основные особенности:

  • Конфигурация на YAML, основной конфиг в /etc/cloud/.

  • Набор модулей для настройки разных аспектов системы. Модули включаются и настраиваются через конфиг.

  • Набор источников данных (datasources). Источники данных так же настраиваются в конфиге и определяют возможность cloud-init считать дополнительные конфигурационные данные «снаружи», чтобы переопределить системные из /etc/cloud/.

Источники данных позволяют хосту-гипервизору параметризовать поднимаемую виртуальную машину. Есть несколько видов данных, передаваемых источникам: network-settings, vendor-data, user-data; для пользователя наиболее интересен последний.

Коротенькая справка по полезным файлам и командам cloud-init:

  • cloud-init clean — удаляет записи о произведённой инициализации. С флагом -r так же выполняет перезагрузку.

  • cloud-init init — выполняет инициализацию. Может отработать не так, как штатная инициализация при загрузке, но для отладки может быть полезно.

  • cloud-id — помогает узнать обнаруженный и выбранный источник данных.

  • /var/log/cloud-init.log, /var/log/cloud-init-output.log (по умолчанию) — журналы выполнения процессов cloud-init при загрузке.

  • /var/lib/cloud/instances/ — содержимое позволяет получить представление, какие источники данных отработали. В поддиректориях сохраняются все полученные конфиги.

Официальная документация.

Обратно к VCD 🤖

К несчастью, VMware Cloud Director не работает ни с Qcow2-образами, ни с RAW. Только с собственными (открытыми) форматами OVA и OVF. Файл OVF — это файл метаданных в XML-формате, который обычно сопровождает VMDK-образ жёсткого диска. А OVA-файл — это tar-архив c OVF, VMDK и файлом контрольных сумм. Сказанное может не быть полностью верно для любых OVA/OVF, но в рамках «директора облаков» остановимся на этом представлении.

Заливку шаблона OVA можно произвести через веб-интерфейс из раздела Content экрана Content Hub по нажатию кнопки ADD цвета морской волны. Любопытно, что можно даже указать URL вместо того, чтобы скачивать и перезаливать файл. После загрузки шаблон будет некоторое время в обработке. C ISO-образами, которые в системе имеют тип Media, у меня порой операция выполняется долго и иногда заканчивается неработоспособным образом.

Множество дистрибутивов с официальными облачными образами в формате OVA/OVF меньше множества дистрибутивов с официальными облачными образами, например:

*) ещё бы, ведь cloud-init — детище Canonical

**) Данная статья в принципе даёт направление, куда копать ⚒️

И так, у нас есть не-OVA-образы, и нам нужно получить OVA. Что же делать?

«Ответ очевиден. Но неверен.»

Старый тред на Reddit гласит, что нет ПО, конвертирующего Qcow2-образы в OVA-файлы. Можно сконвертировать Qcow2 в VMDK, но дальше нужно будет добавить пачку метаданных, чтобы это стало понятным VMware шаблоном ВМ. Последний участник в треде подсказывает, что можно залить сконвертированный VMDK в продукт VMware, создать ВМ, затем экспортировать шаблон. Проблема в том, что напрямую залить VMDK в VCD не получится. А обходные пути на этом этапе ещё кажутся слишком кривыми.

ℹ️ VirtualBox умеет работать с OVA-файлами. Подойдёт ли полученный таким путём файл для VCD, не проверялось.

OVA — всё же открытый формат, и относительно несложный. На GitHub обнаружился репозиторий debian-ova-creator. Он содежит Bash-скрипт, выкачивающий образ Debian указанной версии, конвертирующий его в VMDK, обмазывающий необходимой метадатой и упаковывающий в желанный OVA-файл. Содержимое OVF-файла с минимальными правками взято из образа Ubuntu. В теории несложно адаптировать скрипт для совместимого образа любого дистрибутива.

Скрипт был испробован, полученный OVA-файл был залит в каталог. Если в веб-интерфейсе создать из него виртуальную машину, и провалиться в неё кликом мыши, то среди пунктов меню настройки ВМ будут два, непосредственно связанных с параметрами инициализации — это «Guest OS Customization» и «Guest Properties».

Кастомизация гостевой ОС
Кастомизация ��остевой ОС

Раздел «Guest OS Customization» — VCD-специфичный, там всегда доступен один и тот же набор опций. Не будем их перечислять, а просто сошлёмся на документацию. Опции отсюда применяются через «гостевое дополнение» open-vm-tools (пакет обычно так и называется). Так же существует источник данных cloud-init под названием VMware, который, будучи настроенным, может получать конфигурацию через open-vm-tools. Однако мы в итоге будет использовать другой источник. Как передать необходимые параметры через веб-интерфес VCD, осталось невыясненным (возможно никак), через Terraform-провайдер предположительно это можно сделать через добавленное в прошлом 2024 году поле vcd_vapp_vm.set_extra_config, см. документацию к источнику VMware и запрос #822.

ℹ️ Связка Guest OS Customization с open-vm-tools полезна возможностью задать пароль пользователя root при невозможности сделать это через cloud-init, чтобы получить доступ к машине по крайней мере через графическую консоль.

Свойства «гостя»
Свойства «гостя»

Содержимое следующего раздела с очень близким к предыдущему названием «Guest Properties» зависит от OVF-файла, запакованного в OVA-шаблон. В данном случае это те же опции, что прописаны в Ubuntu'вском шаблоне. Именно через Guest Properties мы будем в итоге передавать конфиг для cloud-init.

Но пока что фокус не удаётся — не смотря на обнадёживающий вид, не удаётся залогиниться с ключом, пара которого передана в «ssh public keys», и на данном этапе следствие в тупике. Остаются крайние меры — попробовать создать работоспособный шаблон руками, держа официальный шаблон Ubuntu за образец. Но сперва...

Ликбез по сетевым вопросам 🕸️

Допустим, у нас на руках виртуальный датацентр (VDC) с одной сетью для виртуальных машин и vApp'ов, сеть подключена к Edge (шлюзу, говоря по-простому), и на шлюз вендором облака выдан какой-никакой пул публичных IP-адресов. Чтобы соединить сеть нашего воображаемого датацентра с миром, требуется несколько действий. Так же все эти настройки можно выполнить через Terraform-провайдер, но это следующий уровень страданий.

Во-первых неплохо бы разобраться с адресацией в нашем VDC. Для этого следует:

  • Перейти в подраздел «Networks» раздела «Networking» бокового древовидного меню VDC. Выбрать сеть VDC (вероятно она есть и пока только одна, но это не точно).

  • Посмотреть адрес шлюза «Gateway CIDR». Если он 192.168.1.1/24, соответственно подсеть «по умолчанию» 192.168.1.0/24.

  • Подсмотреть имя Edge Gateway в поле Connected To.

  • Настроить по желанию «Static IP Pools» и «DHCP». Моё предпочтение — DHCP, однако сетевики старой школы плюются и пишут адреса в синий блокнот. Следует учитывать, что VCD позволяет перекрываться статическим арендам IPv4 Bindings с пулами статических адресов, однако не позволяет перекрываться статическим арендам с пулами DHCP.

Во-вторых если мы желаем, чтобы наши ВМ могли ходить в Интернет за обновлениями и т. п., следует:

  • Перейти в подраздел «Edges» раздела «Networking» бокового древовидного меню VDC. Выбрать Edge Gateway, который привязан к сети VDC (подсмотрен на прошлом этапе, вероятно он вообще один в списке).

  • В разделе «Services» выбранного эджа́ перейти в подраздел «NAT» и добавить правило со следующими параметрами. Name по велению сердца, NAT_Action=SNAT, Internal IP равен существующей подсети (типа 192.168.1.0/24), а External IP назначается при помощи выпадающего списка из выделенного вендором пула. Если вы не желаете пока возиться с Firewall, то в «Advanced Settings» задайте Firewall Match=Bypass, иначе оставьте значение Match Internal Address.

  • [ Если выбрано повозиться с Firewall ] Там же перейти в подраздел «Firewall» и добавить над правилом по умолчанию соответствующее разрешающее правило. Action=Allow; Source -> Firewall IP Addresses, добавить ту же подсеть (либо сначала создать IP Set, зетем назначить его в Source -> Firewall Groups). Destination поставить переключатель в Any Destination.

Далее чтобы подключаться к хостам через Интернет, требуется «пробросить порты». Когда в распоряжении нет большого пула дешёвых адресов, можно сделать специальный хост-бастион🏯. С бастиона пробрасываются необходимые порты на публичный адрес, затем он используется как ProxyJump для получения доступа ко всем прочим хостам по SSH. И/или на бастионе поднимается VPN с ip_forward, разрешённым во внутреннюю сеть. И по необходимости обратный HTTP-прокси. И т. д. В общем случае проброс порта делается так:

  • Переходим в подраздел NAT как на прошлом шаге.

  • Создаём правило. Name произвольно, NAT Action=DNAT, External IP вбиваем тот же, что использовали на прошлом шаге для SNAT, Internal IP — адрес хоста в частной сети, с которого пробрасываем порт, External Port — порт, который будет открыт на External IP, Application — определяет порт на Internal IP, который будет выведен на <External IP>:<External Port>. В случае последнего параметра придётся пошерсти́ть таблицу в поисках необходимой комбинации, так же свои Application Port Profiles можно добавить в одноимённом подразделе раздела «Security» в древовидном меню шлюза. Так же история с Firewall Match, как в списке выше.

  • [ Если выбрано повозиться с Firewall ] Там же перейти в подраздел «Firewall» и выше правила по умолчанию соответствующее разрешающее правило. Name произвольно, лучше чтобы ассоциировалось с правилом NAT. Application как в прошлом шаге, Action=Allow, Source=Any, Destination указывает на Internal IP из прошлого шага.

Теперь мы мало-мальски раскидались с сетью и можем продолжать эксперименты.

Шаблон ВМ: переходим к плану C (custom) 🚲

И так, решено сделать рабочий шаблон ВМ руками. Делался шаблон Debian 13, но для других дистрибутивов GNU/Linux схема такая же, да наверное и для BSD не особо отличается, разница в установщиках и командах пакетных менеджеров.

К данному моменту в распоряжении имеется локальная сеть с доступом в интернет и хост-бастион для доступа по SSH.

1. Загружаем установочный образ Debian netinst в каталог содержимого нашего VDC. Была попытка загрузить полновесный установочный образ DVD, он очень долго висел в обработке, затем был помечен как неисправный. Повторять опыт не захотелось.

2. Создаём ВМ, подключая загруженный образ. Поскольку Debian 13 отсутствует в многообразии известных VCD систем, выбираем Other 5.x or later Linux (64-bit). Диск 10 ГБ. Можно даже 2 ГБ, главное, чтобы базовая система встала, при развёртывании шаблона диск будет увеличиваться. Boot Firmware=EFI (хотя старый добрый BIOS тоже будет работать). CPU и RAM всегда можно подредактировать в любую сторону, так что не критично. Достаточно 2 ядра, 2 гига.

3. Переходим на страницу машины, ожидаем, когда она поднимется, открываем веб-консоль, запускаем установку.

4. На этапе создания пользователей достаточно самой примитивной конфигурации: пользователь user с паролем password (если только вы не вывели 22 порт новой машины напрямую наружу). Это временная настройка.

5. Разметить диск следует вручную. Если выбран EFI на прошлом шаге, то таблица разделов GPT и небольшой, например 128 МБ, раздел EFI; за ним корневой раздел / с ФС Ext4 до конца диска (или XFC, она выделяет иноды динамически, но её нельзя так просто взять и уме��ьшить). Если своп потребуется кровь из носу — всегда можно создать своп-файл, во всяком случае пусть корневой раздел идёт последним, тогда не возникнет проблем при его расширении. Так же имеет смысл выбрать на экране разметки параметр «Обычное использование» («Typical usage») для корневого раздела news вместо standard, что приведёт к выделению в 4 раза большего количества инод на единицу дискового пространства. Это несколько отъедает место на диске, но предупреждает ситуацию, когда условный Docker расходует все дескрипторы, всё выглядит так, как будто на диске кончилось место (хотя оно не кончилось).

6. Выбор зеркал: имеет смысл выбрать более географически близкое зеркало. 🌐

7. Выбор состава системы: предпочтите абсолютный минимум пакетов, чтобы минимизировать образ диска в шаблоне. Достаточно выбрать только SSH-сервер.

8. [ Опционально ] После установки сохраняем снимок (Snapshot) машины со свеженькой ОС, чтобы откатиться в случае проблем, а если такая функция не доступна, создаём временный шаблон. Для создания шаблона нужно выключить ВМ (либо хотя бы извлечь Media). Затем на странице ВМ кликнуть выпадающий список «ALL ACTIONS» и в нём пункт «Create Template». Затем сохранить шаблон под понятным именем в каталоге VDC.

9. Загружаем свежеустановленную ВМ и логинимся с правами суперпользователя. Т. е. в случае Debian либо заходим через веб-консоль от root, либо, если доступ снаружи уже организован, заходим от обычного пользователя и выполняем su с паролем root.

10. Обновляем индекс пакетов apt update, устанавливаем необходимый минимум: apt install --yes cloud-init open-vm-tools, пакет cloud-guest-utils должен подтянуться как рекомендованный.

11. [ Опционально ] Настриваем Netplan.

В Debian службы cloud-init включаются автоматически в конце установки и в /etc/cloud/ находится конфиг с разумными умолчаниями. Так что можно делать шаблон и проверять его работоспособность. И действительно, как показала практика, на ВМ, развёрнутой с такого шаблона, применяются Guest Properties, а корневой раздел увеличивается размера виртуального диска (в любое время, а не только при первой загрузке)! Однако — спойлер — не применяются переданные для cloud-init параметры, так что остаётся ломать голову дальше.

⚠️ На данном этапе в системе ещё существуют УЗ с начальными паролями. Не используйте такую систему для продуктовых развёртываний.

Шаблон ВМ: роем вглубь 🪏

Время раскрыть интригу, как вообще предполагается параметризовать cloud-init в VCD, ведь веб-интерфейс не даёт очевидных подсказок. Рассмотрение работы с Terraform-провайдером VCD не входит в этот текст (а надежда теплится, что будет повод и для статьи об IaC через провайдер), так что проговорим факультативно.

Ответ о передаче настроек cloud-init дала статья Мэтта Элиотта (кто бы тот ни был, да будут благословенны дни его). Статья показалась не точной в деталях, но обстоятельной, а самое главное — в формате расследования она выходит на параметр guest_properties ресурса vcd_vapp_vm (и его младшего родственника vcd_vm), что так же отсылает нас к соответствующему разделу в веб-интерфейсе. Чтобы передать user-data для cloud-init, нужно в поле user-data объекта vcd_vapp_vm.guest_properties передать Base64-кодированный текст конфига. Переданные данные будут отображаться на страничке ВМ в разделе «Guest Properties». Аналогично передаются другие свойства, перечисленные в descriptor.ovf официального образа Ubuntu, это: instance-id, hostname, seedfrom, public-keys, password, а так же network-config (однако нам не потребуется это усложнение, пока мы полагаемся на DHCP).

Изучение журнала cloud-init или команда cloud-id показывают, что при включенной «Guest OS Customization» -> Enable guest customization cloud-init использует источник данных VMware, а при выключенной опции — возвращается к None (который не позволит настроить хост снаружи). А вот вызов cloud-id на работоспособном официальном образе Ubuntu указывает на источник данных OVF. Изучение /etc/cloud/ эталонной убунты не даёт полезных открытий. А вот внимательное (от отчаяния) изучение diff между descriptor.ovf эталонного образа и созданной нами ВМ обнаруживает занимательную разницу:

...
<ovf:VirtualSystem ovf:id="ubuntu-noble-24.04-cloudimg-20251113">
    <ovf:VirtualHardwareSection ovf:transport="iso">
      ...

Аттрибут ovf:transport элемента ovf:VirtualHardwareSection равен iso, тогда как в созданной руками ВМ этот аттрибут равен пустой строке.

Обратить внимание на эту разницу помогает документация к источнику данных OVF, в шапке упоминающая «ISO transport», да и статья Мэтта Элиотта кратко упоминает ISO-образ для этого источника (с предположением, что образ придётся делать вручную, но это не так).

ℹ️ Вспомним, что OVA — это просто тарболл, он распаковывается и запаковывается командами tar -xaf ФАЙЛ.ova и tar caf НОВЫЙ_ФАЙЛ.ova соответственно.

И вот, добавив вручную значение ovf:transport="iso" и запаковав OVA обратно, мы получаем наконец работоспособный шаблон виртуальной машины с cloud-init. 🥲

Тут же не откладывая можно украсить наш шаблон опциями страницы «Guest Properties». Они прописываются в разделе VirtualSystem.ProductSection descriptor.ovf, значения можно взять из образа Ubuntu либо из примера OVF-файла в документации источника данных OVF. Без этого опции можно будет передать через Terraform-провайдер, но на странице «Guest Properties» будет безнадёжно пусто.

⚠️ Не забываем — чтобы держать инженера в тонусе, user-data должна быть Base64-кодирована перед вставкой в поле! Для этого достаточно вызывать base64 ФАЙЛ_USER-DATA.yaml

⚠️ В свою очередь YAML-файл с user-data должен начинаться со специального комметария #cloud-config, иначе cloud-init его просто проигнорирует!

Теперь можно резюмировать необходимые действия, дополнив пункты по созданию шаблона ВМ необходимым колдунством:

11. Возвращаемся в настраиваемую виртуалку с установленным cloud-init и добавляем небольшую кастомизацию /etc/cloud/cloud.d/90_vmc.cfg:

# Это особенное значение, оно должно указываться только так, в одну строку.
# Порядок важен и соответствует порядку поиска ресурсов в списке.
# Сужает список источников данных, которые система будет искать при старте.
  datasource_list: [ NoCloud, VMware, OVF, None ]
  
  # Не отключать возможность настройки через «Guest OS Customization».
  disable_vmware_customization: false
  
  # vi: syn=yaml

После этого не будет лишним проверить, что по крайней мере a. хост стартует без ошибок cloud-init, b. «Guest OS Customization» работают (в частности позволяют сбросить пароль root).

ℹ️ При изменении «Guest OS Customization» предпочтите «Power On, Force Recustomization» из меню «ALL ACTIONS» -> «Power».

12. Помолясь зачищаем хост и заряжаем на профит на инициализацию. Для этого уместно разместить скрипт где-нибудь в /root/, где он не мешает, чтобы упростить себе работу при обновлении шаблона:

#! /bin/bash
set -e  # Падать при первом return != 0,

# В переменных окружения может не быть админских путей
# (зависит от способа получения оболочки root).
export PATH="$PATH:/usr/sbin/:/sbin/"

# Следующий низкоизящный блок получает массив всех обычных пользователей.
# Можно заменить на обычный массив, если список гарантированно известен.
uid_min=$(awk '/^UID_MIN/ {print $2}' /etc/login.defs)
uid_max=$(awk '/^UID_MAX/ {print $2}' /etc/login.defs)
mapfile -t users < <(awk -F: -v min="$uid_min" -v max="$uid_max" \
    '($3>=min  && $3<=max){print $1}' /etc/passwd)

# Удаляем всех обычных (не системных) пользователей.
for user in "${users[@]}"; do
  userdel -rf "$user"
done

# А так же блокируем вход суперпользователя через PAM.
passwd --lock --delete root

# Чистим историю комманд, кэш пакетного менеджера,
# системные журналы.
rm -f /root/.bash_history
rm -rf /var/lib/apt/* /var/cache/apt/
journalctl --vacuum-time=1s

# Удаляем существующие конфигурации ifupdown и Netplan.
rm -f /etc/networking/interfaces.d/*
rm -f /etc/netplan/*

# Взводим cloud-init, удаляя данные прошедшей инициализации.
cloud-init clean --logs

echo
echo '___'
echo Remember to run 'history -c'

Кладём куда-нибудь в /root/cloud-init-prepare.sh и делаем chmod a+x /root/cloud-init-prepare.sh. Затем вызываем скрипт и для проформы чистим историю текущего шелла: history -c. После чего гасим хост.

13. Создаём из подготовленной ВМ шаблон. Неплохая идея использовать в имени текущую дату (как делают разработчики дистрибутивов). Так же имя будет временным, так что можно назвать как-нибудь вроде Debian 13 Custom (20151215) PRE. Затем находим созданный шаблон в каталоге организации, открываем и нажимаем кнопку «DOWNLOAD» в верхнем ряду. VCD какое-то время будет заниматься упаковкой, затем отдаст условный Debian_13_Custom_(20151215)_PRE.ova браузеру для скачивания (будем использовать это имя для примера дальше). Смотрим, и радуемся, какой компактный шаблончик у нас вышел.

14. Распаковываем OVA:

mkdir ova
tar xvf 'Debian_13_Custom_(20151215)_PRE.ova' --directory=ova/
cd ova

Редактируем файл descriptor.ovf: находим аттрибут ovf:transport элемента ovf:VirtualHardwareSection и прописываем ему значение ovf:transport="iso" . А так же, если планируем передавать параметры cloud-init из веб-интерфейса, добавляем соответствующие элементы Property в элемент ProductSection.

По завершении редактирования пакуем новый OVA:

tar cvf '../Debian_13_Custom_(20151215).ova'
cd ..

Но кто из нас любит выполнять рутину руками, поднимите руку. Поэтому был подготовлен небольшой оверинжиниринг (под катом). Скрипт перепаковывает скачанный OVA-файл, добавляя куда следует (если повезёт) ovf:transport="iso" и два доступых для редактирования на странице «Guest Properties» свойства — user-data и network-config. Для запуска нужен Python>=3.9. См. использование python3 ova_cloud_init_patch.py --help.

ova_cloud_init_patch.py
#! /usr/bin/env python3
"""
Patch OVA file for VCD to make cloud-init OVF datasource work

Usage:
    python3 ova_cloud_init_patch.py --help

Requires:
    - python>=3.9
"""

import argparse
import dataclasses
import sys
import tarfile
import tempfile

from pathlib import Path
from xml.etree import ElementTree

@dataclasses.dataclass
class Property:
    _as_element = ('label', 'description')
    key: str
    type: str = 'string'
    userConfigurable: str = 'true'
    value: str = ''  # Default value
    password: str = 'false'
    label: str = '' # Short human-readable name
    description: str = '' # Human-readable description

    def to_elem(self) -> ElementTree.Element:
        dic = dataclasses.asdict(self)

        if not dic['label']:
            dic['label'] = self.key

        attr_dict = {f'ovf:{k}': val for k, val in dic.items() if k not in self._as_element}
        prop_el = ElementTree.Element('ovf:Property', attrib=attr_dict)

        for el_name in self._as_element:
            val = dic[el_name]
            elem = ElementTree.Element(f'ovf:{el_name.capitalize()}')
            elem.text = val
            prop_el.append(elem)
        return prop_el

# On incorrect properties VCD may fire error on uploading
PROPERTIES = [
    Property(
        key='user-data', label='Encoded user-data',
        description='Base64 encoded configuration for cloud-init. Pass with OVF datasource.'),
    Property(
        key='network-config', label='Encoded network-config',
        description='Optional Base64 encoded network configuration for cloud-init. Pass with OVF datasource.'),
]


NamespacesType = dict[str, str]


def get_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            'Patch OVA file for VMware Cloud Director to enable cloud-init'
            ' OVF datasource initialization with "Guest Properties"'
        ),
    )
    parser.add_argument(
        '-i', '--input',
        type=Path,
        help='Source OVA file',
        required=True,)
    parser.add_argument(
        '-o', '--output',
        type=Path,
        help='Output OVA file',
        required=True,)
    parser.add_argument(
        '--break',
        default=False,
        help='Wait for input before pack an output OVA file',
        action=argparse.BooleanOptionalAction,)
    return parser.parse_args()


def get_xmlns(path: Path) -> NamespacesType:
    iter = ElementTree.iterparse(path, events=['start-ns'])
    return {node[0]: node[1] for _, node in iter}


def patch_transport(root: ElementTree.Element, xmlns: NamespacesType) -> None:
    vhs_elem = root.find('.//ovf:VirtualHardwareSection', namespaces=xmlns)
    assert vhs_elem is not None, 'No VirtualHardwareSection tag found!'
    ovf_uri = xmlns['ovf']
    transport_attr = f'{{{ovf_uri}}}transport'
    vhs_elem.set(transport_attr, 'iso')
    print('Transport has been set to "iso"')


def add_properties(root: ElementTree.Element, xmlns: NamespacesType) -> None:
    vs_elem = root.find('.//ovf:VirtualSystem', namespaces=xmlns)
    assert vs_elem is not None, 'No VirtualSystem tag found!'

    ps_tag = 'ovf:ProductSection'
    ps_elem = vs_elem.find(ps_tag, namespaces=xmlns)
    if ps_elem is None:
        ps_elem = ElementTree.Element(
            ps_tag,
            attrib={
                # Empty fields are required
                'ovf:class': '',
                'ovf:instance': '',
                'ovf:required': 'false',
            })
        vs_elem.append(ps_elem)

    prop_keys = {p.key for p in PROPERTIES}
    rm_list = []
    ovf_uri = xmlns['ovf']
    key_attr = f'{{{ovf_uri}}}key'
    for prop_el in ps_elem.findall('ovf:Property', namespaces=xmlns):
        if prop_el.get(key_attr) in prop_keys:
            rm_list.append(prop_el)

    for prop_el in rm_list:
        key = prop_el.get(key_attr)
        print(f'Deleting existing propety "{key}"')
        ps_elem.remove(prop_el)

    for prop in PROPERTIES:
        print(f'Adding property "{prop.key}"')
        ps_elem.append(prop.to_elem())


def main() -> None:
    args = get_args()

    output: Path = args.output
    if args.output.exists():
        print(f'\nPath {output} exists, please try other --output\n', file=sys.stderr)
        sys.exit(1)

    with tempfile.TemporaryDirectory() as tmp_dir:
        tmp_path = Path(tmp_dir)

        print(f'Unpacking to {tmp_dir}')
        with tarfile.open(args.input, 'r') as tar_f:
            tar_f.extractall(path=tmp_dir)

        descriptor_path = tmp_path / 'descriptor.ovf'
        xmlns = get_xmlns(descriptor_path)
        for k, val in xmlns.items():
            ElementTree.register_namespace(k, val)

        tree = ElementTree.parse(descriptor_path)
        root = tree.getroot()

        patch_transport(root, xmlns=xmlns)
        add_properties(root, xmlns=xmlns)

        ElementTree.indent(tree, space=' ' * 4)
        tree.write(descriptor_path, xml_declaration=True, encoding='UTF-8')

        if getattr(args, 'break'):
            print(f'\nPrepared OVA content is in {tmp_dir}')
            print(f'It is possible now to make additional customizations')
            input('Press <Return> to continue')

        output.parent.mkdir(parents=True, exist_ok=True)
        print(f'Writing {output}')
        with tarfile.open(output, 'x') as tar_f:
            tar_f.format = tarfile.USTAR_FORMAT  # Avoid PAX header which is unknown for VCD
            for path in sorted(tmp_path.glob('*')):
                arcname = path.relative_to(tmp_dir)
                tar_f.add(path, arcname=arcname)

        print(f'Deleting {tmp_dir}')


if __name__ == '__main__':
    main()

ℹ️ Такое локальное редактирование требуется лишь раз на шаблон (в случае успеха конечно). В дальнейшем можно раскатывать машину из шаблона, обновлять и конфигурировать, и затем создавать новый шаблон.

Ещё тёпленький OVA заливаем штатным образом в каталог организации по пункту «ADD» -> «OVA/OVF» на странице «Content Hub» -> «Content». Не забываем поправить предлагаемое имя шаблона, чтобы оно не перекрывалось с существующими!

15. Теперь точно конец инструкции. Осталось создать новую ВМ из залитого шаблона через Terraform-провайдер или веб-интерфейс и проверить, что всё работает. Помним, что YAML user-data передаётся в виде base64-кодированной строки.

[ Приложение ] Когда есть план. Netplan!

Netplan — это интерфейс настройки сети через YAML-конфиги. Он, так сказать, cloud-ready и близко дружит как с cloud-init, так и с Systemd. Можно перевести шаблон Debian с родных скриптов на рельсы Netplan. Это не обязательно, так как cloud-init создаёт конфиг в дебиановском /etc/network/interfaces.d, однако может избавить от некоторых проблем, в т. ч. горящий красным по своей прихоти сервис networking. Например, ошибкой ifup No DHCPv6 client software found! при том, что двустечный dhcpd установлен по-умолчанию.

И так, если решились, то устанавливаем пакеты (работаем от root):

apt install --yes netplan.io

Включаем управлялку сетью Systemd, которая будет бэкендом для netplan, и гасим дебиановскую службу по-умолчанию:

systemctl enable --now systemd-networkd
systemctl disable --now networking

Следующие три команды опциональны, и нужны, чтобы не потерять сеть после ребута и до нициализации. Во время инициализации cloud-init создаст свой конфиг.

ENABLE_TEST_COMMANDS=1 netplan migrate
chmod 600 /etc/netplan/*
netplan try  # Тут попросят нажать забой, если всё хорошо

Удаляем родную дебиановскую скриптоту:

apt purge ifupdown
rm -rf /etc/network/  # Прям вот так вот, да

После этого можно выполнять reboot и проверять, всё ли в порядке (а если миграция конфигов не выполнялась, то выполнить на всякий противопожарный cloud clean -r вместо reboot).

Аттрибуция статье Педро Родригеса.

ℹ️ Иногда (особенно в случае неполадки) требуется загрузить хост с другого носителя. Найти пункт меню «ALL ACTIONS» -> «Media» -> «Insert Media» несложно. Но как выбрать альтернативный носитель для загрузки при использовании веб-консоли? Способы есть.

Во-первых на загруженной системе можно вызывать команду systemctl reboot --firmware-setup (предположительно только для EFI-машин.

Во вторых в меню GRUB может быть равносильный пункт «UEFI Firmware Settings» (для EFI машин).

В третьих работает старое доброе тапанье Delete в момент перезагрузки до того, как запустится загрузчик.

Не забываем выполнить «Eject Media» после всех операций, иначе конфиг для cloud-init не попадёт в /dev/sr0.

Сухой остаток 🌵

  • Мы познакомились с веб-интерфейсом VMware Cloud Director.

  • Мы немного познакомились с устройстом сети на платформе.

  • Мы узнали кое-что про облачные образы.

  • Мы НЕ коснулись каких-либо дополнительных инструментов для управления облаком.

  • Остался простор для экспериментов с образами.


2025 Антон «bergentroll» Карманов.

Данный текст и изображения доступны на условиях CC0 1.0 Universal (общественное достояние).