205 lines
7.4 KiB
Go
205 lines
7.4 KiB
Go
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
|
||
}
|