Initial commit: forge-tools-proxmox — MCP-сервер для Proxmox VE
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"forge-tools-proxmox/internal/pve"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// vm_disk.go — операции над дисками ВМ через выделенные эндпоинты
|
||||
// (/resize, /move_disk) или конфиг (добавить/удалить). Все — структурные
|
||||
// мутации: require write gate + confirm + registerPatternTool (probe).
|
||||
// Для QEMU имена дисков: scsi0.., virtio0.., ide0.., sata0...
|
||||
|
||||
func registerVMDiskTools(s *mcp.Server) {
|
||||
registerPatternTool(s, &mcp.Tool{
|
||||
Name: "vm_resize_disk",
|
||||
Description: "Resize a VM disk by name. Size like '+5G' or absolute. Requires confirm + write permission.",
|
||||
InputSchema: schema(map[string]any{
|
||||
"node": strProps("Node name", true),
|
||||
"vmid": strProps("VM ID", true),
|
||||
"disk": strProps("Disk device (e.g. scsi0)", true),
|
||||
"size": strProps("Size change, e.g. +5G or 20G", true),
|
||||
"confirm": strProps("Set to \"true\" to confirm", true, "true"),
|
||||
}, []string{"node", "vmid", "disk", "size", "confirm"}),
|
||||
}, vmPatterns, vmResizeDiskHandler)
|
||||
|
||||
registerPatternTool(s, &mcp.Tool{
|
||||
Name: "vm_add_disk",
|
||||
Description: "Add a new disk (scsi/virtio/ide/sata slot) to a VM. Requires confirm + write permission.",
|
||||
InputSchema: schema(map[string]any{
|
||||
"node": strProps("Node name", true),
|
||||
"vmid": strProps("VM ID", true),
|
||||
"storage": strProps("Storage name (e.g. local-lvm)", true),
|
||||
"size_gb": intProps("Disk size in GB", true),
|
||||
"iface": strProps("Slot to use (e.g. scsi1); required to avoid overwriting", false),
|
||||
"ssd": boolProps("Mark as SSD (default: false)", false),
|
||||
"confirm": strProps("Set to \"true\" to confirm", true, "true"),
|
||||
}, []string{"node", "vmid", "storage", "size_gb", "confirm"}),
|
||||
}, vmPatterns, vmAddDiskHandler)
|
||||
|
||||
registerPatternTool(s, &mcp.Tool{
|
||||
Name: "vm_remove_disk",
|
||||
Description: "Remove a disk from a VM (permanently! frees the storage). Requires confirm + write permission.",
|
||||
InputSchema: schema(map[string]any{
|
||||
"node": strProps("Node name", true),
|
||||
"vmid": strProps("VM ID", true),
|
||||
"disk": strProps("Disk device to remove (e.g. scsi1)", true),
|
||||
"confirm": strProps("Set to \"true\" to confirm permanent removal", true, "true"),
|
||||
}, []string{"node", "vmid", "disk", "confirm"}),
|
||||
}, vmPatterns, vmRemoveDiskHandler)
|
||||
|
||||
registerPatternTool(s, &mcp.Tool{
|
||||
Name: "vm_move_disk",
|
||||
Description: "Move a VM disk to another storage (storage migration). Requires confirm + write permission.",
|
||||
InputSchema: schema(map[string]any{
|
||||
"node": strProps("Node name", true),
|
||||
"vmid": strProps("VM ID", true),
|
||||
"disk": strProps("Disk to move (e.g. scsi0)", true),
|
||||
"storage": strProps("Destination storage", true),
|
||||
"delete": boolProps("Delete the source after successful move (default: false)", false),
|
||||
"confirm": strProps("Set to \"true\" to confirm", true, "true"),
|
||||
}, []string{"node", "vmid", "disk", "storage", "confirm"}),
|
||||
}, vmPatterns, vmMoveDiskHandler)
|
||||
}
|
||||
|
||||
func vmResizeDiskHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
t, err := tenantFor(ctx, req)
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
args := requestArgs(req)
|
||||
node, vmid, host, r := resolveVMArgs(t, args)
|
||||
if r != nil {
|
||||
return r, nil
|
||||
}
|
||||
if err := confirm(args, "vm_resize_disk"); err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
if err := gateVMWrite(t, host, vmid); err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
disk, err := requireName(args, "disk")
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
size := getString(args, "size", "")
|
||||
if size == "" {
|
||||
return errorResult("'size' is required (e.g. +5G or 20G)"), nil
|
||||
}
|
||||
form := url.Values{"disk": {disk}, "size": {size}}
|
||||
upid, err := t.ResizeDisk(ctx, host, node, vmTypeQEMU, vmid, form)
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
return textResult(upidMsg("vm_resize_disk", fmt.Sprintf("%s/%d %s", node, vmid, disk), upid)), nil
|
||||
}
|
||||
|
||||
func vmAddDiskHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
t, err := tenantFor(ctx, req)
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
args := requestArgs(req)
|
||||
node, vmid, host, r := resolveVMArgs(t, args)
|
||||
if r != nil {
|
||||
return r, nil
|
||||
}
|
||||
if err := confirm(args, "vm_add_disk"); err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
if err := gateVMWrite(t, host, vmid); err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
storage := getString(args, "storage", "")
|
||||
sizeGB := getInt(args, "size_gb", 0)
|
||||
if storage == "" || sizeGB <= 0 {
|
||||
return errorResult("'storage' and positive 'size_gb' are required"), nil
|
||||
}
|
||||
iface := getString(args, "iface", "")
|
||||
if iface == "" {
|
||||
// Требуем явный слот: авто-выбор рискует перезаписать существующий
|
||||
// диск (например scsi0). Fail-closed.
|
||||
return errorResult("'iface' is required (e.g. scsi1) to avoid overwriting an existing disk"), nil
|
||||
}
|
||||
if err := pve.ValidateIdentifier(iface); err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
value := fmt.Sprintf("%s:%d", storage, sizeGB)
|
||||
if getBool(args, "ssd", false) {
|
||||
value += ",ssd=1"
|
||||
}
|
||||
upid, err := t.ConfigPost(ctx, host, node, vmTypeQEMU, vmid, url.Values{iface: {value}})
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
return textResult(upidMsg("vm_add_disk", fmt.Sprintf("%s/%d %s", node, vmid, iface), upid)), nil
|
||||
}
|
||||
|
||||
func vmRemoveDiskHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
t, err := tenantFor(ctx, req)
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
args := requestArgs(req)
|
||||
node, vmid, host, r := resolveVMArgs(t, args)
|
||||
if r != nil {
|
||||
return r, nil
|
||||
}
|
||||
if err := confirm(args, "vm_remove_disk"); err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
if err := gateVMWrite(t, host, vmid); err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
disk, err := requireName(args, "disk")
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
// Удаление диска = конфиг UPDATE с delete=<disk> (PVE-семантика).
|
||||
upid, err := t.ConfigPost(ctx, host, node, vmTypeQEMU, vmid, url.Values{"delete": {disk}})
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
return textResult(upidMsg("vm_remove_disk", fmt.Sprintf("%s/%d %s", node, vmid, disk), upid)), nil
|
||||
}
|
||||
|
||||
func vmMoveDiskHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
t, err := tenantFor(ctx, req)
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
args := requestArgs(req)
|
||||
node, vmid, host, r := resolveVMArgs(t, args)
|
||||
if r != nil {
|
||||
return r, nil
|
||||
}
|
||||
if err := confirm(args, "vm_move_disk"); err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
if err := gateVMWrite(t, host, vmid); err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
disk, err := requireName(args, "disk")
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
storage := getString(args, "storage", "")
|
||||
if storage == "" {
|
||||
return errorResult("'storage' is required"), nil
|
||||
}
|
||||
form := url.Values{"disk": {disk}, "storage": {storage}}
|
||||
if getBool(args, "delete", false) {
|
||||
form.Set("delete", "1")
|
||||
}
|
||||
upid, err := t.MoveDisk(ctx, host, node, vmid, form)
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
return textResult(upidMsg("vm_move_disk", fmt.Sprintf("%s/%d %s", node, vmid, disk), upid)), nil
|
||||
}
|
||||
Reference in New Issue
Block a user