Initial commit: forge-tools-proxmox — MCP-сервер для Proxmox VE
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package pve
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"git.totmin.ru/en2zmax/forge-toolkit/configreload"
|
||||
)
|
||||
|
||||
// Manager — одиночный, ПОТОКО-БЕЗОПАСНЫЙ диспетчер процесса (столп 2 §2):
|
||||
// агенты запускаются в отдельных goroutine и конкурентно дёргают один
|
||||
// Manager. В pooled-режиме (isolation=pooled) ядро на каждый вызов
|
||||
// инжектит серверный аргумент _tenant_config = <agentDir>/forge-tools/
|
||||
// proxmox.json — поэтому менеджер кэширует per-путь загрузчики конфига
|
||||
// (configreload, live-reload по контент-хэшу) и per-путь Tenant'ы (свои
|
||||
// клиенты/секреты). Тенанты НЕ делят клиентов между агентами.
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
// configPath — статический -config (легаси-одиночный режим). Пусто =
|
||||
// pooled: конфиг приходит на каждый вызов через _tenant_config.
|
||||
configPath string
|
||||
// loaders кэширует configreload.Loader[*Config] по пути конфига.
|
||||
loaders map[string]*configreload.Loader[*Config]
|
||||
// tenants кэширует Tenant (свои клиенты) по ключу-пути конфига.
|
||||
tenants map[string]*Tenant
|
||||
}
|
||||
|
||||
// NewManager создаёт менеджер. configPath — опциональный постоянный конфиг
|
||||
// (-config); пусто = pooled (тенант из _tenant_config на каждый вызов).
|
||||
func NewManager(configPath string) *Manager {
|
||||
return &Manager{
|
||||
configPath: configPath,
|
||||
loaders: make(map[string]*configreload.Loader[*Config]),
|
||||
tenants: make(map[string]*Tenant),
|
||||
}
|
||||
}
|
||||
|
||||
// Tenant возвращает per-agent тенант. path — _tenant_config (пусто при
|
||||
// -config-режиме). Fail-closed: нет конфига — ошибка (модуль отказывает,
|
||||
// а не работает «с общими» кредами).
|
||||
func (m *Manager) Tenant(ctx context.Context, path string) (*Tenant, error) {
|
||||
cfgPath := m.resolvePath(path)
|
||||
if cfgPath == "" {
|
||||
return nil, errors.New("proxmox: no config (set -config or provide _tenant_config)")
|
||||
}
|
||||
|
||||
loader := m.loaderFor(cfgPath)
|
||||
cfg, err := loader.Get()
|
||||
if err != nil {
|
||||
// ErrNotFound / parse-error без last-good — fail-closed, а не фолбэк.
|
||||
if cfg == nil || !cfg.HasHosts() {
|
||||
return nil, fmt.Errorf("proxmox: tenant config %s: %w", cfgPath, err)
|
||||
}
|
||||
// Есть last-good — работаем со старым (правка была битой), но
|
||||
// сигналим, чтобы не молчать.
|
||||
// (здесь последний рабочий конфиг уже возвращён в cfg)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if t, ok := m.tenants[cfgPath]; ok {
|
||||
return t, nil
|
||||
}
|
||||
t := &Tenant{cfg: cfg, clients: make(map[string]*Client)}
|
||||
m.tenants[cfgPath] = t
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// resolvePath выбирает путь конфига: _tenant_config в приоритете, иначе
|
||||
// статический -config.
|
||||
func (m *Manager) resolvePath(tenantPath string) string {
|
||||
if tenantPath != "" {
|
||||
return tenantPath
|
||||
}
|
||||
return m.configPath
|
||||
}
|
||||
|
||||
// loaderFor возвращает (и кэширует) загрузчик конфига по пути.
|
||||
func (m *Manager) loaderFor(path string) *configreload.Loader[*Config] {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if l, ok := m.loaders[path]; ok {
|
||||
return l
|
||||
}
|
||||
l := configreload.New(path, ParseConfig)
|
||||
m.loaders[path] = l
|
||||
return l
|
||||
}
|
||||
|
||||
// Close закрывает все тенанты (и их клиенты).
|
||||
func (m *Manager) Close() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, t := range m.tenants {
|
||||
t.closeLocked()
|
||||
}
|
||||
m.tenants = nil
|
||||
}
|
||||
|
||||
// Tenant — per-agent конфигурация + свои HTTP-клиенты к хостам. Секреты и
|
||||
// разрешения (allowlist) — строго в рамках одного тенанта.
|
||||
type Tenant struct {
|
||||
cfg *Config
|
||||
mu sync.Mutex
|
||||
clients map[string]*Client // alias -> client
|
||||
}
|
||||
|
||||
// Config возвращает политику тенанта.
|
||||
func (t *Tenant) Config() *Config { return t.cfg }
|
||||
|
||||
// Client возвращает HTTP-клиент для хоста по алиасу (или default).
|
||||
// Строится лениво и кэшируется; потокобезопасно.
|
||||
func (t *Tenant) Client(ctx context.Context, alias string) (*Client, error) {
|
||||
host, ok := t.cfg.Host(alias)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("proxmox: host %q not configured", alias)
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if c, ok := t.clients[host.Alias]; ok {
|
||||
return c, nil
|
||||
}
|
||||
c, err := NewClient(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.clients[host.Alias] = c
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Close закрывает клиенты тенанта. Не используй вне RWMutex Manager.
|
||||
func (t *Tenant) Close() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.closeLocked()
|
||||
}
|
||||
|
||||
func (t *Tenant) closeLocked() {
|
||||
// net/http.Client не имеет Close; здесь точка для будущего пула/окружения.
|
||||
t.clients = nil
|
||||
}
|
||||
|
||||
// joinURL собирает корректный путь API (PathEscape против path-traversal).
|
||||
func joinURL(path string, ids ...string) string {
|
||||
p := path
|
||||
for _, id := range ids {
|
||||
p += "/" + url.PathEscape(id)
|
||||
}
|
||||
return p
|
||||
}
|
||||
Reference in New Issue
Block a user