Skip to content

Commit 88ced6a

Browse files
committed
feat(kubevirt): add VM lifecycle tools into single vm_lifecycle tool
Add the vm_start, vm_stop, and vm_restart tools into a single vm_lifecycle tool with an 'action' parameter. The new vm_lifecycle tool accepts: - namespace: The namespace of the virtual machine - name: The name of the virtual machine - action: The lifecycle action ('start', 'stop', or 'restart') - start: changes runStrategy to Always - stop: changes runStrategy to Halted - restart: stops then starts the VM Code was assisted by Cursor AI. Signed-off-by: Karel Simon <ksimon@redhat.com>
1 parent ef0fc26 commit 88ced6a

File tree

3 files changed

+302
-0
lines changed

3 files changed

+302
-0
lines changed

pkg/kubevirt/vm.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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+
}
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+
}
130+
131+
// RestartVM restarts a VirtualMachine by temporarily setting runStrategy to Halted then back to Always
132+
func RestartVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) (*unstructured.Unstructured, error) {
133+
// Get the current VirtualMachine
134+
vm, err := GetVirtualMachine(ctx, dynamicClient, namespace, name)
135+
if err != nil {
136+
return nil, fmt.Errorf("failed to get VirtualMachine: %w", err)
137+
}
138+
139+
// Stop the VM first
140+
if err := SetVMRunStrategy(vm, RunStrategyHalted); err != nil {
141+
return nil, fmt.Errorf("failed to set runStrategy to Halted: %w", err)
142+
}
143+
144+
vm, err = UpdateVirtualMachine(ctx, dynamicClient, vm)
145+
if err != nil {
146+
return nil, fmt.Errorf("failed to stop VirtualMachine: %w", err)
147+
}
148+
149+
// Start the VM again
150+
if err := SetVMRunStrategy(vm, RunStrategyAlways); err != nil {
151+
return nil, fmt.Errorf("failed to set runStrategy to Always: %w", err)
152+
}
153+
154+
updatedVM, err := UpdateVirtualMachine(ctx, dynamicClient, vm)
155+
if err != nil {
156+
return nil, fmt.Errorf("failed to start VirtualMachine: %w", err)
157+
}
158+
159+
return updatedVM, nil
160+
}

pkg/toolsets/kubevirt/toolset.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ 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_lifecycle "github.com/containers/kubernetes-mcp-server/pkg/toolsets/kubevirt/vm/lifecycle"
1011
)
1112

1213
type Toolset struct{}
@@ -24,6 +25,7 @@ func (t *Toolset) GetDescription() string {
2425
func (t *Toolset) GetTools(o internalk8s.Openshift) []api.ServerTool {
2526
return slices.Concat(
2627
vm_create.Tools(),
28+
vm_lifecycle.Tools(),
2729
)
2830
}
2931

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package lifecycle
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+
// Action represents the lifecycle action to perform on a VM
15+
type Action string
16+
17+
const (
18+
ActionStart Action = "start"
19+
ActionStop Action = "stop"
20+
ActionRestart Action = "restart"
21+
)
22+
23+
func Tools() []api.ServerTool {
24+
return []api.ServerTool{
25+
{
26+
Tool: api.Tool{
27+
Name: "vm_lifecycle",
28+
Description: "Manage VirtualMachine lifecycle: start, stop, or restart a VM",
29+
InputSchema: &jsonschema.Schema{
30+
Type: "object",
31+
Properties: map[string]*jsonschema.Schema{
32+
"namespace": {
33+
Type: "string",
34+
Description: "The namespace of the virtual machine",
35+
},
36+
"name": {
37+
Type: "string",
38+
Description: "The name of the virtual machine",
39+
},
40+
"action": {
41+
Type: "string",
42+
Enum: []any{"start", "stop", "restart"},
43+
Description: "The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), or 'restart' (stops then starts the VM)",
44+
},
45+
},
46+
Required: []string{"namespace", "name", "action"},
47+
},
48+
Annotations: api.ToolAnnotations{
49+
Title: "Virtual Machine: Lifecycle",
50+
ReadOnlyHint: ptr.To(false),
51+
DestructiveHint: ptr.To(false),
52+
IdempotentHint: ptr.To(true),
53+
OpenWorldHint: ptr.To(false),
54+
},
55+
},
56+
Handler: lifecycle,
57+
},
58+
}
59+
}
60+
61+
func lifecycle(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
62+
// Parse input parameters
63+
namespace, err := getRequiredString(params, "namespace")
64+
if err != nil {
65+
return api.NewToolCallResult("", err), nil
66+
}
67+
68+
name, err := getRequiredString(params, "name")
69+
if err != nil {
70+
return api.NewToolCallResult("", err), nil
71+
}
72+
73+
action, err := getRequiredString(params, "action")
74+
if err != nil {
75+
return api.NewToolCallResult("", err), nil
76+
}
77+
78+
dynamicClient := params.AccessControlClientset().DynamicClient()
79+
80+
var vm *unstructured.Unstructured
81+
var message string
82+
83+
switch Action(action) {
84+
case ActionStart:
85+
var wasStarted bool
86+
vm, wasStarted, err = kubevirt.StartVM(params.Context, dynamicClient, namespace, name)
87+
if err != nil {
88+
return api.NewToolCallResult("", err), nil
89+
}
90+
if wasStarted {
91+
message = "# VirtualMachine started successfully\n"
92+
} else {
93+
message = fmt.Sprintf("# VirtualMachine '%s' in namespace '%s' is already running\n", name, namespace)
94+
}
95+
96+
case ActionStop:
97+
var wasRunning bool
98+
vm, wasRunning, err = kubevirt.StopVM(params.Context, dynamicClient, namespace, name)
99+
if err != nil {
100+
return api.NewToolCallResult("", err), nil
101+
}
102+
if wasRunning {
103+
message = "# VirtualMachine stopped successfully\n"
104+
} else {
105+
message = fmt.Sprintf("# VirtualMachine '%s' in namespace '%s' is already stopped\n", name, namespace)
106+
}
107+
108+
case ActionRestart:
109+
vm, err = kubevirt.RestartVM(params.Context, dynamicClient, namespace, name)
110+
if err != nil {
111+
return api.NewToolCallResult("", err), nil
112+
}
113+
message = "# VirtualMachine restarted successfully\n"
114+
115+
default:
116+
return api.NewToolCallResult("", fmt.Errorf("invalid action '%s': must be one of 'start', 'stop', 'restart'", action)), nil
117+
}
118+
119+
// Format the output
120+
marshalledYaml, err := output.MarshalYaml([]*unstructured.Unstructured{vm})
121+
if err != nil {
122+
return api.NewToolCallResult("", fmt.Errorf("failed to marshal VirtualMachine: %w", err)), nil
123+
}
124+
125+
return api.NewToolCallResult(message+marshalledYaml, nil), nil
126+
}
127+
128+
func getRequiredString(params api.ToolHandlerParams, key string) (string, error) {
129+
args := params.GetArguments()
130+
val, ok := args[key]
131+
if !ok {
132+
return "", fmt.Errorf("%s parameter required", key)
133+
}
134+
str, ok := val.(string)
135+
if !ok {
136+
return "", fmt.Errorf("%s parameter must be a string", key)
137+
}
138+
return str, nil
139+
}
140+

0 commit comments

Comments
 (0)