Skip to content

Commit c55d4c1

Browse files
awelsclaude
andcommitted
feat(kubevirt): add run policy support to VM lifecycle management
Adds a new run_policy parameter to the vm_lifecycle tool that allows users to control the VM's runStrategy when starting a virtual machine. The parameter supports three policies: - HighAvailability: VM runs continuously (sets runStrategy to Always) - RestartOnFailure: VM restarts on failure (sets runStrategy to RerunOnFailure) - Once: VM runs once and stops after completion (sets runStrategy to Once) The run_policy parameter is optional and defaults to HighAvailability to maintain backward compatibility with existing usage. Changes include: - Updated StartVM function to accept RunPolicy parameter - Added 19 unit tests covering all run policy combinations - Added 3 integration tests for vm_lifecycle tool - Updated tool schema with enum values and documentation - Auto-generated README.md updates Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Signed-off-by: Alexander Wels <awels@redhat.com>
1 parent 28225bc commit c55d4c1

6 files changed

Lines changed: 329 additions & 46 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,9 +509,14 @@ In case multi-cluster support is enabled (default) and you have access to multip
509509
- `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
510510

511511
- **vm_lifecycle** - Manage VirtualMachine lifecycle: start, stop, or restart a VM
512-
- `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)
512+
- `action` (`string`) **(required)** - The lifecycle action to perform: 'start', 'stop', or 'restart'
513513
- `name` (`string`) **(required)** - The name of the virtual machine
514514
- `namespace` (`string`) **(required)** - The namespace of the virtual machine
515+
- `run_policy` (`string`) - The run policy to use when starting a VM (only applies to 'start' action). Options:
516+
- 'HighAvailability': VM runs continuously (sets runStrategy to Always)
517+
- 'RestartOnFailure': VM restarts on failure (sets runStrategy to RerunOnFailure)
518+
- 'Once': VM runs once and stops after completion (sets runStrategy to Once)
519+
Defaults to 'HighAvailability' if not specified.
515520

516521
</details>
517522

