core.cloud
Создание, удаление и расширение облачных VM через CloudDriver-плагин провайдера.
core.cloud создаёт, удаляет и расширяет облачные VM, вызывая CloudDriver-плагин провайдера (soul-cloud-<provider>) через PluginHost (gRPC-over-stdio). Шаг исполняется на самом Keeper-е, диспетчер on: keeper обязателен (иначе ошибка валидации scenario): side-effect-ы — у провайдера (реальный биллинг) и в Postgres-реестрах Keeper-а, файловую систему хоста модуль не трогает. created не идемпотентен (повтор создаёт новые VM), destroyed каскадно обновляет реестры после успешного удаления, resized может вызвать downtime. SID создаваемого хоста = FQDN, который вернул провайдер.
Требования
- Rootне требуется
- Сторона
keeper-side - Коллекция
soulstack.cloud - Категория
cloud
Состояния
core.cloud.created — Запросить у провайдера count VM: на каждую — запись в реестре souls (status: pending) и bootstrap-токен.
Всегда: cloud-create императивен и не идемпотентен на уровне модуля — повтор создаёт новые VM.
Никогда — операция всегда выполняется.
Параметры
| Параметр | Тип | Обяз. / дефолт | Описание |
|---|---|---|---|
provider | string | required | Имя cloud-Provider в реестре (driver + креды + регион). |
profile | map | optional | Параметры VM-профиля (backend-специфично). |
count | int | optional | Сколько VM создать (default 1, >= 1). |
userdata | string | optional | Cloud-init userdata (взаимоисключим с generate_userdata и self_onboard). |
generate_userdata | bool | optional | Сгенерировать userdata из keeper.yml cloud_init (ADR-017(h)). |
name | string | optional | Базовое имя VM-батча (self-onboard Вариант T): keeper предсказывает FQDN=<name>-<index>.<suffix>. Обязателен при self_onboard. |
self_onboard | bool | optional | VM онбордится сама из cloud-init (Вариант T, ADR-017(h)): keeper предсказывает FQDN и запекает per-VM токены в userdata. Требует name + providers.fqdn_suffix. |
Пример — Создать VM через драйвер (spawn опционален через when:-guard)
- name: provision on: keeper when: has(input.spawn) module: core.cloud.created params: provider: "${ input.spawn.provider }" profile: "${ input.spawn.profile }" count: "${ input.spawn.count }"Output
| Поле | Тип | Описание |
|---|---|---|
hosts | array<object> | по записи на VM: {sid, vm_id, primary_ip, attributes?, bootstrap_token} |
count | number | число созданных VM |
vm_ids | array<string> | provider-side ID созданных VM |
action | created |
core.cloud.destroyed — Удалить VM у провайдера (PluginHost.Destroy(vm_ids)); затем cascade одной PG-транзакцией над реестрами для переданных sids.
Провайдер вернул непустой список удалённых VM.
Провайдер вернул пустой список — удалять было нечего.
Параметры
| Параметр | Тип | Обяз. / дефолт | Описание |
|---|---|---|---|
provider | string | required | Имя cloud-Provider в реестре. |
vm_ids | list<string> | required | Список provider-vm-id для уничтожения. |
sids | list<string> | optional | SID-ы для cascade-обновления реестров (souls/seeds/tokens). |
Output
| Поле | Тип | Описание |
|---|---|---|
action | destroyed | |
vm_ids | array<string> | фактически удалённые провайдером VM |
sids | array<string> | эхо переданных sids |
destroyed_n | number | число удалённых VM |
souls_updated / seeds_orphaned / tokens_burned | number | cascade-счётчики (0, если sids не передан) |
core.cloud.resized — Расширить ресурсы существующих VM (cpu / ram / disk в наших единицах) через CloudDriver.Resize. Драйвер инкапсулирует всю последовательность stop/start; реестр souls не трогается.
Хотя бы одна VM фактически изменена. Драйвер без Resizable-capability → resize.unsupported.
Ни одна VM не изменилась — целевые параметры уже достигнуты.
Параметры
| Параметр | Тип | Обяз. / дефолт | Описание |
|---|---|---|---|
provider | string | required | Имя cloud-Provider в реестре. |
vm_ids | list<string> | required | provider-vm-id для resize (один target на весь батч). |
desired | map | required | Целевые ресурсы в наших единицах: cpu_cores (ядра) / ram_mb (МБ) / disk_gb (ГБ). Хотя бы одно > 0; поля с 0 не меняются. |
allow_downtime | bool | optional | Разрешить downtime (stop/start) для cpu/ram-resize. Обязателен при изменении cpu/ram; disk-only online (default false). |
Output
| Поле | Тип | Описание |
|---|---|---|
action | resized | |
results | array<object> | по VM: {vm_id, caused_downtime, error} |
Справочник
Регистрация Cloud-Provider и Cloud-Profile
Чтобы scenario мог сослаться на provider и profile, их сначала регистрируют в реестрах Keeper-а через Operator API (REST / MCP / web-UI) — это runtime-данные в PostgreSQL, не git.
| Реестр | REST | Permission | Audit |
|---|---|---|---|
| Cloud-Provider | GET · POST /v1/providers · GET · DELETE /v1/providers/{name} | provider.read · provider.create · provider.delete | provider.created / provider.deleted |
| Cloud-Profile | GET · POST /v1/profiles · GET · DELETE /v1/profiles/{name} | profile.read · profile.create · profile.delete | profile.created / profile.deleted |
Заметки
- Keeper-side, не трогает хост: side-effect-ы — у cloud-провайдера и в реестрах Keeper-а (Postgres), а не на Soul-хосте. Манифеста required_capabilities у модуля нет — это keeper-internal операция, root/capability хоста неприменимы (доступ — keeper-разрешение оператора, см. secure_defaults).
- created не идемпотентен по конструкции (changed=true всегда): повтор шага создаёт НОВЫЕ VM, а не сверяется с существующими. Это реальный биллинговый side-effect — управляйте повтором guard-ом на уровне scenario (when: / changed_when:), не полагаясь на идемпотентность модуля.
- destroyed — деструктивная cascade: PluginHost.Destroy(vm_ids) физически уничтожает инстансы; затем (при непустом sids) одна PG-транзакция переводит souls → destroyed, активные soul_seeds → orphaned, bootstrap_tokens → burned. Cascade выполняется ПОСЛЕ успешного cloud-destroy — при провале destroy реестры остаются нетронутыми (хост ещё жив у провайдера).
- resized может вызвать downtime: драйвер сам решает, нужен ли stop/start VM ради расширения (Keeper к последовательности агностичен), в output поле caused_downtime на каждую VM сигналит перезагрузку. Реестр souls не трогается (resize не меняет состав душ). Драйвер без Resizable-capability → resize.unsupported.
- SID создаваемого хоста = FQDN, который вернул провайдер (VmInfo.fqdn); VM без fqdn — шаг падает, такую VM нельзя использовать как SID.
- Provider — учётка облака (имя, тип драйвера, credentials_ref = vault-путь; секрет в реестре не резолвится и не хранится в открытом виде). Profile — VM-spec поверх Provider-а (образ, сеть, размер). Удаление Provider с зависимыми Profile-ями → 409 (FK RESTRICT); POST Profile с несуществующим provider → 422.
Безопасность и умолчания
- Keeper-side, не Soul-side: шаг исполняется в процессе Keeper-а (on: keeper), side-effect-ы — у провайдера и в Postgres-реестрах, не на хосте, поэтому root/capability-семантика неприменима. Запуск такого scenario регулируется RBAC оператора (keeper-разрешение); создаваемые записи souls пишутся с CreatedByAID: null (keeper-internal action).
- Реальный финансовый side-effect (created): шаг создаёт настоящие VM у провайдера — это биллинг, и он не идемпотентен (повтор создаёт новые VM, а не сверяется с существующими). Управляйте повтором guard-ом на уровне scenario, не полагаясь на идемпотентность модуля.
- destroyed — деструктивная cascade-операция: физически уничтожает инстансы, затем переводит записи реестров в финальные состояния. Связку sid↔vm_id держит caller — ошибка в ней приведёт к destroy не той VM, поэтому источник vm_ids/sids должен быть доверенным.
- Plain bootstrap-token в register-output (created): hosts[].bootstrap_token — plain одноразовый токен, намеренно в output, потому что cloud-init flow обязан передать его на VM при первичной загрузке (единственный момент, когда plain-токен виден; в БД — только hash, восстановить нельзя). Секретность держит substring-фильтр audit.MaskSecrets (фрагмент token) на ВСЕХ каналах вывода; переименовывать ключ bootstrap_token без проверки фильтра нельзя — иначе one-time token leak.
- Обязательный audit-event cloud.provisioned пишется для всех state-ов; audit-фейл ВАЛИТ шаг (compliance-инвариант — деструктивная/биллинговая операция не должна пройти молча). В audit-payload — provider / vm_ids / sids / cascade-счётчики, но НЕ plain-токены.