Skip to content
Merged
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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,16 @@ In case multi-cluster support is enabled (default) and you have access to multip

</details>

<details>

<summary>kubevirt</summary>

- **vm-troubleshoot** - Generate a step-by-step troubleshooting guide for diagnosing VirtualMachine issues
- `namespace` (`string`) **(required)** - The namespace of the VirtualMachine to troubleshoot
- `name` (`string`) **(required)** - The name of the VirtualMachine to troubleshoot

</details>


<!-- AVAILABLE-TOOLSETS-PROMPTS-END -->

Expand Down
6 changes: 6 additions & 0 deletions evals/tasks/kubevirt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ KubeVirt-focused MCP tasks live here. Each folder under this directory represent
- **[hard] update-vm-resources** - Update VM CPU and memory resources
- **Prompt:** *A VirtualMachine named test-vm-update exists in the vm-test namespace. It currently has 1 vCPU and 2Gi of memory. Please update the VirtualMachine to add an additional vCPU (making it 2 vCPUs total) and increase the memory to at least 3Gi.*

### VM Troubleshooting

- **[hard] troubleshoot-vm** - Use the vm-troubleshoot prompt to diagnose VirtualMachine issues
- **Prompt:** *There is a VirtualMachine named "broken-vm" in the vm-test namespace that is not working correctly. Please use the vm-troubleshoot prompt to diagnose the issue with this VirtualMachine. Follow the troubleshooting guide and report your findings, including the root cause and recommended action.*
- **Tests:** Agent's ability to use MCP prompts for guided troubleshooting workflows

## Helper Scripts

Many tasks rely on helper scripts located in `evals/tasks/kubevirt/helpers/`:
Expand Down
133 changes: 133 additions & 0 deletions evals/tasks/kubevirt/troubleshoot-vm/task.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
kind: Task
metadata:
name: "troubleshoot-vm"
difficulty: hard
description: "Use the vm-troubleshoot prompt to diagnose and fix VirtualMachine issues"
steps:
setup:
inline: |-
#!/usr/bin/env bash
NS="${EVAL_NAMESPACE:-vm-test}"
kubectl delete namespace "$NS" --ignore-not-found
kubectl create namespace "$NS"

# Create a VM that references a missing Secret for cloud-init
# The agent should identify the missing Secret and create it to fix the VM
cat <<EOF | kubectl apply -f -
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: broken-vm
namespace: $NS
labels:
app: broken-vm
spec:
runStrategy: Always
template:
spec:
domain:
devices:
disks:
- name: containerdisk
disk:
bus: virtio
- name: cloudinit
disk:
bus: virtio
resources:
requests:
memory: 2Gi
terminationGracePeriodSeconds: 0
volumes:
- name: containerdisk
containerDisk:
image: quay.io/containerdisks/fedora:latest
- name: cloudinit
cloudInitNoCloud:
secretRef:
name: vm-cloud-init
EOF

# Wait for VM to be created
kubectl wait --for=create vm/broken-vm -n "$NS" --timeout=10s
echo "VM created with missing Secret reference - waiting for failure state"

# Give some time for the VM to attempt to start and fail
sleep 10
verify:
inline: |-
#!/usr/bin/env bash
NS="${EVAL_NAMESPACE:-vm-test}"

echo "=== Verification: Checking if agent fixed the VM ==="

# Verify that the VM still exists
if ! kubectl get virtualmachine broken-vm -n "$NS" > /dev/null 2>&1; then
echo "✗ VirtualMachine broken-vm no longer exists"
exit 1
fi
echo "✓ VirtualMachine broken-vm exists"

# Check if the Secret was created by the agent
if kubectl get secret vm-cloud-init -n "$NS" > /dev/null 2>&1; then
echo "✓ Secret vm-cloud-init was created"
else
echo "✗ Secret vm-cloud-init was not created - agent did not fix the issue"
exit 1
fi

