Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,8 +524,8 @@ In case multi-cluster support is enabled (default) and you have access to multip
- `storage` (`string`) - Optional storage size for the VM's root disk when using DataSources (e.g., '30Gi', '50Gi', '100Gi'). Defaults to 30Gi. Ignored when using container disks.
- `workload` (`string`) - The workload for the VM. Accepts OS names (e.g., 'fedora' (default), 'ubuntu', 'centos', 'centos-stream', 'debian', 'rhel', 'opensuse', 'opensuse-tumbleweed', 'opensuse-leap') or full container disk image URLs

- **vm_lifecycle** - Manage VirtualMachine lifecycle: start, stop, or restart a VM
- `action` (`string`) **(required)** - The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), or 'restart' (stops then starts the VM)
- **vm_lifecycle** - Manage VirtualMachine lifecycle: start, stop, restart, or delete a VM
- `action` (`string`) **(required)** - The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), or 'restart' (stops then starts the VM), or 'delete' (deletes the VM)
- `name` (`string`) **(required)** - The name of the virtual machine
- `namespace` (`string`) **(required)** - The namespace of the virtual machine

Expand Down
7 changes: 7 additions & 0 deletions pkg/kubevirt/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ func UpdateVirtualMachine(ctx context.Context, client dynamic.Interface, vm *uns
Update(ctx, vm, metav1.UpdateOptions{})
}

// DeleteVirtualMachine deletes a VirtualMachine by namespace and name
func DeleteVirtualMachine(ctx context.Context, client dynamic.Interface, namespace, name string) error {
return client.Resource(VirtualMachineGVR).
Namespace(namespace).
Delete(ctx, name, metav1.DeleteOptions{})
}

// StartVM starts a VirtualMachine by updating its runStrategy to Always
// Returns the updated VM and true if the VM was started, false if it was already running
func StartVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) (*unstructured.Unstructured, bool, error) {
Expand Down
38 changes: 38 additions & 0 deletions pkg/kubevirt/vm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,41 @@ func TestRestartVMNotFound(t *testing.T) {
t.Errorf("Error = %v, want to contain 'failed to get VirtualMachine'", err)
}
}

func TestDeleteVM(t *testing.T) {
scheme := runtime.NewScheme()
initialVM := createTestVM("test-vm", "default", RunStrategyAlways)
client := fake.NewSimpleDynamicClient(scheme, initialVM)
ctx := context.Background()

err := DeleteVirtualMachine(ctx, client, "default", "test-vm")
if err != nil {
t.Errorf("Unexpected error during deletion: %v", err)
return
}

// Verify the VM is deleted
_, err = GetVirtualMachine(ctx, client, "default", "test-vm")
if err == nil {
t.Errorf("Expected error when getting deleted VM, got nil")
return
}
if !strings.Contains(err.Error(), "not found") {
t.Errorf("Error = %v, want to contain 'not found'", err)
}
}

func TestDeleteVMNotFound(t *testing.T) {
scheme := runtime.NewScheme()
client := fake.NewSimpleDynamicClient(scheme)
ctx := context.Background()

err := DeleteVirtualMachine(ctx, client, "default", "non-existent-vm")
if err == nil {
t.Errorf("Expected error for non-existent VM deletion, got nil")
return
}
if !strings.Contains(err.Error(), "not found") {
t.Errorf("Error = %v, want to contain 'not found'", err)
}
}
24 changes: 23 additions & 1 deletion pkg/mcp/kubevirt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -651,8 +651,30 @@ func (s *KubevirtSuite) TestVMLifecycle() {
})
})

s.Run("vm_lifecycle action=delete on running VM", func() {
toolResult, err := s.CallTool("vm_lifecycle", map[string]interface{}{
"name": "test-vm-lifecycle",
"namespace": "default",
"action": "delete",
})
s.Run("no error", func() {
s.Nilf(err, "call tool failed %v", err)
s.Falsef(toolResult.IsError, "call tool failed")
})
var decodedResult []unstructured.Unstructured
err = yaml.Unmarshal([]byte(toolResult.Content[0].(mcp.TextContent).Text), &decodedResult)
s.Run("returns yaml content", func() {
s.Nilf(err, "invalid tool result content %v", err)
s.Truef(strings.HasPrefix(toolResult.Content[0].(mcp.TextContent).Text, "# VirtualMachine deleted successfully"),
"Expected success message, got %v", toolResult.Content[0].(mcp.TextContent).Text)
s.Require().Lenf(decodedResult, 1, "invalid resource count, expected 1, got %v", len(decodedResult))
s.Equal("test-vm-lifecycle", decodedResult[0].GetName(), "invalid resource name")
s.Equal("default", decodedResult[0].GetNamespace(), "invalid resource namespace")
})
})

s.Run("vm_lifecycle on non-existent VM", func() {
for _, action := range []string{"start", "stop", "restart"} {
for _, action := range []string{"start", "stop", "restart", "delete"} {
s.Run("action="+action, func() {
toolResult, err := s.CallTool("vm_lifecycle", map[string]interface{}{
"name": "non-existent-vm",
Expand Down
7 changes: 4 additions & 3 deletions pkg/mcp/testdata/toolsets-kubevirt-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,17 @@
"destructiveHint": true,
"openWorldHint": false
},
"description": "Manage VirtualMachine lifecycle: start, stop, or restart a VM",
"description": "Manage VirtualMachine lifecycle: start, stop, restart, or delete a VM",
"inputSchema": {
"type": "object",
"properties": {
"action": {
"description": "The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), or 'restart' (stops then starts the VM)",
"description": "The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), or 'restart' (stops then starts the VM), or 'delete' (deletes the VM)",
"enum": [
"start",
"stop",
"restart"
"restart",
"delete"
],
"type": "string"
},
Expand Down
13 changes: 10 additions & 3 deletions pkg/toolsets/kubevirt/vm/lifecycle/tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ const (
ActionStart Action = "start"
ActionStop Action = "stop"
ActionRestart Action = "restart"
ActionDelete Action = "delete"
)

func Tools() []api.ServerTool {
return []api.ServerTool{
{
Tool: api.Tool{
Name: "vm_lifecycle",
Description: "Manage VirtualMachine lifecycle: start, stop, or restart a VM",
Description: "Manage VirtualMachine lifecycle: start, stop, restart, or delete a VM",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
Expand All @@ -39,8 +40,8 @@ func Tools() []api.ServerTool {
},
"action": {
Type: "string",
Enum: []any{string(ActionStart), string(ActionStop), string(ActionRestart)},
Description: "The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), or 'restart' (stops then starts the VM)",
Enum: []any{string(ActionStart), string(ActionStop), string(ActionRestart), string(ActionDelete)},
Description: "The lifecycle action to perform: 'start' (changes runStrategy to Always), 'stop' (changes runStrategy to Halted), 'restart' (stops then starts the VM), or 'delete' (deletes the VM)",
},
},
Required: []string{"namespace", "name", "action"},
Expand Down Expand Up @@ -111,6 +112,12 @@ func lifecycle(params api.ToolHandlerParams) (*api.ToolCallResult, error) {
return api.NewToolCallResult("", err), nil
}
message = "# VirtualMachine restarted successfully\n"
case ActionDelete:
err = kubevirt.DeleteVirtualMachine(params.Context, dynamicClient, namespace, name)
if err != nil {
return api.NewToolCallResult("", err), nil
}
message = "# VirtualMachine deleted successfully\n"

default:
return api.NewToolCallResult("", fmt.Errorf("invalid action '%s': must be one of 'start', 'stop', 'restart'", action)), nil
Expand Down
Loading