Files
forge-tools-proxmox/README.md
T

228 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).