# Wait for VM to become ready after the fix (with timeout)
echo "Waiting for VM to become ready after fix..."
READY=false
for i in {1..30}; do
VM_READY=$(kubectl get virtualmachine broken-vm -n "$NS" -o jsonpath='{.status.ready}' 2>/dev/null || echo "false")
if [[ "$VM_READY" == "true" ]]; then
READY=true
break
fi
sleep 5
done

if [[ "$READY" == "true" ]]; then
echo "✓ VM is now ready - fix was successful!"
else
VM_STATUS=$(kubectl get virtualmachine broken-vm -n "$NS" -o jsonpath='{.status.printableStatus}' 2>/dev/null || echo "Unknown")
echo "⚠ VM is not ready yet (status: $VM_STATUS) - fix may need more time or was incomplete"
# Don't fail here as the VM may still be starting up
fi

# Check if virt-launcher pod exists and is running
LAUNCHER_POD=$(kubectl get pods -n "$NS" -l kubevirt.io=virt-launcher,vm.kubevirt.io/name=broken-vm -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
if [[ -n "$LAUNCHER_POD" ]]; then
POD_PHASE=$(kubectl get pod "$LAUNCHER_POD" -n "$NS" -o jsonpath='{.status.phase}' 2>/dev/null || true)
echo "✓ virt-launcher pod exists (phase: $POD_PHASE)"
else
echo "ℹ No virt-launcher pod found yet"
fi

echo ""
echo "=== Troubleshooting and Fix Eval Complete ==="
echo "The agent should have:"
echo " 1. Used the vm-troubleshoot prompt with namespace=$NS and name=broken-vm"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also missing from the Task API at the moment IMHO, we can define this in Evals but I also think each Task should be able to assert that tools and/or prompts are called.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lyarwood +1 here - we have an open discussion trying to figure out how we want to solve this: mcpchecker/mcpchecker#126

Interested in hearing if you have any thoughts 😄

echo " 2. Identified the root cause (missing Secret vm-cloud-init)"
echo " 3. Created the missing Secret to fix the VM"
echo " 4. Reported the action taken and result"
echo ""

exit 0
cleanup:
inline: |-
#!/usr/bin/env bash
NS="${EVAL_NAMESPACE:-vm-test}"
kubectl delete virtualmachine broken-vm -n "$NS" --ignore-not-found
kubectl delete secret vm-cloud-init -n "$NS" --ignore-not-found
kubectl delete namespace "$NS" --ignore-not-found
prompt:
inline: |-
There is a VirtualMachine named "broken-vm" in the ${EVAL_NAMESPACE:-vm-test} namespace that is not working correctly.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried running this with mcpchecker? IIRC it no longer supports bash substitutions, something we can address in the project but it will lead to this being passed directly to the agent/model in it's current form.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mcpchecker passed. Since this substitution is in all kubevirt's eval tasks, I would update it in different PR in all tasks.


Please use the vm-troubleshoot prompt to diagnose the issue with this VirtualMachine.
Follow the troubleshooting guide to identify the problem, fix it, and report your findings including:
- The root cause of the issue
- What action you took to fix it
- Whether the fix was successful
97 changes: 97 additions & 0 deletions pkg/kubevirt/gvr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package kubevirt

import (
"k8s.io/apimachinery/pkg/runtime/schema"
)

// KubeVirt core resources
var (
// VirtualMachineGVK is the GroupVersionKind for VirtualMachine resources
VirtualMachineGVK = schema.GroupVersionKind{
Group: "kubevirt.io",
Version: "v1",
Kind: "VirtualMachine",
}

// VirtualMachineGVR is the GroupVersionResource for VirtualMachine resources
VirtualMachineGVR = schema.GroupVersionResource{
Group: "kubevirt.io",
Version: "v1",
Resource: "virtualmachines",
}

// VirtualMachineInstanceGVR is the GroupVersionResource for VirtualMachineInstance resources
VirtualMachineInstanceGVR = schema.GroupVersionResource{
Group: "kubevirt.io",
Version: "v1",
Resource: "virtualmachineinstances",
}
)

// CDI (Containerized Data Importer) resources
var (
// DataVolumeGVR is the GroupVersionResource for DataVolume resources
DataVolumeGVR = schema.GroupVersionResource{
Group: "cdi.kubevirt.io",
Version: "v1beta1",
Resource: "datavolumes",
}

// DataSourceGVR is the GroupVersionResource for DataSource resources
DataSourceGVR = schema.GroupVersionResource{
Group: "cdi.kubevirt.io",
Version: "v1beta1",
Resource: "datasources",
}
)

// Instancetype resources
var (
// VirtualMachineClusterInstancetypeGVR is the GroupVersionResource for cluster-scoped VirtualMachineClusterInstancetype resources
VirtualMachineClusterInstancetypeGVR = schema.GroupVersionResource{
Group: "instancetype.kubevirt.io",
Version: "v1beta1",
Resource: "virtualmachineclusterinstancetypes",
}

// VirtualMachineInstancetypeGVR is the GroupVersionResource for namespaced VirtualMachineInstancetype resources
VirtualMachineInstancetypeGVR = schema.GroupVersionResource{
Group: "instancetype.kubevirt.io",
Version: "v1beta1",
Resource: "virtualmachineinstancetypes",
}
)

// Preference resources
var (
// VirtualMachineClusterPreferenceGVR is the GroupVersionResource for cluster-scoped VirtualMachineClusterPreference resources
VirtualMachineClusterPreferenceGVR = schema.GroupVersionResource{
Group: "instancetype.kubevirt.io",
Version: "v1beta1",
Resource: "virtualmachineclusterpreferences",
}

// VirtualMachinePreferenceGVR is the GroupVersionResource for namespaced VirtualMachinePreference resources
VirtualMachinePreferenceGVR = schema.GroupVersionResource{
Group: "instancetype.kubevirt.io",
Version: "v1beta1",
Resource: "virtualmachinepreferences",
}
)

// Kubernetes core resources
var (
// PersistentVolumeClaimGVR is the GroupVersionResource for PersistentVolumeClaim resources
PersistentVolumeClaimGVR = schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "persistentvolumeclaims",
}

// PodGVR is the GroupVersionResource for Pod resources
PodGVR = schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "pods",
}
)
43 changes: 6 additions & 37 deletions pkg/kubevirt/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
)

