Files
forge-tools-proxmox/internal/tools/vm_disk.go
T

205 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}