Initial commit: forge-tools-proxmox — MCP-сервер для Proxmox VE
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user