12 KiB
forge-tools-proxmox
MCP-сервер для гипервизора Proxmox VE (MCP по stdio) — управление
кластером/нодами, QEMU-ВМ, LXC-контейнерами, снапшотами, бэкапами, задачами,
хранилищами и сетью. Зеркало архитектуры forge-tools/ssh + postgres:
stdio-only, одиночный потокобезопасный Manager, ручные JSON-Schema,
--health, graceful shutdown, isolation=pooled (per-agent конфиг приходит
на каждый вызов через _tenant_config).
Границы (принцип «дополнять, а не дублировать»). Домен — только гипервизор. Запуск кода внутри гостя — не здесь: это
ssh__run(remote exec); локальные файлы —filesystem; БД —postgres. Управление Ceph/ZFS/SDN/firewall/HA/ACL и восстановление бэкапов — кандидаты в отдельные узкиеforge-tools(см. «Roadmap»).
Инструменты (54)
| Домен | Инструменты |
|---|---|
| cluster (4) | clusters_list, cluster_status, nodes_list, node_status (read) |
| node (1) | node_network (read) |
| VM (13) | vms_list, vm_describe, vm_config, vm_next_id (read) · vm_start, vm_stop, vm_reboot, vm_shutdown, vm_suspend, vm_resume, vm_clone, vm_delete, vm_convert_template (мутации) |
| VM config/disk/net (10) | vm_config_update, vm_resize_disk, vm_add_disk, vm_remove_disk, vm_move_disk, vm_add_network, vm_update_network, vm_remove_network, vm_set_cloudinit (мутации) |
| LXC (12) | containers_list, container_describe, container_config (read) · container_start, container_stop, container_reboot, container_shutdown, container_clone, container_delete, container_config_update, container_resize (мутации) |
| snapshot (4) | snapshot_list (read) · snapshot_create, snapshot_delete, snapshot_rollback (мутации) |
| backup (2) | backup_list (read) · backup_create (async UPID) |
| task (4) | tasks_list, task_status (+wait), task_log (read) · task_stop (мутация) |
| guest (1) | guest_ips (read, QEMU guest-agent) |
| storage (4) | storage_list, storage_status, templates_list, isos_list (read) |
Конфигурация
Путь per-agent pve.json (pooled → на каждый вызов ядро инжектит
_tenant_config); либо статический -config для одиночного режима.
{
"hosts": [
{ "alias": "pve1",
"url": "https://10.0.0.10:8006/api2/json",
"token_id": "root@pam!forge-dev",
"token_secret": "${PROXMOX_TOKEN_SECRET}", // из env / gitignored *.local.json
"ca_file": "${PROXMOX_CA_FILE}", // предпочтительно; insecure=true только for dev-lab
"insecure": false,
"allow_nodes": ["pve1"],
"allow_vmids": [500] }
],
"default": "pve1",
"read_only": true, // false включает мутации (в доп. к confirm+approval)
"timeout_sec": 15,
"task_poll_max_sec": 600,
"max_output_bytes": 4194304
}
- Секреты никогда в git:
token_secret/пароль — только${VAR}или gitignored*.local.json. В репозитории хранится лишь шаблонpve.json.example; рабочиеpve.json/config.jsonи*.local.jsonзакрыты локальным.gitignore. read_only(дефолтtrue) иallow_nodes/allow_vmids(per-host) — fail-closed: без явныхallow_vmidsмутации недостижимы. Это второй слой поверх least-privilege токена.ca_file— кастомный CA для self-signed кластера;insecure— только для локального dev-lab.- Live-reload через
configreload: правкиpve.jsonприменяются без рестарта (по контент-хэшу).
Переменные окружения
| Переменная | Назначение |
|---|---|
FORGE_TENANT_CONFIG |
Каталог с per-agent pve.json (режим isolation=pooled). |
PROXMOX_HOST |
Адрес гипервизора (подставляется в url). |
PROXMOX_TOKEN_ID |
ID API-токена PVE, напр. root@pam!forge-dev. |
PROXMOX_TOKEN_SECRET |
Секрет API-токена. |
PROXMOX_CA_FILE |
Путь к кастомному CA self-signed кластера. |
Значения в конфиге поддерживают подстановку ${VAR} из окружения — реальные
адреса и токены в репозиторий не попадают.
Несколько гипервизоров и коллизии VMID
Каждый hosts[] — отдельный кластер со своими кредами/TLS и своими
правами. VMID/ноды на разных гипервизорах могут пересекаться, поэтому
идентичность ресурса — всегда (host, node, vmid), а не «голый vmid».
hostвыбирает целевой гипервизор (aliасс, дефолт =default).allow_vmids/allow_nodes— per-host: права изолированы между кластерами. Одни и те жеvmid=100на pve1 и pve2 дают разный вердикт авторизации (никакого протекания прав).- При
len(hosts) > 1hostобязателен для всех инструментов, кромеclusters_list(флот-обзор). Не указан → отказ (fail-closed на неоднозначность), чтобы случайно не задеть чужой кластер. - Глобальный
allowlistпри мульти-гипервизоре — запрещён (ошибка конфигурации): права обязаны быть per-host. Для одиночного гипервизора глобальныйallowlistостаётся допустимым фолбэком. - Probe-паттерны несут
host=<alias>первым — permission-правила могут различать кластеры:patterns: - match: "proxmox__vm_delete" pattern: "host=pve1 node=pve vmid=500" then: ask - Нет флот-агрегатов по VMID (
clusters_listдаёт карту алиасов, дальше — вызов сhost), чтобы не терять принадлежность хоста.
Безопасность (§9.5)
| Против чего | Защита в модуле | Внешний барьер |
|---|---|---|
| Случайное удаление/rollback/migrate | confirm="true", read_only, per-host allow_vmids/allow_nodes, path-валидация |
require_approval в agent.yaml |
| Коллизия VMID между гипервизорами | host обязателен при мульти; авторизация per-host; глобальный allowlist запрещён |
— |
| Инъекция в URL (node/vmid/snapname/storage/upid) | guard-валидация (ValidateIdentifier/ValidateUPID) + url.PathEscape |
least-privilege токен в PVE |
| Изменение конфига вслепую | DenyConfigKey (delete/revert/hotplug/spice/...) для update-инструментов |
— |
| Внешний доступ | URL объявляет оператор (модель не задаёт хост), кастомный CA, таймаут | — |
| Долгие/необратимые операции | async UPID + task_status(wait) (bounded task_poll_max_sec) |
— |
| Запуск кода в госте | не реализуется (зона ssh__run) |
— |
Главный барьер — на стороне PVE: токен с минимальными правами
(для чтения достаточно Sys.Audit, VM.Audit, Datastore.Audit; для мутаций —
только нужное). Модуль держит guard как второй слой (стандарт
двухслойности в §9.5).
Probe/approval: мутации регистрируются через registerPatternTool
(эмитятся host=<alias> node=<node> vmid=<vmid>), оператор может задать
patterns-правила по ресурсу; дефолт permission — ask, и всё
деструктивное ещё и в require_approval. always не эмитим — каждый
опасный вызов отдельный ask.
Подключение к агенту
// config.json ядра
"mcp": { "servers": {
"proxmox": { "command": "./forge-tools/proxmox/forge-tools-proxmox", "args": [], "isolation": "pooled" }
}}
# agent.yaml
mcp_servers:
- proxmox
tools:
require_approval:
- "proxmox__vm_start" # и прочие lifecycle
- "proxmox__vm_delete"
- "proxmox__vm_config_update"
- "proxmox__vm_add_disk" ... "proxmox__vm_move_disk"
- "proxmox__vm_add_network" ... "proxmox__vm_remove_network"
- "proxmox__container_*"
- "proxmox__snapshot_rollback" # + snapshot_delete
- "proxmox__backup_create"
- "proxmox__task_stop"
blocked: []
# при желании тонкие правила по ресурсу:
permission:
default: ask
patterns:
- match: "proxmox__vm_delete"
pattern: "node=pve vmid=500"
then: ask
Сборка и ручной тест (T1)
make build # CGO_ENABLED=0, статический бинарник
./forge-tools-proxmox --health # ok
T1 по stdio:
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}\n' \
| ./forge-tools-proxmox -config /abs/path/pve.json
tools/list → 54 инструмента (27 мутаций с probe-флагом); node_status в
read_only → данные; vm_delete без confirm/вне allow_vmids → понятный отказ
без обращения к API.
Безопасный тест на homelab (не трогая существующие ВМ)
- Single-node
pve(10.0.0.10, PVE 8.4.21). Существующие ВМ — 100…304. - Тестовая ВМ 500 — клон шаблона 106 (
template.example.com, server: OVMF,scsi016G наlocal-nc,net0→vmbr0,agent:1), черезproxmox__vm_clone(newid=500) ИЛИ вручную. - Dev-конфиг:
"hosts":[{"alias":"pve","allow_nodes":["pve"],"allow_vmids":[500]}]→ модуль физически не может задеть 100–304. guest_ipsтестируется на клоне 500 (нужен guest-agent, в шаблоне включён).
Roadmap (отдельные узкие модули, не сюда)
proxmox-backup— restore/delete/prune vzdump (необратимые, нужна выверенная семантика), расписания, retention.proxmox-storage— Ceph/ZFS-pools, ISO/template upload, disk import.proxmox-net— bridges/bonds/SDN-VXLAN, HA, firewall, ACL/users.- Node reboot/shutdown — сознательно НЕ включено (однонодовый blast-radius).
Тесты
go test ./... -count=1 # unit: политика/guard (без сети и без PVE)
Integration-кейсы (-run Integration) — против реального PVE, только на VMID 500.
Разработка
make lint # go vet + gofmt -l (пусто = ок)
make test # go test -race ./...
make build
Требования: Go 1.27+ и доступ к приватному Go-модулю
forge-toolkit
(export GOPRIVATE=git.totmin.ru).
Лицензия
Apache-2.0 — см. LICENSE.