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= (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 }