kubectl-mcp-server 1.15.0__py3-none-any.whl → 1.17.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/METADATA +34 -13
- kubectl_mcp_server-1.17.0.dist-info/RECORD +75 -0
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/cli/cli.py +83 -9
- kubectl_mcp_tool/cli/output.py +14 -0
- kubectl_mcp_tool/config/__init__.py +46 -0
- kubectl_mcp_tool/config/loader.py +386 -0
- kubectl_mcp_tool/config/schema.py +184 -0
- kubectl_mcp_tool/crd_detector.py +247 -0
- kubectl_mcp_tool/k8s_config.py +19 -0
- kubectl_mcp_tool/mcp_server.py +246 -8
- kubectl_mcp_tool/observability/__init__.py +59 -0
- kubectl_mcp_tool/observability/metrics.py +223 -0
- kubectl_mcp_tool/observability/stats.py +255 -0
- kubectl_mcp_tool/observability/tracing.py +335 -0
- kubectl_mcp_tool/prompts/__init__.py +43 -0
- kubectl_mcp_tool/prompts/builtin.py +695 -0
- kubectl_mcp_tool/prompts/custom.py +298 -0
- kubectl_mcp_tool/prompts/prompts.py +180 -4
- kubectl_mcp_tool/safety.py +155 -0
- kubectl_mcp_tool/tools/__init__.py +20 -0
- kubectl_mcp_tool/tools/backup.py +881 -0
- kubectl_mcp_tool/tools/capi.py +727 -0
- kubectl_mcp_tool/tools/certs.py +709 -0
- kubectl_mcp_tool/tools/cilium.py +582 -0
- kubectl_mcp_tool/tools/cluster.py +384 -0
- kubectl_mcp_tool/tools/gitops.py +552 -0
- kubectl_mcp_tool/tools/keda.py +464 -0
- kubectl_mcp_tool/tools/kiali.py +652 -0
- kubectl_mcp_tool/tools/kubevirt.py +803 -0
- kubectl_mcp_tool/tools/policy.py +554 -0
- kubectl_mcp_tool/tools/rollouts.py +790 -0
- tests/test_browser.py +2 -2
- tests/test_config.py +386 -0
- tests/test_ecosystem.py +331 -0
- tests/test_mcp_integration.py +251 -0
- tests/test_observability.py +521 -0
- tests/test_prompts.py +716 -0
- tests/test_safety.py +218 -0
- tests/test_tools.py +70 -8
- kubectl_mcp_server-1.15.0.dist-info/RECORD +0 -49
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.15.0.dist-info → kubectl_mcp_server-1.17.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
"""KubeVirt VM lifecycle toolset for kubectl-mcp-server.
|
|
2
|
+
|
|
3
|
+
Provides tools for managing virtual machines on Kubernetes via KubeVirt.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
import json
|
|
8
|
+
from typing import Dict, Any, List
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from fastmcp import FastMCP
|
|
12
|
+
from fastmcp.tools import ToolAnnotations
|
|
13
|
+
except ImportError:
|
|
14
|
+
from mcp.server.fastmcp import FastMCP
|
|
15
|
+
from mcp.types import ToolAnnotations
|
|
16
|
+
|
|
17
|
+
from ..k8s_config import _get_kubectl_context_args
|
|
18
|
+
from ..crd_detector import crd_exists
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# KubeVirt CRDs
|
|
22
|
+
VM_CRD = "virtualmachines.kubevirt.io"
|
|
23
|
+
VMI_CRD = "virtualmachineinstances.kubevirt.io"
|
|
24
|
+
VMIPRESET_CRD = "virtualmachineinstancepresets.kubevirt.io"
|
|
25
|
+
VMIRS_CRD = "virtualmachineinstancereplicasets.kubevirt.io"
|
|
26
|
+
VMPOOL_CRD = "virtualmachinepools.pool.kubevirt.io"
|
|
27
|
+
DATASOURCE_CRD = "datasources.cdi.kubevirt.io"
|
|
28
|
+
DATAVOLUME_CRD = "datavolumes.cdi.kubevirt.io"
|
|
29
|
+
VMCLONE_CRD = "virtualmachineclones.clone.kubevirt.io"
|
|
30
|
+
INSTANCETYPE_CRD = "virtualmachineinstancetypes.instancetype.kubevirt.io"
|
|
31
|
+
CLUSTERINSTANCETYPE_CRD = "virtualmachineclusterinstancetypes.instancetype.kubevirt.io"
|
|
32
|
+
PREFERENCE_CRD = "virtualmachinepreferences.instancetype.kubevirt.io"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _run_kubectl(args: List[str], context: str = "") -> Dict[str, Any]:
|
|
36
|
+
"""Run kubectl command and return result."""
|
|
37
|
+
cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
|
|
38
|
+
try:
|
|
39
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
40
|
+
if result.returncode == 0:
|
|
41
|
+
return {"success": True, "output": result.stdout}
|
|
42
|
+
return {"success": False, "error": result.stderr}
|
|
43
|
+
except subprocess.TimeoutExpired:
|
|
44
|
+
return {"success": False, "error": "Command timed out"}
|
|
45
|
+
except Exception as e:
|
|
46
|
+
return {"success": False, "error": str(e)}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_resources(kind: str, namespace: str = "", context: str = "", label_selector: str = "") -> List[Dict]:
|
|
50
|
+
"""Get Kubernetes resources of a specific kind."""
|
|
51
|
+
args = ["get", kind, "-o", "json"]
|
|
52
|
+
if namespace:
|
|
53
|
+
args.extend(["-n", namespace])
|
|
54
|
+
else:
|
|
55
|
+
args.append("-A")
|
|
56
|
+
if label_selector:
|
|
57
|
+
args.extend(["-l", label_selector])
|
|
58
|
+
|
|
59
|
+
result = _run_kubectl(args, context)
|
|
60
|
+
if result["success"]:
|
|
61
|
+
try:
|
|
62
|
+
data = json.loads(result["output"])
|
|
63
|
+
return data.get("items", [])
|
|
64
|
+
except json.JSONDecodeError:
|
|
65
|
+
return []
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _virtctl_available() -> bool:
|
|
70
|
+
"""Check if virtctl CLI is available."""
|
|
71
|
+
try:
|
|
72
|
+
result = subprocess.run(["virtctl", "version", "--client"],
|
|
73
|
+
capture_output=True, timeout=5)
|
|
74
|
+
return result.returncode == 0
|
|
75
|
+
except Exception:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _run_virtctl(args: List[str], context: str = "") -> Dict[str, Any]:
|
|
80
|
+
"""Run virtctl command if available."""
|
|
81
|
+
if not _virtctl_available():
|
|
82
|
+
return {"success": False, "error": "virtctl CLI not available"}
|
|
83
|
+
|
|
84
|
+
cmd = ["virtctl"] + args
|
|
85
|
+
if context:
|
|
86
|
+
cmd.extend(["--context", context])
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
90
|
+
if result.returncode == 0:
|
|
91
|
+
return {"success": True, "output": result.stdout}
|
|
92
|
+
return {"success": False, "error": result.stderr}
|
|
93
|
+
except subprocess.TimeoutExpired:
|
|
94
|
+
return {"success": False, "error": "Command timed out"}
|
|
95
|
+
except Exception as e:
|
|
96
|
+
return {"success": False, "error": str(e)}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def kubevirt_vms_list(
|
|
100
|
+
namespace: str = "",
|
|
101
|
+
context: str = "",
|
|
102
|
+
label_selector: str = ""
|
|
103
|
+
) -> Dict[str, Any]:
|
|
104
|
+
"""List KubeVirt VirtualMachines.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
namespace: Filter by namespace (empty for all namespaces)
|
|
108
|
+
context: Kubernetes context to use (optional)
|
|
109
|
+
label_selector: Label selector to filter VMs
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
List of VirtualMachines with their status
|
|
113
|
+
"""
|
|
114
|
+
if not crd_exists(VM_CRD, context):
|
|
115
|
+
return {
|
|
116
|
+
"success": False,
|
|
117
|
+
"error": "KubeVirt is not installed (virtualmachines.kubevirt.io CRD not found)"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
vms = []
|
|
121
|
+
for item in _get_resources("virtualmachines.kubevirt.io", namespace, context, label_selector):
|
|
122
|
+
status = item.get("status", {})
|
|
123
|
+
spec = item.get("spec", {})
|
|
124
|
+
conditions = status.get("conditions", [])
|
|
125
|
+
|
|
126
|
+
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
127
|
+
paused_cond = next((c for c in conditions if c.get("type") == "Paused"), {})
|
|
128
|
+
|
|
129
|
+
# Get resource info from template
|
|
130
|
+
template = spec.get("template", {}).get("spec", {})
|
|
131
|
+
domain = template.get("domain", {})
|
|
132
|
+
resources = domain.get("resources", {})
|
|
133
|
+
cpu = domain.get("cpu", {})
|
|
134
|
+
|
|
135
|
+
vms.append({
|
|
136
|
+
"name": item["metadata"]["name"],
|
|
137
|
+
"namespace": item["metadata"]["namespace"],
|
|
138
|
+
"running": spec.get("running", False),
|
|
139
|
+
"run_strategy": spec.get("runStrategy"),
|
|
140
|
+
"ready": ready_cond.get("status") == "True",
|
|
141
|
+
"paused": paused_cond.get("status") == "True",
|
|
142
|
+
"print_status": status.get("printableStatus", "Unknown"),
|
|
143
|
+
"created": status.get("created", False),
|
|
144
|
+
"cpu_cores": cpu.get("cores", 1),
|
|
145
|
+
"cpu_sockets": cpu.get("sockets", 1),
|
|
146
|
+
"cpu_threads": cpu.get("threads", 1),
|
|
147
|
+
"memory": resources.get("requests", {}).get("memory", ""),
|
|
148
|
+
"volume_count": len(template.get("volumes", [])),
|
|
149
|
+
"network_count": len(template.get("networks", [])),
|
|
150
|
+
"state_change_requests": status.get("stateChangeRequests", []),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
# Summary
|
|
154
|
+
running = sum(1 for v in vms if v["running"])
|
|
155
|
+
ready = sum(1 for v in vms if v["ready"])
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"context": context or "current",
|
|
159
|
+
"total": len(vms),
|
|
160
|
+
"running": running,
|
|
161
|
+
"ready": ready,
|
|
162
|
+
"vms": vms,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def kubevirt_vm_get(
|
|
167
|
+
name: str,
|
|
168
|
+
namespace: str,
|
|
169
|
+
context: str = ""
|
|
170
|
+
) -> Dict[str, Any]:
|
|
171
|
+
"""Get detailed information about a VirtualMachine.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
name: Name of the VM
|
|
175
|
+
namespace: Namespace of the VM
|
|
176
|
+
context: Kubernetes context to use (optional)
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Detailed VM information
|
|
180
|
+
"""
|
|
181
|
+
if not crd_exists(VM_CRD, context):
|
|
182
|
+
return {"success": False, "error": "KubeVirt is not installed"}
|
|
183
|
+
|
|
184
|
+
args = ["get", "virtualmachines.kubevirt.io", name, "-n", namespace, "-o", "json"]
|
|
185
|
+
result = _run_kubectl(args, context)
|
|
186
|
+
|
|
187
|
+
if result["success"]:
|
|
188
|
+
try:
|
|
189
|
+
data = json.loads(result["output"])
|
|
190
|
+
return {
|
|
191
|
+
"success": True,
|
|
192
|
+
"context": context or "current",
|
|
193
|
+
"vm": data,
|
|
194
|
+
}
|
|
195
|
+
except json.JSONDecodeError:
|
|
196
|
+
return {"success": False, "error": "Failed to parse response"}
|
|
197
|
+
|
|
198
|
+
return {"success": False, "error": result.get("error", "Unknown error")}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def kubevirt_vmis_list(
|
|
202
|
+
namespace: str = "",
|
|
203
|
+
context: str = "",
|
|
204
|
+
label_selector: str = ""
|
|
205
|
+
) -> Dict[str, Any]:
|
|
206
|
+
"""List KubeVirt VirtualMachineInstances (running VMs).
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
namespace: Filter by namespace (empty for all namespaces)
|
|
210
|
+
context: Kubernetes context to use (optional)
|
|
211
|
+
label_selector: Label selector to filter VMIs
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
List of running VirtualMachineInstances
|
|
215
|
+
"""
|
|
216
|
+
if not crd_exists(VMI_CRD, context):
|
|
217
|
+
return {
|
|
218
|
+
"success": False,
|
|
219
|
+
"error": "KubeVirt is not installed"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
vmis = []
|
|
223
|
+
for item in _get_resources("virtualmachineinstances.kubevirt.io", namespace, context, label_selector):
|
|
224
|
+
status = item.get("status", {})
|
|
225
|
+
spec = item.get("spec", {})
|
|
226
|
+
conditions = status.get("conditions", [])
|
|
227
|
+
|
|
228
|
+
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
229
|
+
live_migratable = next((c for c in conditions if c.get("type") == "LiveMigratable"), {})
|
|
230
|
+
|
|
231
|
+
domain = spec.get("domain", {})
|
|
232
|
+
resources = domain.get("resources", {})
|
|
233
|
+
|
|
234
|
+
# Get guest info if available
|
|
235
|
+
guest_info = status.get("guestOSInfo", {})
|
|
236
|
+
|
|
237
|
+
vmis.append({
|
|
238
|
+
"name": item["metadata"]["name"],
|
|
239
|
+
"namespace": item["metadata"]["namespace"],
|
|
240
|
+
"phase": status.get("phase", "Unknown"),
|
|
241
|
+
"ready": ready_cond.get("status") == "True",
|
|
242
|
+
"live_migratable": live_migratable.get("status") == "True",
|
|
243
|
+
"node": status.get("nodeName", ""),
|
|
244
|
+
"ip_addresses": [iface.get("ipAddress") for iface in status.get("interfaces", []) if iface.get("ipAddress")],
|
|
245
|
+
"memory": resources.get("requests", {}).get("memory", ""),
|
|
246
|
+
"guest_os": guest_info.get("name", ""),
|
|
247
|
+
"guest_os_version": guest_info.get("version", ""),
|
|
248
|
+
"migration_state": status.get("migrationState"),
|
|
249
|
+
"active_pods": status.get("activePods", {}),
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
# Summary
|
|
253
|
+
running = sum(1 for v in vmis if v["phase"] == "Running")
|
|
254
|
+
scheduled = sum(1 for v in vmis if v["phase"] == "Scheduled")
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
"context": context or "current",
|
|
258
|
+
"total": len(vmis),
|
|
259
|
+
"running": running,
|
|
260
|
+
"scheduled": scheduled,
|
|
261
|
+
"vmis": vmis,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def kubevirt_vm_start(
|
|
266
|
+
name: str,
|
|
267
|
+
namespace: str,
|
|
268
|
+
context: str = ""
|
|
269
|
+
) -> Dict[str, Any]:
|
|
270
|
+
"""Start a VirtualMachine.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
name: Name of the VM
|
|
274
|
+
namespace: Namespace of the VM
|
|
275
|
+
context: Kubernetes context to use (optional)
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Start result
|
|
279
|
+
"""
|
|
280
|
+
if not crd_exists(VM_CRD, context):
|
|
281
|
+
return {"success": False, "error": "KubeVirt is not installed"}
|
|
282
|
+
|
|
283
|
+
# Try virtctl first
|
|
284
|
+
if _virtctl_available():
|
|
285
|
+
result = _run_virtctl(["start", name, "-n", namespace], context)
|
|
286
|
+
if result["success"]:
|
|
287
|
+
return {
|
|
288
|
+
"success": True,
|
|
289
|
+
"context": context or "current",
|
|
290
|
+
"message": f"Started VM {name}",
|
|
291
|
+
"output": result.get("output", ""),
|
|
292
|
+
}
|
|
293
|
+
# Don't return error, fall through to patch
|
|
294
|
+
|
|
295
|
+
# Fallback to patching
|
|
296
|
+
patch = {"spec": {"running": True}}
|
|
297
|
+
args = [
|
|
298
|
+
"patch", "virtualmachines.kubevirt.io", name,
|
|
299
|
+
"-n", namespace,
|
|
300
|
+
"--type=merge",
|
|
301
|
+
"-p", json.dumps(patch)
|
|
302
|
+
]
|
|
303
|
+
result = _run_kubectl(args, context)
|
|
304
|
+
|
|
305
|
+
if result["success"]:
|
|
306
|
+
return {
|
|
307
|
+
"success": True,
|
|
308
|
+
"context": context or "current",
|
|
309
|
+
"message": f"Started VM {name}",
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {"success": False, "error": result.get("error", "Failed to start VM")}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def kubevirt_vm_stop(
|
|
316
|
+
name: str,
|
|
317
|
+
namespace: str,
|
|
318
|
+
force: bool = False,
|
|
319
|
+
context: str = ""
|
|
320
|
+
) -> Dict[str, Any]:
|
|
321
|
+
"""Stop a VirtualMachine.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
name: Name of the VM
|
|
325
|
+
namespace: Namespace of the VM
|
|
326
|
+
force: Force stop (like pulling the power)
|
|
327
|
+
context: Kubernetes context to use (optional)
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
Stop result
|
|
331
|
+
"""
|
|
332
|
+
if not crd_exists(VM_CRD, context):
|
|
333
|
+
return {"success": False, "error": "KubeVirt is not installed"}
|
|
334
|
+
|
|
335
|
+
# Try virtctl first
|
|
336
|
+
if _virtctl_available():
|
|
337
|
+
cmd = ["stop", name, "-n", namespace]
|
|
338
|
+
if force:
|
|
339
|
+
cmd.append("--force")
|
|
340
|
+
result = _run_virtctl(cmd, context)
|
|
341
|
+
if result["success"]:
|
|
342
|
+
return {
|
|
343
|
+
"success": True,
|
|
344
|
+
"context": context or "current",
|
|
345
|
+
"message": f"Stopped VM {name}" + (" (forced)" if force else ""),
|
|
346
|
+
"output": result.get("output", ""),
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# Fallback to patching
|
|
350
|
+
patch = {"spec": {"running": False}}
|
|
351
|
+
args = [
|
|
352
|
+
"patch", "virtualmachines.kubevirt.io", name,
|
|
353
|
+
"-n", namespace,
|
|
354
|
+
"--type=merge",
|
|
355
|
+
"-p", json.dumps(patch)
|
|
356
|
+
]
|
|
357
|
+
result = _run_kubectl(args, context)
|
|
358
|
+
|
|
359
|
+
if result["success"]:
|
|
360
|
+
return {
|
|
361
|
+
"success": True,
|
|
362
|
+
"context": context or "current",
|
|
363
|
+
"message": f"Stopped VM {name}",
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {"success": False, "error": result.get("error", "Failed to stop VM")}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def kubevirt_vm_restart(
|
|
370
|
+
name: str,
|
|
371
|
+
namespace: str,
|
|
372
|
+
context: str = ""
|
|
373
|
+
) -> Dict[str, Any]:
|
|
374
|
+
"""Restart a VirtualMachine.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
name: Name of the VM
|
|
378
|
+
namespace: Namespace of the VM
|
|
379
|
+
context: Kubernetes context to use (optional)
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Restart result
|
|
383
|
+
"""
|
|
384
|
+
if not crd_exists(VM_CRD, context):
|
|
385
|
+
return {"success": False, "error": "KubeVirt is not installed"}
|
|
386
|
+
|
|
387
|
+
if _virtctl_available():
|
|
388
|
+
result = _run_virtctl(["restart", name, "-n", namespace], context)
|
|
389
|
+
if result["success"]:
|
|
390
|
+
return {
|
|
391
|
+
"success": True,
|
|
392
|
+
"context": context or "current",
|
|
393
|
+
"message": f"Restarted VM {name}",
|
|
394
|
+
"output": result.get("output", ""),
|
|
395
|
+
}
|
|
396
|
+
return {"success": False, "error": result.get("error", "Failed to restart")}
|
|
397
|
+
|
|
398
|
+
return {"success": False, "error": "virtctl CLI required for restart operation"}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def kubevirt_vm_pause(
|
|
402
|
+
name: str,
|
|
403
|
+
namespace: str,
|
|
404
|
+
context: str = ""
|
|
405
|
+
) -> Dict[str, Any]:
|
|
406
|
+
"""Pause a VirtualMachine.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
name: Name of the VM
|
|
410
|
+
namespace: Namespace of the VM
|
|
411
|
+
context: Kubernetes context to use (optional)
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
Pause result
|
|
415
|
+
"""
|
|
416
|
+
if not crd_exists(VM_CRD, context):
|
|
417
|
+
return {"success": False, "error": "KubeVirt is not installed"}
|
|
418
|
+
|
|
419
|
+
if _virtctl_available():
|
|
420
|
+
result = _run_virtctl(["pause", "vm", name, "-n", namespace], context)
|
|
421
|
+
if result["success"]:
|
|
422
|
+
return {
|
|
423
|
+
"success": True,
|
|
424
|
+
"context": context or "current",
|
|
425
|
+
"message": f"Paused VM {name}",
|
|
426
|
+
"output": result.get("output", ""),
|
|
427
|
+
}
|
|
428
|
+
return {"success": False, "error": result.get("error", "Failed to pause")}
|
|
429
|
+
|
|
430
|
+
return {"success": False, "error": "virtctl CLI required for pause operation"}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def kubevirt_vm_unpause(
|
|
434
|
+
name: str,
|
|
435
|
+
namespace: str,
|
|
436
|
+
context: str = ""
|
|
437
|
+
) -> Dict[str, Any]:
|
|
438
|
+
"""Unpause a VirtualMachine.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
name: Name of the VM
|
|
442
|
+
namespace: Namespace of the VM
|
|
443
|
+
context: Kubernetes context to use (optional)
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Unpause result
|
|
447
|
+
"""
|
|
448
|
+
if not crd_exists(VM_CRD, context):
|
|
449
|
+
return {"success": False, "error": "KubeVirt is not installed"}
|
|
450
|
+
|
|
451
|
+
if _virtctl_available():
|
|
452
|
+
result = _run_virtctl(["unpause", "vm", name, "-n", namespace], context)
|
|
453
|
+
if result["success"]:
|
|
454
|
+
return {
|
|
455
|
+
"success": True,
|
|
456
|
+
"context": context or "current",
|
|
457
|
+
"message": f"Unpaused VM {name}",
|
|
458
|
+
"output": result.get("output", ""),
|
|
459
|
+
}
|
|
460
|
+
return {"success": False, "error": result.get("error", "Failed to unpause")}
|
|
461
|
+
|
|
462
|
+
return {"success": False, "error": "virtctl CLI required for unpause operation"}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def kubevirt_vm_migrate(
|
|
466
|
+
name: str,
|
|
467
|
+
namespace: str,
|
|
468
|
+
context: str = ""
|
|
469
|
+
) -> Dict[str, Any]:
|
|
470
|
+
"""Trigger live migration of a VirtualMachineInstance.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
name: Name of the VM
|
|
474
|
+
namespace: Namespace of the VM
|
|
475
|
+
context: Kubernetes context to use (optional)
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
Migration result
|
|
479
|
+
"""
|
|
480
|
+
if not crd_exists(VM_CRD, context):
|
|
481
|
+
return {"success": False, "error": "KubeVirt is not installed"}
|
|
482
|
+
|
|
483
|
+
if _virtctl_available():
|
|
484
|
+
result = _run_virtctl(["migrate", name, "-n", namespace], context)
|
|
485
|
+
if result["success"]:
|
|
486
|
+
return {
|
|
487
|
+
"success": True,
|
|
488
|
+
"context": context or "current",
|
|
489
|
+
"message": f"Triggered migration for VM {name}",
|
|
490
|
+
"output": result.get("output", ""),
|
|
491
|
+
}
|
|
492
|
+
return {"success": False, "error": result.get("error", "Failed to migrate")}
|
|
493
|
+
|
|
494
|
+
return {"success": False, "error": "virtctl CLI required for migration operation"}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def kubevirt_datasources_list(
|
|
498
|
+
namespace: str = "",
|
|
499
|
+
context: str = "",
|
|
500
|
+
label_selector: str = ""
|
|
501
|
+
) -> Dict[str, Any]:
|
|
502
|
+
"""List KubeVirt DataSources (for disk images).
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
namespace: Filter by namespace (empty for all namespaces)
|
|
506
|
+
context: Kubernetes context to use (optional)
|
|
507
|
+
label_selector: Label selector to filter
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
List of DataSources
|
|
511
|
+
"""
|
|
512
|
+
if not crd_exists(DATASOURCE_CRD, context):
|
|
513
|
+
return {
|
|
514
|
+
"success": False,
|
|
515
|
+
"error": "CDI DataSources CRD not found"
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
datasources = []
|
|
519
|
+
for item in _get_resources("datasources.cdi.kubevirt.io", namespace, context, label_selector):
|
|
520
|
+
spec = item.get("spec", {})
|
|
521
|
+
status = item.get("status", {})
|
|
522
|
+
conditions = status.get("conditions", [])
|
|
523
|
+
|
|
524
|
+
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
525
|
+
source = spec.get("source", {})
|
|
526
|
+
|
|
527
|
+
datasources.append({
|
|
528
|
+
"name": item["metadata"]["name"],
|
|
529
|
+
"namespace": item["metadata"]["namespace"],
|
|
530
|
+
"ready": ready_cond.get("status") == "True",
|
|
531
|
+
"source_pvc": source.get("pvc", {}),
|
|
532
|
+
"source_snapshot": source.get("snapshot", {}),
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
"context": context or "current",
|
|
537
|
+
"total": len(datasources),
|
|
538
|
+
"datasources": datasources,
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def kubevirt_instancetypes_list(
|
|
543
|
+
namespace: str = "",
|
|
544
|
+
context: str = "",
|
|
545
|
+
include_cluster: bool = True
|
|
546
|
+
) -> Dict[str, Any]:
|
|
547
|
+
"""List KubeVirt InstanceTypes (VM sizing templates).
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
namespace: Filter by namespace (empty for all)
|
|
551
|
+
context: Kubernetes context to use (optional)
|
|
552
|
+
include_cluster: Include cluster-wide instance types
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
List of InstanceTypes
|
|
556
|
+
"""
|
|
557
|
+
instancetypes = []
|
|
558
|
+
|
|
559
|
+
if crd_exists(INSTANCETYPE_CRD, context):
|
|
560
|
+
for item in _get_resources("virtualmachineinstancetypes.instancetype.kubevirt.io", namespace, context):
|
|
561
|
+
spec = item.get("spec", {})
|
|
562
|
+
cpu = spec.get("cpu", {})
|
|
563
|
+
memory = spec.get("memory", {})
|
|
564
|
+
|
|
565
|
+
instancetypes.append({
|
|
566
|
+
"name": item["metadata"]["name"],
|
|
567
|
+
"namespace": item["metadata"]["namespace"],
|
|
568
|
+
"kind": "VirtualMachineInstancetype",
|
|
569
|
+
"cpu_guest": cpu.get("guest", 1),
|
|
570
|
+
"cpu_model": cpu.get("model"),
|
|
571
|
+
"memory_guest": memory.get("guest", ""),
|
|
572
|
+
"memory_hugepages": memory.get("hugepages", {}),
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
if include_cluster and crd_exists(CLUSTERINSTANCETYPE_CRD, context):
|
|
576
|
+
for item in _get_resources("virtualmachineclusterinstancetypes.instancetype.kubevirt.io", "", context):
|
|
577
|
+
spec = item.get("spec", {})
|
|
578
|
+
cpu = spec.get("cpu", {})
|
|
579
|
+
memory = spec.get("memory", {})
|
|
580
|
+
|
|
581
|
+
instancetypes.append({
|
|
582
|
+
"name": item["metadata"]["name"],
|
|
583
|
+
"namespace": "",
|
|
584
|
+
"kind": "VirtualMachineClusterInstancetype",
|
|
585
|
+
"cpu_guest": cpu.get("guest", 1),
|
|
586
|
+
"cpu_model": cpu.get("model"),
|
|
587
|
+
"memory_guest": memory.get("guest", ""),
|
|
588
|
+
"memory_hugepages": memory.get("hugepages", {}),
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
"context": context or "current",
|
|
593
|
+
"total": len(instancetypes),
|
|
594
|
+
"instancetypes": instancetypes,
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def kubevirt_datavolumes_list(
|
|
599
|
+
namespace: str = "",
|
|
600
|
+
context: str = "",
|
|
601
|
+
label_selector: str = ""
|
|
602
|
+
) -> Dict[str, Any]:
|
|
603
|
+
"""List KubeVirt DataVolumes (disk images).
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
namespace: Filter by namespace (empty for all namespaces)
|
|
607
|
+
context: Kubernetes context to use (optional)
|
|
608
|
+
label_selector: Label selector to filter
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
List of DataVolumes
|
|
612
|
+
"""
|
|
613
|
+
if not crd_exists(DATAVOLUME_CRD, context):
|
|
614
|
+
return {
|
|
615
|
+
"success": False,
|
|
616
|
+
"error": "CDI DataVolumes CRD not found"
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
datavolumes = []
|
|
620
|
+
for item in _get_resources("datavolumes.cdi.kubevirt.io", namespace, context, label_selector):
|
|
621
|
+
spec = item.get("spec", {})
|
|
622
|
+
status = item.get("status", {})
|
|
623
|
+
conditions = status.get("conditions", [])
|
|
624
|
+
|
|
625
|
+
ready_cond = next((c for c in conditions if c.get("type") == "Ready"), {})
|
|
626
|
+
bound_cond = next((c for c in conditions if c.get("type") == "Bound"), {})
|
|
627
|
+
|
|
628
|
+
source = spec.get("source", {})
|
|
629
|
+
source_type = list(source.keys())[0] if source else "unknown"
|
|
630
|
+
|
|
631
|
+
datavolumes.append({
|
|
632
|
+
"name": item["metadata"]["name"],
|
|
633
|
+
"namespace": item["metadata"]["namespace"],
|
|
634
|
+
"phase": status.get("phase", "Unknown"),
|
|
635
|
+
"ready": ready_cond.get("status") == "True",
|
|
636
|
+
"bound": bound_cond.get("status") == "True",
|
|
637
|
+
"progress": status.get("progress", "N/A"),
|
|
638
|
+
"source_type": source_type,
|
|
639
|
+
"storage_size": spec.get("pvc", {}).get("resources", {}).get("requests", {}).get("storage", ""),
|
|
640
|
+
"storage_class": spec.get("pvc", {}).get("storageClassName", ""),
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
"context": context or "current",
|
|
645
|
+
"total": len(datavolumes),
|
|
646
|
+
"datavolumes": datavolumes,
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def kubevirt_detect(context: str = "") -> Dict[str, Any]:
|
|
651
|
+
"""Detect if KubeVirt is installed and its components.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
context: Kubernetes context to use (optional)
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
Detection results for KubeVirt
|
|
658
|
+
"""
|
|
659
|
+
return {
|
|
660
|
+
"context": context or "current",
|
|
661
|
+
"installed": crd_exists(VM_CRD, context),
|
|
662
|
+
"cli_available": _virtctl_available(),
|
|
663
|
+
"crds": {
|
|
664
|
+
"virtualmachines": crd_exists(VM_CRD, context),
|
|
665
|
+
"virtualmachineinstances": crd_exists(VMI_CRD, context),
|
|
666
|
+
"virtualmachineinstancepresets": crd_exists(VMIPRESET_CRD, context),
|
|
667
|
+
"virtualmachineinstancereplicasets": crd_exists(VMIRS_CRD, context),
|
|
668
|
+
"datasources": crd_exists(DATASOURCE_CRD, context),
|
|
669
|
+
"datavolumes": crd_exists(DATAVOLUME_CRD, context),
|
|
670
|
+
"instancetypes": crd_exists(INSTANCETYPE_CRD, context),
|
|
671
|
+
"clusterinstancetypes": crd_exists(CLUSTERINSTANCETYPE_CRD, context),
|
|
672
|
+
},
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def register_kubevirt_tools(mcp: FastMCP, non_destructive: bool = False):
|
|
677
|
+
"""Register KubeVirt tools with the MCP server."""
|
|
678
|
+
|
|
679
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
680
|
+
def kubevirt_vms_list_tool(
|
|
681
|
+
namespace: str = "",
|
|
682
|
+
context: str = "",
|
|
683
|
+
label_selector: str = ""
|
|
684
|
+
) -> str:
|
|
685
|
+
"""List KubeVirt VirtualMachines."""
|
|
686
|
+
return json.dumps(kubevirt_vms_list(namespace, context, label_selector), indent=2)
|
|
687
|
+
|
|
688
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
689
|
+
def kubevirt_vm_get_tool(
|
|
690
|
+
name: str,
|
|
691
|
+
namespace: str,
|
|
692
|
+
context: str = ""
|
|
693
|
+
) -> str:
|
|
694
|
+
"""Get detailed information about a VirtualMachine."""
|
|
695
|
+
return json.dumps(kubevirt_vm_get(name, namespace, context), indent=2)
|
|
696
|
+
|
|
697
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
698
|
+
def kubevirt_vmis_list_tool(
|
|
699
|
+
namespace: str = "",
|
|
700
|
+
context: str = "",
|
|
701
|
+
label_selector: str = ""
|
|
702
|
+
) -> str:
|
|
703
|
+
"""List running VirtualMachineInstances."""
|
|
704
|
+
return json.dumps(kubevirt_vmis_list(namespace, context, label_selector), indent=2)
|
|
705
|
+
|
|
706
|
+
@mcp.tool()
|
|
707
|
+
def kubevirt_vm_start_tool(
|
|
708
|
+
name: str,
|
|
709
|
+
namespace: str,
|
|
710
|
+
context: str = ""
|
|
711
|
+
) -> str:
|
|
712
|
+
"""Start a VirtualMachine."""
|
|
713
|
+
if non_destructive:
|
|
714
|
+
return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
|
|
715
|
+
return json.dumps(kubevirt_vm_start(name, namespace, context), indent=2)
|
|
716
|
+
|
|
717
|
+
@mcp.tool()
|
|
718
|
+
def kubevirt_vm_stop_tool(
|
|
719
|
+
name: str,
|
|
720
|
+
namespace: str,
|
|
721
|
+
force: bool = False,
|
|
722
|
+
context: str = ""
|
|
723
|
+
) -> str:
|
|
724
|
+
"""Stop a VirtualMachine."""
|
|
725
|
+
if non_destructive:
|
|
726
|
+
return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
|
|
727
|
+
return json.dumps(kubevirt_vm_stop(name, namespace, force, context), indent=2)
|
|
728
|
+
|
|
729
|
+
@mcp.tool()
|
|
730
|
+
def kubevirt_vm_restart_tool(
|
|
731
|
+
name: str,
|
|
732
|
+
namespace: str,
|
|
733
|
+
context: str = ""
|
|
734
|
+
) -> str:
|
|
735
|
+
"""Restart a VirtualMachine."""
|
|
736
|
+
if non_destructive:
|
|
737
|
+
return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
|
|
738
|
+
return json.dumps(kubevirt_vm_restart(name, namespace, context), indent=2)
|
|
739
|
+
|
|
740
|
+
@mcp.tool()
|
|
741
|
+
def kubevirt_vm_pause_tool(
|
|
742
|
+
name: str,
|
|
743
|
+
namespace: str,
|
|
744
|
+
context: str = ""
|
|
745
|
+
) -> str:
|
|
746
|
+
"""Pause a VirtualMachine."""
|
|
747
|
+
if non_destructive:
|
|
748
|
+
return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
|
|
749
|
+
return json.dumps(kubevirt_vm_pause(name, namespace, context), indent=2)
|
|
750
|
+
|
|
751
|
+
@mcp.tool()
|
|
752
|
+
def kubevirt_vm_unpause_tool(
|
|
753
|
+
name: str,
|
|
754
|
+
namespace: str,
|
|
755
|
+
context: str = ""
|
|
756
|
+
) -> str:
|
|
757
|
+
"""Unpause a VirtualMachine."""
|
|
758
|
+
if non_destructive:
|
|
759
|
+
return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
|
|
760
|
+
return json.dumps(kubevirt_vm_unpause(name, namespace, context), indent=2)
|
|
761
|
+
|
|
762
|
+
@mcp.tool()
|
|
763
|
+
def kubevirt_vm_migrate_tool(
|
|
764
|
+
name: str,
|
|
765
|
+
namespace: str,
|
|
766
|
+
context: str = ""
|
|
767
|
+
) -> str:
|
|
768
|
+
"""Trigger live migration of a VirtualMachine."""
|
|
769
|
+
if non_destructive:
|
|
770
|
+
return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
|
|
771
|
+
return json.dumps(kubevirt_vm_migrate(name, namespace, context), indent=2)
|
|
772
|
+
|
|
773
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
774
|
+
def kubevirt_datasources_list_tool(
|
|
775
|
+
namespace: str = "",
|
|
776
|
+
context: str = "",
|
|
777
|
+
label_selector: str = ""
|
|
778
|
+
) -> str:
|
|
779
|
+
"""List KubeVirt DataSources."""
|
|
780
|
+
return json.dumps(kubevirt_datasources_list(namespace, context, label_selector), indent=2)
|
|
781
|
+
|
|
782
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
783
|
+
def kubevirt_instancetypes_list_tool(
|
|
784
|
+
namespace: str = "",
|
|
785
|
+
context: str = "",
|
|
786
|
+
include_cluster: bool = True
|
|
787
|
+
) -> str:
|
|
788
|
+
"""List KubeVirt InstanceTypes (VM sizing templates)."""
|
|
789
|
+
return json.dumps(kubevirt_instancetypes_list(namespace, context, include_cluster), indent=2)
|
|
790
|
+
|
|
791
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
792
|
+
def kubevirt_datavolumes_list_tool(
|
|
793
|
+
namespace: str = "",
|
|
794
|
+
context: str = "",
|
|
795
|
+
label_selector: str = ""
|
|
796
|
+
) -> str:
|
|
797
|
+
"""List KubeVirt DataVolumes (disk images)."""
|
|
798
|
+
return json.dumps(kubevirt_datavolumes_list(namespace, context, label_selector), indent=2)
|
|
799
|
+
|
|
800
|
+
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
801
|
+
def kubevirt_detect_tool(context: str = "") -> str:
|
|
802
|
+
"""Detect if KubeVirt is installed and its components."""
|
|
803
|
+
return json.dumps(kubevirt_detect(context), indent=2)
|