Skip to content

Commit 655a75f

Browse files
committed
feat: add vm_stop tool for KubeVirt VirtualMachines
Implement functionality to stop VirtualMachines by updating their runStrategy to 'Halted'. This commit includes: - pkg/kubevirt/vm.go: Add StopVM() function - Stops a VM by setting runStrategy to Halted - Idempotent operation: checks if VM is already stopped - Returns updated VM and boolean indicating if state changed - Consistent error handling with StartVM() - pkg/toolsets/kubevirt/vm/stop/tool.go: MCP tool implementation - Input validation for namespace and name parameters - Proper error handling and user feedback - Provides clear messaging about whether VM was stopped or already halted The tool gracefully handles already-stopped VMs and provides clear feedback on whether the VM state changed. Code was assisted by Cursor AI Signed-off-by: Karel Simon <ksimon@redhat.com>
1 parent 477c91e commit 655a75f

File tree

2 files changed

+127
-0
lines changed

2 files changed

+127
-0
lines changed

pkg/kubevirt/vm.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,36 @@ func StartVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, na
9494

9595
return updatedVM, true, nil
9696
}
97+
98+
// StopVM stops a VirtualMachine by updating its runStrategy to Halted
99+
// Returns the updated VM and true if the VM was stopped, false if it was already stopped
100+
func StopVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) (*unstructured.Unstructured, bool, error) {
101+
// Get the current VirtualMachine
102+
vm, err := GetVirtualMachine(ctx, dynamicClient, namespace, name)
103+
if err != nil {
104+
return nil, false, fmt.Errorf("failed to get VirtualMachine: %w", err)
105+
}
106+
107+
currentStrategy, found, err := GetVMRunStrategy(vm)
108+
if err != nil {
109+
return nil, false, err
110+
}
111+
112+
// Check if already stopped
113+
if found && currentStrategy == RunStrategyHalted {
114+
return vm, false, nil
115+
}
116+
117+
// Update runStrategy to Halted
118+
if err := SetVMRunStrategy(vm, RunStrategyHalted); err != nil {
119+
return nil, false, fmt.Errorf("failed to set runStrategy: %w", err)
120+
}
121+
122+
// Update the VM in the cluster
123+
updatedVM, err := UpdateVirtualMachine(ctx, dynamicClient, vm)
124+
if err != nil {
125+
return nil, false, fmt.Errorf("failed to stop VirtualMachine: %w", err)
126+
}
127+
128+
return updatedVM, true, nil
129+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package stop
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/containers/kubernetes-mcp-server/pkg/api"
7+
"github.com/containers/kubernetes-mcp-server/pkg/kubevirt"
8+
"github.com/containers/kubernetes-mcp-server/pkg/output"
9+
"github.com/google/jsonschema-go/jsonschema"
10+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11+
"k8s.io/utils/ptr"
12+
)
13+
14+
func Tools() []api.ServerTool {
15+
return []api.ServerTool{
16+
{
17+
Tool: api.Tool{
18+
Name: "vm_stop",
19+
Description: "Stop a running VirtualMachine by changing its runStrategy to Halted",
20+
InputSchema: &jsonschema.Schema{
21+
Type: "object",
22+
Properties: map[string]*jsonschema.Schema{
23+
"namespace": {
24+
Type: "string",
25+
Description: "The namespace of the virtual machine",
26+
},
27+
"name": {
28+
Type: "string",
29+
Description: "The name of the virtual machine to stop",
30+
},
31+
},
32+
Required: []string{"namespace", "name"},
33+
},
34+
Annotations: api.ToolAnnotations{
35+
Title: "Virtual Machine: Stop",
36+
ReadOnlyHint: ptr.To(false),
37+
DestructiveHint: ptr.To(false),
38+
IdempotentHint: ptr.To(true),
39+
OpenWorldHint: ptr.To(false),
40+
},
41+
},
42+
Handler: stop,
43+
},
44+
}
45+
}
46+
47+
func stop(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
48+
// Parse input parameters
49+
namespace, err := getRequiredString(params, "namespace")
50+
if err != nil {
51+
return api.NewToolCallResult("", err), nil
52+
}
53+
54+
name, err := getRequiredString(params, "name")
55+
if err != nil {
56+
return api.NewToolCallResult("", err), nil
57+
}
58+
59+
// Stop the VirtualMachine using kubevirt helper
60+
dynamicClient := params.AccessControlClientset().DynamicClient()
61+
vm, wasRunning, err := kubevirt.StopVM(params.Context, dynamicClient, namespace, name)
62+
if err != nil {
63+
return api.NewToolCallResult("", err), nil
64+
}
65+
66+
// Generate appropriate message
67+
var message string
68+
if wasRunning {
69+
message = "# VirtualMachine stopped successfully\n"
70+
} else {
71+
message = fmt.Sprintf("# VirtualMachine '%s' in namespace '%s' is already stopped\n", name, namespace)
72+
}
73+
74+
// Format the output
75+
marshalledYaml, err := output.MarshalYaml([]*unstructured.Unstructured{vm})
76+
if err != nil {
77+
return api.NewToolCallResult("", fmt.Errorf("failed to marshal VirtualMachine: %w", err)), nil
78+
}
79+
80+
return api.NewToolCallResult(message+marshalledYaml, nil), nil
81+
}
82+
83+
func getRequiredString(params api.ToolHandlerParams, key string) (string, error) {
84+
args := params.GetArguments()
85+
val, ok := args[key]
86+
if !ok {
87+
return "", fmt.Errorf("%s parameter required", key)
88+
}
89+
str, ok := val.(string)
90+
if !ok {
91+
return "", fmt.Errorf("%s parameter must be a string", key)
92+
}
93+
return str, nil
94+
}

0 commit comments

Comments
 (0)