Skip to content

Commit 3a38d3a

Browse files
committed
feat(kubevirt): Add vm_start and vm_stop tools
Add lifecycle management tools for starting and stopping VirtualMachines. These tools provide simple, single-action operations that prevent destructive workarounds like delete/recreate. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Lee Yarwood <lyarwood@redhat.com>
1 parent 775deaf commit 3a38d3a

File tree

3 files changed

+250
-0
lines changed

3 files changed

+250
-0
lines changed

pkg/toolsets/kubevirt/toolset.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
88
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"
99
vm_create "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/create"
10+
vm_start "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/start"
11+
vm_stop "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/stop"
1012
vm_troubleshoot "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/troubleshoot"
1113
)
1214

@@ -25,6 +27,8 @@ func (t *Toolset) GetDescription() string {
2527
func (t *Toolset) GetTools(o internalk8s.Openshift) []api.ServerTool {
2628
return slices.Concat(
2729
vm_create.Tools(),
30+
vm_start.Tools(),
31+
vm_stop.Tools(),
2832
vm_troubleshoot.Tools(),
2933
)
3034
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package start
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/containers/kubernetes-mcp-server/pkg/api"
7+
"github.com/containers/kubernetes-mcp-server/pkg/output"
8+
"github.com/google/jsonschema-go/jsonschema"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11+
"k8s.io/apimachinery/pkg/runtime/schema"
12+
"k8s.io/client-go/dynamic"
13+
"k8s.io/utils/ptr"
14+
)
15+
16+
func Tools() []api.ServerTool {
17+
return []api.ServerTool{
18+
{
19+
Tool: api.Tool{
20+
Name: "vm_start",
21+
Description: "Start a halted or stopped VirtualMachine by changing its runStrategy to Always",
22+
InputSchema: &jsonschema.Schema{
23+
Type: "object",
24+
Properties: map[string]*jsonschema.Schema{
25+
"namespace": {
26+
Type: "string",
27+
Description: "The namespace of the virtual machine",
28+
},
29+
"name": {
30+
Type: "string",
31+
Description: "The name of the virtual machine to start",
32+
},
33+
},
34+
Required: []string{"namespace", "name"},
35+
},
36+
Annotations: api.ToolAnnotations{
37+
Title: "Virtual Machine: Start",
38+
ReadOnlyHint: ptr.To(false),
39+
DestructiveHint: ptr.To(false),
40+
IdempotentHint: ptr.To(true),
41+
OpenWorldHint: ptr.To(false),
42+
},
43+
},
44+
Handler: start,
45+
},
46+
}
47+
}
48+
49+
func start(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
50+
// Parse required parameters
51+
namespace, err := getRequiredString(params, "namespace")
52+
if err != nil {
53+
return api.NewToolCallResult("", err), nil
54+
}
55+
56+
name, err := getRequiredString(params, "name")
57+
if err != nil {
58+
return api.NewToolCallResult("", err), nil
59+
}
60+
61+
// Get dynamic client
62+
restConfig := params.RESTConfig()
63+
if restConfig == nil {
64+
return api.NewToolCallResult("", fmt.Errorf("failed to get REST config")), nil
65+
}
66+
67+
dynamicClient, err := dynamic.NewForConfig(restConfig)
68+
if err != nil {
69+
return api.NewToolCallResult("", fmt.Errorf("failed to create dynamic client: %w", err)), nil
70+
}
71+
72+
// Get the current VM
73+
gvr := schema.GroupVersionResource{
74+
Group: "kubevirt.io",
75+
Version: "v1",
76+
Resource: "virtualmachines",
77+
}
78+
79+
vm, err := dynamicClient.Resource(gvr).Namespace(namespace).Get(
80+
params.Context,
81+
name,
82+
metav1.GetOptions{},
83+
)
84+
if err != nil {
85+
return api.NewToolCallResult("", fmt.Errorf("failed to get VirtualMachine: %w", err)), nil
86+
}
87+
88+
// Update runStrategy to Always
89+
if err := unstructured.SetNestedField(vm.Object, "Always", "spec", "runStrategy"); err != nil {
90+
return api.NewToolCallResult("", fmt.Errorf("failed to set runStrategy: %w", err)), nil
91+
}
92+
93+
// Update the VM
94+
updatedVM, err := dynamicClient.Resource(gvr).Namespace(namespace).Update(
95+
params.Context,
96+
vm,
97+
metav1.UpdateOptions{},
98+
)
99+
if err != nil {
100+
return api.NewToolCallResult("", fmt.Errorf("failed to update VirtualMachine: %w", err)), nil
101+
}
102+
103+
// Format the output
104+
marshalledYaml, err := output.MarshalYaml(updatedVM)
105+
if err != nil {
106+
return api.NewToolCallResult("", fmt.Errorf("failed to marshal VirtualMachine: %w", err)), nil
107+
}
108+
109+
return api.NewToolCallResult("# VirtualMachine started successfully\n"+marshalledYaml, nil), nil
110+
}
111+
112+
func getRequiredString(params api.ToolHandlerParams, key string) (string, error) {
113+
args := params.GetArguments()
114+
val, ok := args[key]
115+
if !ok {
116+
return "", fmt.Errorf("%s parameter required", key)
117+
}
118+
str, ok := val.(string)
119+
if !ok {
120+
return "", fmt.Errorf("%s parameter must be a string", key)
121+
}
122+
return str, nil
123+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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/output"
8+
"github.com/google/jsonschema-go/jsonschema"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11+
"k8s.io/apimachinery/pkg/runtime/schema"
12+
"k8s.io/client-go/dynamic"
13+
"k8s.io/utils/ptr"
14+
)
15+
16+
func Tools() []api.ServerTool {
17+
return []api.ServerTool{
18+
{
19+
Tool: api.Tool{
20+
Name: "vm_stop",
21+
Description: "Stop a running VirtualMachine by changing its runStrategy to Halted",
22+
InputSchema: &jsonschema.Schema{
23+
Type: "object",
24+
Properties: map[string]*jsonschema.Schema{
25+
"namespace": {
26+
Type: "string",
27+
Description: "The namespace of the virtual machine",
28+
},
29+
"name": {
30+
Type: "string",
31+
Description: "The name of the virtual machine to stop",
32+
},
33+
},
34+
Required: []string{"namespace", "name"},
35+
},
36+
Annotations: api.ToolAnnotations{
37+
Title: "Virtual Machine: Stop",
38+
ReadOnlyHint: ptr.To(false),
39+
DestructiveHint: ptr.To(false),
40+
IdempotentHint: ptr.To(true),
41+
OpenWorldHint: ptr.To(false),
42+
},
43+
},
44+
Handler: stop,
45+
},
46+
}
47+
}
48+
49+
func stop(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
50+
// Parse required parameters
51+
namespace, err := getRequiredString(params, "namespace")
52+
if err != nil {
53+
return api.NewToolCallResult("", err), nil
54+
}
55+
56+
name, err := getRequiredString(params, "name")
57+
if err != nil {
58+
return api.NewToolCallResult("", err), nil
59+
}
60+
61+
// Get dynamic client
62+
restConfig := params.RESTConfig()
63+
if restConfig == nil {
64+
return api.NewToolCallResult("", fmt.Errorf("failed to get REST config")), nil
65+
}
66+
67+
dynamicClient, err := dynamic.NewForConfig(restConfig)
68+
if err != nil {
69+
return api.NewToolCallResult("", fmt.Errorf("failed to create dynamic client: %w", err)), nil
70+
}
71+
72+
// Get the current VM
73+
gvr := schema.GroupVersionResource{
74+
Group: "kubevirt.io",
75+
Version: "v1",
76+
Resource: "virtualmachines",
77+
}
78+
79+
vm, err := dynamicClient.Resource(gvr).Namespace(namespace).Get(
80+
params.Context,
81+
name,
82+
metav1.GetOptions{},
83+
)
84+
if err != nil {
85+
return api.NewToolCallResult("", fmt.Errorf("failed to get VirtualMachine: %w", err)), nil
86+
}
87+
88+
// Update runStrategy to Halted
89+
if err := unstructured.SetNestedField(vm.Object, "Halted", "spec", "runStrategy"); err != nil {
90+
return api.NewToolCallResult("", fmt.Errorf("failed to set runStrategy: %w", err)), nil
91+
}
92+
93+
// Update the VM
94+
updatedVM, err := dynamicClient.Resource(gvr).Namespace(namespace).Update(
95+
params.Context,
96+
vm,
97+
metav1.UpdateOptions{},
98+
)
99+
if err != nil {
100+
return api.NewToolCallResult("", fmt.Errorf("failed to update VirtualMachine: %w", err)), nil
101+
}
102+
103+
// Format the output
104+
marshalledYaml, err := output.MarshalYaml(updatedVM)
105+
if err != nil {
106+
return api.NewToolCallResult("", fmt.Errorf("failed to marshal VirtualMachine: %w", err)), nil
107+
}
108+
109+
return api.NewToolCallResult("# VirtualMachine stopped successfully\n"+marshalledYaml, nil), nil
110+
}
111+
112+
func getRequiredString(params api.ToolHandlerParams, key string) (string, error) {
113+
args := params.GetArguments()
114+
val, ok := args[key]
115+
if !ok {
116+
return "", fmt.Errorf("%s parameter required", key)
117+
}
118+
str, ok := val.(string)
119+
if !ok {
120+
return "", fmt.Errorf("%s parameter must be a string", key)
121+
}
122+
return str, nil
123+
}

0 commit comments

Comments
 (0)