Files

355 lines
12 KiB
Go
Raw Permalink 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"
"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
}