pkg/kubevirt/vm.go

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,19 @@ import (
1111

1212
// RunStrategy represents the run strategy for a VirtualMachine
1313
type RunStrategy string
14+
type RunPolicy string
1415

1516
const (
16-
RunStrategyAlways RunStrategy = "Always"
17-
RunStrategyHalted RunStrategy = "Halted"
17+
RunStrategyAlways RunStrategy = "Always"
18+
RunStrategyHalted RunStrategy = "Halted"
19+
RunStrategyManual RunStrategy = "Manual"
20+
RunStrategyRerunOnFailure RunStrategy = "RerunOnFailure"
21+
RunStrategyOnce RunStrategy = "Once"
22+
RunStrategyWaitAsReceiver RunStrategy = "WaitAsReceiver"
23+
24+
RunPolicyHighAvailability RunPolicy = "HighAvailability"
25+
RunPolicyRestartOnFailure RunPolicy = "RestartOnFailure"
26+
RunPolicyOnce RunPolicy = "Once"
1827
)
1928

2029
// GetVirtualMachine retrieves a VirtualMachine by namespace and name
@@ -45,9 +54,17 @@ func UpdateVirtualMachine(ctx context.Context, client dynamic.Interface, vm *uns
4554
Update(ctx, vm, metav1.UpdateOptions{})
4655
}
4756

48-
// StartVM starts a VirtualMachine by updating its runStrategy to Always
49-
// Returns the updated VM and true if the VM was started, false if it was already running
50-
func StartVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) (*unstructured.Unstructured, bool, error) {
57+
// StartVM starts a VirtualMachine by updating its runStrategy based on the runPolicy
58+
// runPolicy can be one of: HighAvailability, RestartOnFailure, Once
59+
// - HighAvailability: The VM will be started if it is not already running, if it is already running the runStrategy
60+
// will be set to Always.
61+
// - RestartOnFailure: The VM will be started if it is not already running and will be restarted if it fails, if it
62+
// is already running the runStrategy will be set to RerunOnFailure.
63+
// - Once: The VM will be started if it is not already running and will be stopped after it completes, if it is already
64+
// running the runStrategy will be set to Once.
65+
// Returns the updated VM and true if the VM was started, or if it was already running and the runStrategy changed.
66+
// Returns false if it was already running and the runStrategy did not change.
67+
func StartVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string, runPolicy RunPolicy) (*unstructured.Unstructured, bool, error) {
5168
// Get the current VirtualMachine
5269
vm, err := GetVirtualMachine(ctx, dynamicClient, namespace, name)
5370
if err != nil {
@@ -60,12 +77,12 @@ func StartVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, na
6077
}
6178

6279
// Check if already running
63-
if found && currentStrategy == RunStrategyAlways {
80+
if found && currentStrategy == getRunStrategyFromRunPolicy(runPolicy) {
6481
return vm, false, nil
6582
}
6683

67-
// Update runStrategy to Always
68-
if err := SetVMRunStrategy(vm, RunStrategyAlways); err != nil {
84+
// Update runStrategy to the appropriate value
85+
if err := SetVMRunStrategy(vm, getRunStrategyFromRunPolicy(runPolicy)); err != nil {
6986
return nil, false, fmt.Errorf("failed to set runStrategy: %w", err)
7087
}
7188

@@ -78,6 +95,23 @@ func StartVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, na
7895
return updatedVM, true, nil
7996
}
8097

98+
// getRunStrategyFromRunPolicy returns the RunStrategy for a given RunPolicy
99+
// - HighAvailability: Always
100+
// - RestartOnFailure: RerunOnFailure
101+
// - Once: Once
102+
// Returns the RunStrategy
103+
func getRunStrategyFromRunPolicy(runPolicy RunPolicy) RunStrategy {
104+
switch runPolicy {
105+
case RunPolicyHighAvailability:
106+
return RunStrategyAlways
107+
case RunPolicyRestartOnFailure:
108+
return RunStrategyRerunOnFailure
109+
case RunPolicyOnce:
110+
return RunStrategyOnce
111+
}
112+
return RunStrategyAlways
113+
}
114+
81115
// StopVM stops a VirtualMachine by updating its runStrategy to Halted
82116
// Returns the updated VM and true if the VM was stopped, false if it was already stopped
83117
func StopVM(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) (*unstructured.Unstructured, bool, error) {

pkg/kubevirt/vm_test.go

Lines changed: 154 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,39 +29,123 @@ func createTestVM(name, namespace string, runStrategy RunStrategy) *unstructured
2929

3030
func TestStartVM(t *testing.T) {
3131
tests := []struct {
32-
name string
33-
initialVM *unstructured.Unstructured
34-
wantStarted bool
35-
wantError bool
36-
errorContains string
32+
name string
33+
runPolicy RunPolicy
34+
initialVM *unstructured.Unstructured
35+
wantStarted bool
36+
wantRunStrategy RunStrategy
37+
wantError bool
38+
errorContains string
3739
}{
40+
// HighAvailability policy tests
3841
{
39-
name: "Start VM that is Halted",
40-
initialVM: createTestVM("test-vm", "default", RunStrategyHalted),
41-
wantStarted: true,
42-
wantError: false,
42+
name: "HighAvailability: Start halted VM",
43+
runPolicy: RunPolicyHighAvailability,
44+
initialVM: createTestVM("test-vm", "default", RunStrategyHalted),
45+
wantStarted: true,
46+
wantRunStrategy: RunStrategyAlways,
4347
},
4448
{
45-
name: "Start VM that is already running (Always)",
46-
initialVM: createTestVM("test-vm", "default", RunStrategyAlways),
47-
wantStarted: false,
48-
wantError: false,
49+
name: "HighAvailability: VM already running with Always",
50+
runPolicy: RunPolicyHighAvailability,
51+
initialVM: createTestVM("test-vm", "default", RunStrategyAlways),
52+
wantStarted: false,
53+
wantRunStrategy: RunStrategyAlways,
4954
},
5055
{
51-
name: "Start VM without runStrategy",
52-
initialVM: &unstructured.Unstructured{
53-
Object: map[string]interface{}{
54-
"apiVersion": "kubevirt.io/v1",
55-
"kind": "VirtualMachine",
56-
"metadata": map[string]interface{}{
57-
"name": "test-vm",
58-
"namespace": "default",
59-
},
60-
"spec": map[string]interface{}{},
61-
},
62-
},
63-
wantStarted: true,
64-
wantError: false,
56+
name: "HighAvailability: Change from RerunOnFailure to Always",
57+
runPolicy: RunPolicyHighAvailability,
58+
initialVM: createTestVM("test-vm", "default", RunStrategyRerunOnFailure),
59+
wantStarted: true,
60+
wantRunStrategy: RunStrategyAlways,
61+
},
62+
{
63+
name: "HighAvailability: Change from Once to Always",
64+
runPolicy: RunPolicyHighAvailability,
65+
initialVM: createTestVM("test-vm", "default", RunStrategyOnce),
66+
wantStarted: true,
67+
wantRunStrategy: RunStrategyAlways,
68+
},
69+
{
70+
name: "HighAvailability: Change from Manual to Always",
71+
runPolicy: RunPolicyHighAvailability,
72+
initialVM: createTestVM("test-vm", "default", RunStrategyManual),
73+
wantStarted: true,
74+
wantRunStrategy: RunStrategyAlways,
75+
},
76+
77+
// RestartOnFailure policy tests
78+
{
79+
name: "RestartOnFailure: Start halted VM",
80+
runPolicy: RunPolicyRestartOnFailure,
81+
initialVM: createTestVM("test-vm", "default", RunStrategyHalted),
82+
wantStarted: true,
83+
wantRunStrategy: RunStrategyRerunOnFailure,
84+
},
85+
{
86+
name: "RestartOnFailure: VM already running with RerunOnFailure",
87+
runPolicy: RunPolicyRestartOnFailure,
88+
initialVM: createTestVM("test-vm", "default", RunStrategyRerunOnFailure),
89+
wantStarted: false,
90+
wantRunStrategy: RunStrategyRerunOnFailure,
91+
},
92+
{
93+
name: "RestartOnFailure: Change from Always to RerunOnFailure",
94+
runPolicy: RunPolicyRestartOnFailure,
95+
initialVM: createTestVM("test-vm", "default", RunStrategyAlways),
96+
wantStarted: true,
97+
wantRunStrategy: RunStrategyRerunOnFailure,
98+
},
99+
{
100+
name: "RestartOnFailure: Change from Once to RerunOnFailure",
101+
runPolicy: RunPolicyRestartOnFailure,
102+
initialVM: createTestVM("test-vm", "default", RunStrategyOnce),
103+
wantStarted: true,
104+
wantRunStrategy: RunStrategyRerunOnFailure,
105+
},
106+
{
107+
name: "RestartOnFailure: Change from Manual to RerunOnFailure",
108+
runPolicy: RunPolicyRestartOnFailure,
109+
initialVM: createTestVM("test-vm", "default", RunStrategyManual),
110+
wantStarted: true,
111+
wantRunStrategy: RunStrategyRerunOnFailure,
112+
},
113+
114+
// Once policy tests
115+
{
116+
name: "Once: Start halted VM",
117+
runPolicy: RunPolicyOnce,
118+
initialVM: createTestVM("test-vm", "default", RunStrategyHalted),
119+
wantStarted: true,
120+
wantRunStrategy: RunStrategyOnce,
121+
},
122+
{
123+
name: "Once: VM already running with Once",
124+
runPolicy: RunPolicyOnce,
125+
initialVM: createTestVM("test-vm", "default", RunStrategyOnce),
126+
wantStarted: false,
127+
wantRunStrategy: RunStrategyOnce,
128+
},
129+
{
130+
name: "Once: Change from Always to Once",
131+
runPolicy: RunPolicyOnce,
132+
initialVM: createTestVM("test-vm", "default", RunStrategyAlways),
133+
wantStarted: true,
134+
wantRunStrategy: RunStrategyOnce,
135+
},
136+
{
137+
name: "Once: Change from RerunOnFailure to Once",
138+
runPolicy: RunPolicyOnce,
139+
initialVM: createTestVM("test-vm", "default", RunStrategyRerunOnFailure),
140+
wantStarted: true,
141+
wantRunStrategy: RunStrategyOnce,
142+
},
143+
{
144+
name: "Once: Change from Manual to Once",
145+
runPolicy: RunPolicyOnce,
146+
initialVM: createTestVM("test-vm", "default", RunStrategyManual),
147+
wantStarted: true,
148+
wantRunStrategy: RunStrategyOnce,
65149
},
66150
}
67151

@@ -71,7 +155,7 @@ func TestStartVM(t *testing.T) {
71155
client := fake.NewSimpleDynamicClient(scheme, tt.initialVM)
72156
ctx := context.Background()
73157

74-
vm, wasStarted, err := StartVM(ctx, client, tt.initialVM.GetNamespace(), tt.initialVM.GetName())
158+
vm, wasStarted, err := StartVM(ctx, client, tt.initialVM.GetNamespace(), tt.initialVM.GetName(), tt.runPolicy)
75159

76160
if tt.wantError {
77161
if err == nil {
@@ -98,7 +182,7 @@ func TestStartVM(t *testing.T) {
98182
t.Errorf("wasStarted = %v, want %v", wasStarted, tt.wantStarted)
99183
}
100184

101-
// Verify the VM's runStrategy is Always
185+
// Verify the VM's runStrategy matches expected
102186
strategy, found, err := GetVMRunStrategy(vm)
103187
if err != nil {
104188
t.Errorf("Failed to get runStrategy: %v", err)
@@ -108,8 +192,8 @@ func TestStartVM(t *testing.T) {
108192
t.Errorf("runStrategy not found")
109193
return
110194
}
111-
if strategy != RunStrategyAlways {
112-
t.Errorf("Strategy = %q, want %q", strategy, RunStrategyAlways)
195+
if strategy != tt.wantRunStrategy {
196+
t.Errorf("Strategy = %q, want %q", strategy, tt.wantRunStrategy)
113197
}
114198
})
115199
}
@@ -120,7 +204,7 @@ func TestStartVMNotFound(t *testing.T) {
120204
client := fake.NewSimpleDynamicClient(scheme)
121205
ctx := context.Background()
122206

123-
_, _, err := StartVM(ctx, client, "default", "non-existent-vm")
207+
_, _, err := StartVM(ctx, client, "default", "non-existent-vm", RunPolicyHighAvailability)
124208
if err == nil {
125209
t.Errorf("Expected error for non-existent VM, got nil")
126210
return
@@ -327,3 +411,41 @@ func TestRestartVMNotFound(t *testing.T) {
327411
t.Errorf("Error = %v, want to contain 'failed to get VirtualMachine'", err)
328412
}
329413
}
414+
415+
func TestGetRunStrategyFromRunPolicy(t *testing.T) {
416+
tests := []struct {
417+
name string
418+
runPolicy RunPolicy
419+
wantRunStrategy RunStrategy
420+
}{
421+
{
422+
name: "HighAvailability maps to Always",
423+
runPolicy: RunPolicyHighAvailability,
424+
wantRunStrategy: RunStrategyAlways,
425+
},
426+
{
427+
name: "RestartOnFailure maps to RerunOnFailure",
428+
runPolicy: RunPolicyRestartOnFailure,
429+
wantRunStrategy: RunStrategyRerunOnFailure,
430+
},
431+
{
432+
name: "Once maps to Once",
433+
runPolicy: RunPolicyOnce,
434+
wantRunStrategy: RunStrategyOnce,
435+
},
436+
{
437+
name: "Invalid policy defaults to Always",
438+
runPolicy: RunPolicy("invalid"),
439+
wantRunStrategy: RunStrategyAlways,
440+
},
441+
}
442+
443+
for _, tt := range tests {
444+
t.Run(tt.name, func(t *testing.T) {
445+
got := getRunStrategyFromRunPolicy(tt.runPolicy)
446+
if got != tt.wantRunStrategy {
447+
t.Errorf("getRunStrategyFromRunPolicy(%q) = %q, want %q", tt.runPolicy, got, tt.wantRunStrategy)
448+
}
449+
})
450+
}
451+
}

0 commit comments

Comments
 (0)