Expand Down Expand Up @@ -63,12 +62,6 @@ func SearchDataSources(ctx context.Context, dynamicClient dynamic.Interface) map

// collectDataSources collects DataSources from well-known namespaces and all namespaces
func collectDataSources(ctx context.Context, dynamicClient dynamic.Interface) map[string]DataSourceInfo {
gvr := schema.GroupVersionResource{
Group: "cdi.kubevirt.io",
Version: "v1beta1",
Resource: "datasources",
}

// Try to list DataSources from well-known namespaces first
wellKnownNamespaces := []string{
"openshift-virtualization-os-images",
Expand All @@ -77,7 +70,7 @@ func collectDataSources(ctx context.Context, dynamicClient dynamic.Interface) ma

var items []unstructured.Unstructured
for _, ns := range wellKnownNamespaces {
list, err := dynamicClient.Resource(gvr).Namespace(ns).List(ctx, metav1.ListOptions{})
list, err := dynamicClient.Resource(DataSourceGVR).Namespace(ns).List(ctx, metav1.ListOptions{})
if err != nil {
slog.Debug("failed to list DataSources in well-known namespace", "namespace", ns, "error", err)
} else {
Expand All @@ -86,7 +79,7 @@ func collectDataSources(ctx context.Context, dynamicClient dynamic.Interface) ma
}

// List DataSources from all namespaces
list, err := dynamicClient.Resource(gvr).List(ctx, metav1.ListOptions{})
list, err := dynamicClient.Resource(DataSourceGVR).List(ctx, metav1.ListOptions{})
if err != nil {
slog.Debug("failed to list DataSources cluster-wide", "error", err)
} else {
Expand Down Expand Up @@ -137,14 +130,8 @@ func collectDataSources(ctx context.Context, dynamicClient dynamic.Interface) ma
// or if API calls fail.
func SearchPreferences(ctx context.Context, dynamicClient dynamic.Interface, namespace string) []PreferenceInfo {
// Search for cluster-wide VirtualMachineClusterPreferences
clusterPreferenceGVR := schema.GroupVersionResource{
Group: "instancetype.kubevirt.io",
Version: "v1beta1",
Resource: "virtualmachineclusterpreferences",
}

var results []PreferenceInfo
clusterList, err := dynamicClient.Resource(clusterPreferenceGVR).List(ctx, metav1.ListOptions{})
clusterList, err := dynamicClient.Resource(VirtualMachineClusterPreferenceGVR).List(ctx, metav1.ListOptions{})
if err != nil {
slog.Debug("failed to list cluster-scoped VirtualMachineClusterPreferences", "error", err)
} else {
Expand All @@ -157,13 +144,7 @@ func SearchPreferences(ctx context.Context, dynamicClient dynamic.Interface, nam
}

// Search for namespaced VirtualMachinePreferences
namespacedPreferenceGVR := schema.GroupVersionResource{
Group: "instancetype.kubevirt.io",
Version: "v1beta1",
Resource: "virtualmachinepreferences",
}

namespacedList, err := dynamicClient.Resource(namespacedPreferenceGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
namespacedList, err := dynamicClient.Resource(VirtualMachinePreferenceGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
slog.Debug("failed to list namespaced VirtualMachinePreferences", "namespace", namespace, "error", err)
} else {
Expand Down Expand Up @@ -194,14 +175,8 @@ func SearchPreferences(ctx context.Context, dynamicClient dynamic.Interface, nam
// or if API calls fail.
func SearchInstancetypes(ctx context.Context, dynamicClient dynamic.Interface, namespace string) []InstancetypeInfo {
// Search for cluster-wide VirtualMachineClusterInstancetypes
clusterInstancetypeGVR := schema.GroupVersionResource{
Group: "instancetype.kubevirt.io",
Version: "v1beta1",
Resource: "virtualmachineclusterinstancetypes",
}

var results []InstancetypeInfo
clusterList, err := dynamicClient.Resource(clusterInstancetypeGVR).List(ctx, metav1.ListOptions{})
clusterList, err := dynamicClient.Resource(VirtualMachineClusterInstancetypeGVR).List(ctx, metav1.ListOptions{})
if err != nil {
slog.Debug("failed to list cluster-scoped VirtualMachineClusterInstancetypes", "error", err)
} else {
Expand All @@ -215,13 +190,7 @@ func SearchInstancetypes(ctx context.Context, dynamicClient dynamic.Interface, n
}

// Search for namespaced VirtualMachineInstancetypes
namespacedInstancetypeGVR := schema.GroupVersionResource{
Group: "instancetype.kubevirt.io",
Version: "v1beta1",
Resource: "virtualmachineinstancetypes",
}

namespacedList, err := dynamicClient.Resource(namespacedInstancetypeGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
namespacedList, err := dynamicClient.Resource(VirtualMachineInstancetypeGVR).Namespace(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
slog.Debug("failed to list namespaced VirtualMachineInstancetypes", "namespace", namespace, "error", err)
} else {
Expand Down
17 changes: 0 additions & 17 deletions pkg/kubevirt/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
)

Expand All @@ -18,22 +17,6 @@ const (
RunStrategyHalted RunStrategy = "Halted"
)

var (
// VirtualMachineGVK is the GroupVersionKind for VirtualMachine resources
VirtualMachineGVK = schema.GroupVersionKind{
Group: "kubevirt.io",
Version: "v1",
Kind: "VirtualMachine",
}

// VirtualMachineGVR is the GroupVersionResource for VirtualMachine resources
VirtualMachineGVR = schema.GroupVersionResource{
Group: "kubevirt.io",
Version: "v1",
Resource: "virtualmachines",
}
)

// GetVirtualMachine retrieves a VirtualMachine by namespace and name
func GetVirtualMachine(ctx context.Context, client dynamic.Interface, namespace, name string) (*unstructured.Unstructured, error) {
return client.Resource(VirtualMachineGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
Expand Down
Loading