Skip to content

Commit 477c91e

Browse files
committed
feat: add vm_start tool for KubeVirt VirtualMachines
Implement functionality to start VirtualMachines by updating their runStrategy to 'Always'. This commit includes: - pkg/kubevirt/vm.go: Core helper functions and types - VirtualMachine GVK/GVR constants - RunStrategy type with Always/Halted constants - GetVirtualMachine() - retrieve a VM from the cluster - GetVMRunStrategy() - get current runStrategy from a VM - SetVMRunStrategy() - set runStrategy on a VM - UpdateVirtualMachine() - update a VM in the cluster - StartVM() - start a VM (idempotent, checks if already running) - pkg/toolsets/kubevirt/vm/start/tool.go: MCP tool implementation - Input validation for namespace and name parameters - Proper error handling and user feedback - Returns updated VM YAML on success The implementation uses the Kubernetes dynamic client pattern consistent with the existing vm_create tool. Code was assisted by Cursor AI Signed-off-by: Karel Simon <ksimon@redhat.com>
1 parent b706778 commit 477c91e

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed

pkg/kubevirt/vm.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package kubevirt
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9+
"k8s.io/apimachinery/pkg/runtime/schema"
10+
"k8s.io/client-go/dynamic"
11+
)
12+
13+
// RunStrategy represents the run strategy for a VirtualMachine
14+
type RunStrategy string
15+
16+
const (
17+
RunStrategyAlways RunStrategy = "Always"
18+
RunStrategyHalted RunStrategy = "Halted"
19+
)
20+
21+
var (
22+
// VirtualMachineGVK is the GroupVersionKind for VirtualMachine resources
23+
VirtualMachineGVK = schema.GroupVersionKind{
24+
Group: "kubevirt.io",
25+
Version: "v1",
26+
Kind: "VirtualMachine",
27+
}
28+
29+
// VirtualMachineGVR is the GroupVersionResource for VirtualMachine resources
30+
VirtualMachineGVR = schema.GroupVersionResource{
31+
Group: "kubevirt.io",
32+
Version: "v1",
33+
Resource: "virtualmachines",
34+
}
35+
)
36+
37+
// GetVirtualMachine retrieves a VirtualMachine by namespace and name
38+
func GetVirtualMachine(ctx context.Context, client dynamic.Interface, namespace, name string) (*unstructured.Unstructured, error) {
39+
return client.Resource(VirtualMachineGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
40+
}
41+
42+
// GetVMRunStrategy retrieves the current runStrategy from a VirtualMachine
43+
// Returns the strategy, whether it was found, and any error
44+
func GetVMRunStrategy(vm *unstructured.Unstructured) (RunStrategy, bool, error) {
45+
strategy, found, err := unstructured.NestedString(vm.Object, "spec", "runStrategy")
46+
if err != nil {
47+
return "", false, fmt.Errorf("failed to read runStrategy: %w", err)
48+
}
49+
50+
return RunStrategy(strategy), found, nil
51+
}
52+
53+
// SetVMRunStrategy sets the runStrategy on a VirtualMachine
54+
func SetVMRunStrategy(vm *unstructured.Unstructured, strategy RunStrategy) error {
55+
return unstructured.SetNestedField(vm.Object, string(strategy), "spec", "runStrategy")
56+
}
57+
58+
// UpdateVirtualMachine updates a VirtualMachine in the cluster
59+
func UpdateVirtualMachine(ctx context.Context, client dynamic.Interface, vm *unstructured.Unstructured) (*unstructured.Unstructured, error) {
60+
return client.Resource(VirtualMachineGVR).
61+
Namespace(vm.GetNamespace()).
62+
Update(ctx, vm, metav1.UpdateOptions{})
63+
}
64+
65+
// StartVM starts a VirtualMachine by updating its runStrategy to Always
66+
// Returns the updated VM and true if the VM was started, false if it was already running
67+
func StartVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) (*unstructured.Unstructured, bool, error) {
68+
// Get the current VirtualMachine
69+
vm, err := GetVirtualMachine(ctx, dynamicClient, namespace, name)
70+
if err != nil {
71+
return nil, false, fmt.Errorf("failed to get VirtualMachine: %w", err)
72+
}
73+
74+
currentStrategy, found, err := GetVMRunStrategy(vm)
75+
if err != nil {
76+
return nil, false, err
77+
}
78+
79+
// Check if already running
80+
if found && currentStrategy == RunStrategyAlways {
81+
return vm, false, nil
82+
}
83+
84+
// Update runStrategy to Always
85+
if err := SetVMRunStrategy(vm, RunStrategyAlways); err != nil {
86+
return nil, false, fmt.Errorf("failed to set runStrategy: %w", err)
87+
}
88+
89+
// Update the VM in the cluster
90+
updatedVM, err := UpdateVirtualMachine(ctx, dynamicClient, vm)
91+
if err != nil {
92+
return nil, false, fmt.Errorf("failed to start VirtualMachine: %w", err)
93+
}
94+
95+
return updatedVM, true, nil
96+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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/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_start",
19+
Description: "Start a halted or stopped VirtualMachine by changing its runStrategy to Always",
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 start",
30+
},
31+
},
32+
Required: []string{"namespace", "name"},
33+
},
34+
Annotations: api.ToolAnnotations{
35+
Title: "Virtual Machine: Start",
36+
ReadOnlyHint: ptr.To(false),
37+
DestructiveHint: ptr.To(false),
38+
IdempotentHint: ptr.To(true),
39+
OpenWorldHint: ptr.To(false),
40+
},
41+
},
42+
Handler: start,
43+
},
44+
}
45+
}
46+
47+
func start(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+
// Start the VirtualMachine using kubevirt helper
60+
dynamicClient := params.AccessControlClientset().DynamicClient()
61+
vm, wasStarted, err := kubevirt.StartVM(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 wasStarted {
69+
message = "# VirtualMachine started successfully\n"
70+
} else {
71+
message = fmt.Sprintf("# VirtualMachine '%s' in namespace '%s' is already running\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)