228 lines
12 KiB
Markdown
228 lines
12 KiB
Markdown
# 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).
|