package tools import ( "context" "fmt" "net/url" "strconv" "github.com/modelcontextprotocol/go-sdk/mcp" ) // lxc.go — LXC-контейнеры: чтение (list/describe/config) + lifecycle // (start/stop/reboot/shutdown), clone, delete, config_update, resize. // Всё по аналогии с QEMU, но через /lxc. Мутации = gate + confirm + probe. const vmTypeLXC = "lxc" func registerLXCTools(s *mcp.Server) { // --- read --- s.AddTool(&mcp.Tool{ Name: "containers_list", Description: "List LXC containers across the cluster. Read-only.", InputSchema: schema(map[string]any{ "host": strProps("Cluster alias (default: primary)", false), }, nil), }, containersListHandler) s.AddTool(&mcp.Tool{ Name: "container_describe", Description: "Describe one LXC container: status + config. Read-only.", InputSchema: schema(map[string]any{ "node": strProps("Node name", true), "vmid": strProps("CT ID", true), "host": strProps("Cluster alias (default: primary)", false), }, []string{"node", "vmid"}), }, containerDescribeHandler) s.AddTool(&mcp.Tool{ Name: "container_config", Description: "Get the raw LXC config of a container. Read-only.", InputSchema: schema(map[string]any{ "node": strProps("Node name", true), "vmid": strProps("CT ID", true), "host": strProps("Cluster alias (default: primary)", false), }, []string{"node", "vmid"}), }, containerConfigHandler) // --- lifecycle --- registerPatternTool(s, &mcp.Tool{ Name: "container_start", Description: "Start an LXC container. Requires write permission.", InputSchema: containerActionSchema(false), }, vmPatterns, containerStartHandler) registerPatternTool(s, &mcp.Tool{ Name: "container_stop", Description: "Stop an LXC container. Requires confirm + write permission.", InputSchema: containerActionSchema(true), }, vmPatterns, containerStopHandler) registerPatternTool(s, &mcp.Tool{ Name: "container_reboot", Description: "Reboot an LXC container. Requires confirm + write permission.", InputSchema: containerActionSchema(true), }, vmPatterns, containerRebootHandler) registerPatternTool(s, &mcp.Tool{ Name: "container_shutdown", Description: "Gracefully shut down an LXC container. Requires confirm + write permission.", InputSchema: containerActionSchema(true), }, vmPatterns, containerShutdownHandler) // --- clone / delete / config / resize --- registerPatternTool(s, &mcp.Tool{ Name: "container_clone", Description: "Clone an LXC container/template into a new CT ID. Requires confirm + write permission.", InputSchema: schema(map[string]any{ "node": strProps("Node name", true), "vmid": strProps("Source CT ID", true), "newid": intProps("New CT ID (0 = next free)", false), "name": strProps("Name for the clone", false), "confirm": strProps("Set to \"true\" to confirm", true, "true"), }, []string{"node", "vmid", "confirm"}), }, vmPatterns, containerCloneHandler) registerPatternTool(s, &mcp.Tool{ Name: "container_delete", Description: "Permanently delete an LXC container. Requires confirm + write permission.", InputSchema: schema(map[string]any{ "node": strProps("Node name", true), "vmid": strProps("CT ID", true), "confirm": strProps("Set to \"true\" to confirm permanent deletion", true, "true"), }, []string{"node", "vmid", "confirm"}), }, vmPatterns, containerDeleteHandler) registerPatternTool(s, &mcp.Tool{ Name: "container_config_update", Description: "Update safe LXC config fields (hostname, memory, swap, cores, unprivileged). Denied keys rejected. Requires confirm + write permission.", InputSchema: schema(map[string]any{ "node": strProps("Node name", true), "vmid": strProps("CT ID", true), "updates": objectProps("Object of key->value config fields", true), "confirm": strProps("Set to \"true\" to confirm", true, "true"), }, []string{"node", "vmid", "updates", "confirm"}), }, vmPatterns, containerConfigUpdateHandler) registerPatternTool(s, &mcp.Tool{ Name: "container_resize", Description: "Resize a rootfs/mountpoint of an LXC container. Requires confirm + write permission.", InputSchema: schema(map[string]any{ "node": strProps("Node name", true), "vmid": strProps("CT ID", true), "disk": strProps("Disk to resize (e.g. rootfs)", true), "size": strProps("Size change, e.g. +5G", true), "confirm": strProps("Set to \"true\" to confirm", true, "true"), }, []string{"node", "vmid", "disk", "size", "confirm"}), }, vmPatterns, containerResizeHandler) } // containerActionSchema — схема для lifecycle LXC (с/без confirm). func containerActionSchema(withConfirm bool) map[string]any { props := map[string]any{ "node": strProps("Node name", true), "vmid": strProps("CT ID", true), } req := []string{"node", "vmid"} if withConfirm { props["confirm"] = strProps("Set to \"true\" to confirm", true, "true") req = append(req, "confirm") } return schema(props, req) } // --- read --- func containersListHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { t, err := tenantFor(ctx, req) if err != nil { return errorResult(err.Error()), nil } host, err := resolveHost(t, requestArgs(req)) if err != nil { return errorResult(err.Error()), nil } cctx, cancel := timeout(ctx, t) defer cancel() data, err := t.GuestResources(cctx, host, vmTypeLXC) if err != nil { return errorResult(err.Error()), nil } return textResult(pretty(data)), nil } func containerDescribeHandler(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 } cctx, cancel := timeout(ctx, t) defer cancel() status, err := t.ContainerStatus(cctx, host, node, vmid) if err != nil { return errorResult(err.Error()), nil } cfg, err := t.ContainerConfig(cctx, host, node, vmid) if err != nil { return errorResult(err.Error()), nil } return textResult("=== Status ===\n" + pretty(status) + "\n\n=== Config ===\n" + pretty(cfg)), nil } func containerConfigHandler(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 } cctx, cancel := timeout(ctx, t) defer cancel() data, err := t.ContainerConfig(cctx, host, node, vmid) if err != nil { return errorResult(err.Error()), nil } return textResult(pretty(data)), nil } // --- lifecycle --- func containerStartHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return containerActionHandler(ctx, req, "start", false) } func containerStopHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return containerActionHandler(ctx, req, "stop", true) } func containerRebootHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return containerActionHandler(ctx, req, "reboot", true) } func containerShutdownHandler(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return containerActionHandler(ctx, req, "shutdown", true) } func containerActionHandler(ctx context.Context, req *mcp.CallToolRequest, action string, needConfirm bool) (*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 needConfirm { if err := confirm(args, "container_"+action); err != nil { return errorResult(err.Error()), nil } } if err := gateVMWrite(t, host, vmid); err != nil { return errorResult(err.Error()), nil } upid, err := t.ContainerAction(ctx, host, node, vmid, action) if err != nil { return errorResult(err.Error()), nil } return textResult(upidMsg("container_"+action, fmt.Sprintf("%s/%d", node, vmid), upid)), nil } func containerCloneHandler(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, src, host, r := resolveVMArgs(t, args) if r != nil { return r, nil } if err := confirm(args, "container_clone"); err != nil { return errorResult(err.Error()), nil } if err := gateVMWrite(t, host, src); err != nil { return errorResult(err.Error()), nil } newID := getInt(args, "newid", 0) if newID == 0 { data, err := t.NextID(ctx, host) if err != nil { return errorResult(err.Error()), nil } newID = intFromData(data) } form := url.Values{"newid": {strconv.Itoa(newID)}} if name := getString(args, "name", ""); name != "" { form.Set("name", name) } upid, err := t.ContainerClone(ctx, host, node, src, newID, form) if err != nil { return errorResult(err.Error()), nil } return textResult(upidMsg("container_clone", fmt.Sprintf("%d -> %d", src, newID), upid)), nil } func containerDeleteHandler(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, "container_delete"); err != nil { return errorResult(err.Error()), nil } if err := gateVMWrite(t, host, vmid); err != nil { return errorResult(err.Error()), nil } upid, err := t.ContainerDelete(ctx, host, node, vmid) if err != nil { return errorResult(err.Error()), nil } return textResult(upidMsg("container_delete", fmt.Sprintf("%s/%d", node, vmid), upid)), nil } func containerConfigUpdateHandler(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, "container_config_update"); err != nil { return errorResult(err.Error()), nil } if err := gateVMWrite(t, host, vmid); err != nil { return errorResult(err.Error()), nil } updates := getObject(args, "updates") if len(updates) == 0 { return errorResult("'updates' must be a non-empty object"), nil } form := make(url.Values, len(updates)) for k, v := range updates { if t.Config().DenyConfigKey(k) { return errorResult(fmt.Sprintf("field %q is denied by policy", k)), nil } form.Set(k, fmt.Sprint(v)) } upid, err := t.ConfigPost(ctx, host, node, vmTypeLXC, vmid, form) if err != nil { return errorResult(err.Error()), nil } return textResult(upidMsg("container_config_update", fmt.Sprintf("%s/%d", node, vmid), upid)), nil } func containerResizeHandler(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, "container_resize"); 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)"), nil } upid, err := t.ResizeDisk(ctx, host, node, vmTypeLXC, vmid, url.Values{"disk": {disk}, "size": {size}}) if err != nil { return errorResult(err.Error()), nil } return textResult(upidMsg("container_resize", fmt.Sprintf("%s/%d %s", node, vmid, disk), upid)), nil }