Initial commit: forge-tools-proxmox — MCP-сервер для Proxmox VE

This commit is contained in:
Maksim Totmin
2026-10-01 10:41:43 +07:00
commit a7addaead9
30 changed files with 4226 additions and 0 deletions
+227
View File
@@ -0,0 +1,227 @@
# 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` для одиночного режима.
```jsonc
{
"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`](.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) > 1` `host` обязателен** для всех инструментов, кроме
`clusters_list` (флот-обзор). Не указан → отказ (fail-closed на
неоднозначность), чтобы случайно не задеть чужой кластер.
- **Глобальный `allowlist` при мульти-гипервизоре — запрещён** (ошибка
конфигурации): права обязаны быть per-host. Для одиночного гипервизора
глобальный `allowlist` остаётся допустимым фолбэком.
- **Probe-паттерны** несут `host=<alias>` первым — permission-правила могут
различать кластеры:
```yaml
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.
## Подключение к агенту
```jsonc
// config.json ядра
"mcp": { "servers": {
"proxmox": { "command": "./forge-tools/proxmox/forge-tools-proxmox", "args": [], "isolation": "pooled" }
}}
```
```yaml
# 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)
```bash
make build # CGO_ENABLED=0, статический бинарник
./forge-tools-proxmox --health # ok
```
T1 по stdio:
```bash
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,
`scsi0` 16G на `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).
## Тесты
```bash
go test ./... -count=1 # unit: политика/guard (без сети и без PVE)
```
Integration-кейсы (`-run Integration`) — против реального PVE, только на VMID 500.
---
## Разработка
```bash
make lint # go vet + gofmt -l (пусто = ок)
make test # go test -race ./...
make build
```
Требования: Go 1.27+ и доступ к приватному Go-модулю
[`forge-toolkit`](https://git.totmin.ru/en2zmax/forge-toolkit)
(`export GOPRIVATE=git.totmin.ru`).
---
## Лицензия
Apache-2.0 — см. [`LICENSE`](LICENSE).