kubectl-mcp-server 1.14.0__py3-none-any.whl → 1.16.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.
Files changed (37) hide show
  1. kubectl_mcp_server-1.16.0.dist-info/METADATA +1047 -0
  2. kubectl_mcp_server-1.16.0.dist-info/RECORD +61 -0
  3. kubectl_mcp_tool/__init__.py +1 -1
  4. kubectl_mcp_tool/crd_detector.py +247 -0
  5. kubectl_mcp_tool/k8s_config.py +304 -63
  6. kubectl_mcp_tool/mcp_server.py +27 -0
  7. kubectl_mcp_tool/tools/__init__.py +20 -0
  8. kubectl_mcp_tool/tools/backup.py +881 -0
  9. kubectl_mcp_tool/tools/capi.py +727 -0
  10. kubectl_mcp_tool/tools/certs.py +709 -0
  11. kubectl_mcp_tool/tools/cilium.py +582 -0
  12. kubectl_mcp_tool/tools/cluster.py +395 -121
  13. kubectl_mcp_tool/tools/core.py +157 -60
  14. kubectl_mcp_tool/tools/cost.py +97 -41
  15. kubectl_mcp_tool/tools/deployments.py +173 -56
  16. kubectl_mcp_tool/tools/diagnostics.py +40 -13
  17. kubectl_mcp_tool/tools/gitops.py +552 -0
  18. kubectl_mcp_tool/tools/helm.py +133 -46
  19. kubectl_mcp_tool/tools/keda.py +464 -0
  20. kubectl_mcp_tool/tools/kiali.py +652 -0
  21. kubectl_mcp_tool/tools/kubevirt.py +803 -0
  22. kubectl_mcp_tool/tools/networking.py +106 -32
  23. kubectl_mcp_tool/tools/operations.py +176 -50
  24. kubectl_mcp_tool/tools/pods.py +162 -50
  25. kubectl_mcp_tool/tools/policy.py +554 -0
  26. kubectl_mcp_tool/tools/rollouts.py +790 -0
  27. kubectl_mcp_tool/tools/security.py +89 -36
  28. kubectl_mcp_tool/tools/storage.py +35 -16
  29. tests/test_browser.py +2 -2
  30. tests/test_ecosystem.py +331 -0
  31. tests/test_tools.py +73 -10
  32. kubectl_mcp_server-1.14.0.dist-info/METADATA +0 -780
  33. kubectl_mcp_server-1.14.0.dist-info/RECORD +0 -49
  34. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/WHEEL +0 -0
  35. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/entry_points.txt +0 -0
  36. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/licenses/LICENSE +0 -0
  37. {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,881 @@
1
+ """Backup toolset for kubectl-mcp-server.
2
+
3
+ Provides tools for managing Velero backups and restores.
4
+ """
5
+
6
+ import subprocess
7
+ import json
8
+ from typing import Dict, Any, List, Optional
9
+ from datetime import datetime
10
+
11
+ try:
12
+ from fastmcp import FastMCP
13
+ from fastmcp.tools import ToolAnnotations
14
+ except ImportError:
15
+ from mcp.server.fastmcp import FastMCP
16
+ from mcp.types import ToolAnnotations
17
+
18
+ from ..k8s_config import _get_kubectl_context_args
19
+ from ..crd_detector import crd_exists
20
+
21
+
22
+ VELERO_BACKUP_CRD = "backups.velero.io"
23
+ VELERO_RESTORE_CRD = "restores.velero.io"
24
+ VELERO_SCHEDULE_CRD = "schedules.velero.io"
25
+ VELERO_BSL_CRD = "backupstoragelocations.velero.io"
26
+ VELERO_VSL_CRD = "volumesnapshotlocations.velero.io"
27
+
28
+
29
+ def _run_kubectl(args: List[str], context: str = "") -> Dict[str, Any]:
30
+ """Run kubectl command and return result."""
31
+ cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
32
+ try:
33
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
34
+ if result.returncode == 0:
35
+ return {"success": True, "output": result.stdout}
36
+ return {"success": False, "error": result.stderr}
37
+ except subprocess.TimeoutExpired:
38
+ return {"success": False, "error": "Command timed out"}
39
+ except Exception as e:
40
+ return {"success": False, "error": str(e)}
41
+
42
+
43
+ def _get_resources(kind: str, namespace: str = "", context: str = "", label_selector: str = "") -> List[Dict]:
44
+ """Get Kubernetes resources of a specific kind."""
45
+ args = ["get", kind, "-o", "json"]
46
+ if namespace:
47
+ args.extend(["-n", namespace])
48
+ else:
49
+ args.append("-A")
50
+ if label_selector:
51
+ args.extend(["-l", label_selector])
52
+
53
+ result = _run_kubectl(args, context)
54
+ if result["success"]:
55
+ try:
56
+ data = json.loads(result["output"])
57
+ return data.get("items", [])
58
+ except json.JSONDecodeError:
59
+ return []
60
+ return []
61
+
62
+
63
+ def _velero_cli_available() -> bool:
64
+ """Check if velero CLI is available."""
65
+ try:
66
+ result = subprocess.run(["velero", "version", "--client-only"],
67
+ capture_output=True, timeout=5)
68
+ return result.returncode == 0
69
+ except Exception:
70
+ return False
71
+
72
+
73
+ def _run_velero(args: List[str], context: str = "") -> Dict[str, Any]:
74
+ """Run velero CLI command if available."""
75
+ if not _velero_cli_available():
76
+ return {"success": False, "error": "Velero CLI not available"}
77
+
78
+ cmd = ["velero"] + args
79
+ if context:
80
+ cmd.extend(["--kubecontext", context])
81
+
82
+ try:
83
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
84
+ if result.returncode == 0:
85
+ return {"success": True, "output": result.stdout}
86
+ return {"success": False, "error": result.stderr}
87
+ except subprocess.TimeoutExpired:
88
+ return {"success": False, "error": "Command timed out"}
89
+ except Exception as e:
90
+ return {"success": False, "error": str(e)}
91
+
92
+
93
+ def backup_list(
94
+ namespace: str = "velero",
95
+ context: str = "",
96
+ label_selector: str = ""
97
+ ) -> Dict[str, Any]:
98
+ """List Velero backups.
99
+
100
+ Args:
101
+ namespace: Velero namespace (default: velero)
102
+ context: Kubernetes context to use (optional)
103
+ label_selector: Label selector to filter backups
104
+
105
+ Returns:
106
+ List of backups with their status
107
+ """
108
+ if not crd_exists(VELERO_BACKUP_CRD, context):
109
+ return {
110
+ "success": False,
111
+ "error": "Velero is not installed (backups.velero.io CRD not found)"
112
+ }
113
+
114
+ backups = []
115
+ for item in _get_resources("backups.velero.io", namespace, context, label_selector):
116
+ status = item.get("status", {})
117
+ spec = item.get("spec", {})
118
+ progress = status.get("progress", {})
119
+
120
+ backups.append({
121
+ "name": item["metadata"]["name"],
122
+ "namespace": item["metadata"]["namespace"],
123
+ "phase": status.get("phase", "Unknown"),
124
+ "started": status.get("startTimestamp", ""),
125
+ "completed": status.get("completionTimestamp", ""),
126
+ "expiration": status.get("expiration", ""),
127
+ "errors": status.get("errors", 0),
128
+ "warnings": status.get("warnings", 0),
129
+ "items_backed_up": progress.get("itemsBackedUp", 0),
130
+ "total_items": progress.get("totalItems", 0),
131
+ "included_namespaces": spec.get("includedNamespaces", []),
132
+ "excluded_namespaces": spec.get("excludedNamespaces", []),
133
+ "storage_location": spec.get("storageLocation", ""),
134
+ "ttl": spec.get("ttl", ""),
135
+ })
136
+
137
+ completed = sum(1 for b in backups if b["phase"] == "Completed")
138
+ failed = sum(1 for b in backups if b["phase"] == "Failed")
139
+
140
+ return {
141
+ "context": context or "current",
142
+ "total": len(backups),
143
+ "completed": completed,
144
+ "failed": failed,
145
+ "backups": backups,
146
+ }
147
+
148
+
149
+ def backup_get(
150
+ name: str,
151
+ namespace: str = "velero",
152
+ context: str = ""
153
+ ) -> Dict[str, Any]:
154
+ """Get detailed information about a backup.
155
+
156
+ Args:
157
+ name: Name of the backup
158
+ namespace: Velero namespace (default: velero)
159
+ context: Kubernetes context to use (optional)
160
+
161
+ Returns:
162
+ Detailed backup information
163
+ """
164
+ if not crd_exists(VELERO_BACKUP_CRD, context):
165
+ return {"success": False, "error": "Velero is not installed"}
166
+
167
+ args = ["get", "backups.velero.io", name, "-n", namespace, "-o", "json"]
168
+ result = _run_kubectl(args, context)
169
+
170
+ if result["success"]:
171
+ try:
172
+ data = json.loads(result["output"])
173
+ return {
174
+ "success": True,
175
+ "context": context or "current",
176
+ "backup": data,
177
+ }
178
+ except json.JSONDecodeError:
179
+ return {"success": False, "error": "Failed to parse response"}
180
+
181
+ return {"success": False, "error": result.get("error", "Unknown error")}
182
+
183
+
184
+ def backup_create(
185
+ name: str = "",
186
+ namespace: str = "velero",
187
+ included_namespaces: List[str] = None,
188
+ excluded_namespaces: List[str] = None,
189
+ included_resources: List[str] = None,
190
+ excluded_resources: List[str] = None,
191
+ label_selector: str = "",
192
+ storage_location: str = "",
193
+ ttl: str = "720h",
194
+ snapshot_volumes: bool = True,
195
+ context: str = ""
196
+ ) -> Dict[str, Any]:
197
+ """Create a new Velero backup.
198
+
199
+ Args:
200
+ name: Backup name (auto-generated if empty)
201
+ namespace: Velero namespace (default: velero)
202
+ included_namespaces: Namespaces to include
203
+ excluded_namespaces: Namespaces to exclude
204
+ included_resources: Resources to include
205
+ excluded_resources: Resources to exclude
206
+ label_selector: Label selector for resources
207
+ storage_location: Backup storage location
208
+ ttl: Time to live (default: 720h / 30 days)
209
+ snapshot_volumes: Whether to snapshot volumes
210
+ context: Kubernetes context to use (optional)
211
+
212
+ Returns:
213
+ Backup creation result
214
+ """
215
+ if not crd_exists(VELERO_BACKUP_CRD, context):
216
+ return {"success": False, "error": "Velero is not installed"}
217
+
218
+ if not name:
219
+ name = f"backup-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
220
+
221
+ if _velero_cli_available():
222
+ args = ["backup", "create", name, "-n", namespace]
223
+
224
+ if included_namespaces:
225
+ args.extend(["--include-namespaces", ",".join(included_namespaces)])
226
+ if excluded_namespaces:
227
+ args.extend(["--exclude-namespaces", ",".join(excluded_namespaces)])
228
+ if included_resources:
229
+ args.extend(["--include-resources", ",".join(included_resources)])
230
+ if excluded_resources:
231
+ args.extend(["--exclude-resources", ",".join(excluded_resources)])
232
+ if label_selector:
233
+ args.extend(["--selector", label_selector])
234
+ if storage_location:
235
+ args.extend(["--storage-location", storage_location])
236
+ if ttl:
237
+ args.extend(["--ttl", ttl])
238
+ if not snapshot_volumes:
239
+ args.append("--snapshot-volumes=false")
240
+
241
+ result = _run_velero(args, context)
242
+ if result["success"]:
243
+ return {
244
+ "success": True,
245
+ "context": context or "current",
246
+ "message": f"Backup '{name}' created",
247
+ "backup_name": name,
248
+ "output": result["output"],
249
+ }
250
+ return result
251
+
252
+ backup_spec = {
253
+ "apiVersion": "velero.io/v1",
254
+ "kind": "Backup",
255
+ "metadata": {
256
+ "name": name,
257
+ "namespace": namespace,
258
+ },
259
+ "spec": {
260
+ "ttl": ttl,
261
+ "snapshotVolumes": snapshot_volumes,
262
+ }
263
+ }
264
+
265
+ if included_namespaces:
266
+ backup_spec["spec"]["includedNamespaces"] = included_namespaces
267
+ if excluded_namespaces:
268
+ backup_spec["spec"]["excludedNamespaces"] = excluded_namespaces
269
+ if included_resources:
270
+ backup_spec["spec"]["includedResources"] = included_resources
271
+ if excluded_resources:
272
+ backup_spec["spec"]["excludedResources"] = excluded_resources
273
+ if label_selector:
274
+ # Validate and sanitize label_selector
275
+ parsed_labels = {}
276
+ for segment in label_selector.split(","):
277
+ segment = segment.strip()
278
+ if not segment:
279
+ continue
280
+ if segment.count("=") != 1:
281
+ return {"success": False, "error": f"Invalid label selector segment: '{segment}'. Expected format: key=value"}
282
+ key, value = segment.split("=")
283
+ key, value = key.strip(), value.strip()
284
+ if not key:
285
+ return {"success": False, "error": f"Invalid label selector: empty key in '{segment}'"}
286
+ parsed_labels[key] = value
287
+ if parsed_labels:
288
+ backup_spec["spec"]["labelSelector"] = {"matchLabels": parsed_labels}
289
+ if storage_location:
290
+ backup_spec["spec"]["storageLocation"] = storage_location
291
+
292
+ args = ["apply", "-f", "-"]
293
+ cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
294
+
295
+ try:
296
+ result = subprocess.run(
297
+ cmd,
298
+ input=json.dumps(backup_spec),
299
+ capture_output=True,
300
+ text=True,
301
+ timeout=60
302
+ )
303
+ if result.returncode == 0:
304
+ return {
305
+ "success": True,
306
+ "context": context or "current",
307
+ "message": f"Backup '{name}' created",
308
+ "backup_name": name,
309
+ }
310
+ return {"success": False, "error": result.stderr}
311
+ except Exception as e:
312
+ return {"success": False, "error": str(e)}
313
+
314
+
315
+ def backup_delete(
316
+ name: str,
317
+ namespace: str = "velero",
318
+ context: str = ""
319
+ ) -> Dict[str, Any]:
320
+ """Delete a Velero backup.
321
+
322
+ Args:
323
+ name: Name of the backup to delete
324
+ namespace: Velero namespace (default: velero)
325
+ context: Kubernetes context to use (optional)
326
+
327
+ Returns:
328
+ Deletion result
329
+ """
330
+ if not crd_exists(VELERO_BACKUP_CRD, context):
331
+ return {"success": False, "error": "Velero is not installed"}
332
+
333
+ if _velero_cli_available():
334
+ result = _run_velero(["backup", "delete", name, "-n", namespace, "--confirm"], context)
335
+ if result["success"]:
336
+ return {
337
+ "success": True,
338
+ "context": context or "current",
339
+ "message": f"Backup '{name}' deletion requested",
340
+ }
341
+ return result
342
+
343
+ args = ["delete", "backups.velero.io", name, "-n", namespace]
344
+ result = _run_kubectl(args, context)
345
+
346
+ if result["success"]:
347
+ return {
348
+ "success": True,
349
+ "context": context or "current",
350
+ "message": f"Backup '{name}' deleted",
351
+ }
352
+
353
+ return {"success": False, "error": result.get("error", "Failed to delete backup")}
354
+
355
+
356
+ def restore_list(
357
+ namespace: str = "velero",
358
+ context: str = "",
359
+ label_selector: str = ""
360
+ ) -> Dict[str, Any]:
361
+ """List Velero restores.
362
+
363
+ Args:
364
+ namespace: Velero namespace (default: velero)
365
+ context: Kubernetes context to use (optional)
366
+ label_selector: Label selector to filter restores
367
+
368
+ Returns:
369
+ List of restores with their status
370
+ """
371
+ if not crd_exists(VELERO_RESTORE_CRD, context):
372
+ return {
373
+ "success": False,
374
+ "error": "Velero is not installed (restores.velero.io CRD not found)"
375
+ }
376
+
377
+ restores = []
378
+ for item in _get_resources("restores.velero.io", namespace, context, label_selector):
379
+ status = item.get("status", {})
380
+ spec = item.get("spec", {})
381
+ progress = status.get("progress", {})
382
+
383
+ restores.append({
384
+ "name": item["metadata"]["name"],
385
+ "namespace": item["metadata"]["namespace"],
386
+ "phase": status.get("phase", "Unknown"),
387
+ "backup_name": spec.get("backupName", ""),
388
+ "started": status.get("startTimestamp", ""),
389
+ "completed": status.get("completionTimestamp", ""),
390
+ "errors": status.get("errors", 0),
391
+ "warnings": status.get("warnings", 0),
392
+ "items_restored": progress.get("itemsRestored", 0),
393
+ "total_items": progress.get("totalItems", 0),
394
+ "included_namespaces": spec.get("includedNamespaces", []),
395
+ "excluded_namespaces": spec.get("excludedNamespaces", []),
396
+ })
397
+
398
+ completed = sum(1 for r in restores if r["phase"] == "Completed")
399
+ failed = sum(1 for r in restores if r["phase"] == "Failed")
400
+
401
+ return {
402
+ "context": context or "current",
403
+ "total": len(restores),
404
+ "completed": completed,
405
+ "failed": failed,
406
+ "restores": restores,
407
+ }
408
+
409
+
410
+ def restore_create(
411
+ backup_name: str,
412
+ name: str = "",
413
+ namespace: str = "velero",
414
+ included_namespaces: List[str] = None,
415
+ excluded_namespaces: List[str] = None,
416
+ included_resources: List[str] = None,
417
+ excluded_resources: List[str] = None,
418
+ namespace_mappings: Dict[str, str] = None,
419
+ restore_pvs: bool = True,
420
+ context: str = ""
421
+ ) -> Dict[str, Any]:
422
+ """Create a restore from a backup.
423
+
424
+ Args:
425
+ backup_name: Name of the backup to restore from
426
+ name: Restore name (auto-generated if empty)
427
+ namespace: Velero namespace (default: velero)
428
+ included_namespaces: Namespaces to restore
429
+ excluded_namespaces: Namespaces to exclude
430
+ included_resources: Resources to restore
431
+ excluded_resources: Resources to exclude
432
+ namespace_mappings: Map source namespaces to target namespaces
433
+ restore_pvs: Whether to restore persistent volumes
434
+ context: Kubernetes context to use (optional)
435
+
436
+ Returns:
437
+ Restore creation result
438
+ """
439
+ if not crd_exists(VELERO_RESTORE_CRD, context):
440
+ return {"success": False, "error": "Velero is not installed"}
441
+
442
+ if not name:
443
+ name = f"restore-{backup_name}-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"
444
+
445
+ if _velero_cli_available():
446
+ args = ["restore", "create", name, "--from-backup", backup_name, "-n", namespace]
447
+
448
+ if included_namespaces:
449
+ args.extend(["--include-namespaces", ",".join(included_namespaces)])
450
+ if excluded_namespaces:
451
+ args.extend(["--exclude-namespaces", ",".join(excluded_namespaces)])
452
+ if included_resources:
453
+ args.extend(["--include-resources", ",".join(included_resources)])
454
+ if excluded_resources:
455
+ args.extend(["--exclude-resources", ",".join(excluded_resources)])
456
+ if namespace_mappings:
457
+ for src, dst in namespace_mappings.items():
458
+ args.extend(["--namespace-mappings", f"{src}:{dst}"])
459
+ if not restore_pvs:
460
+ args.append("--restore-volumes=false")
461
+
462
+ result = _run_velero(args, context)
463
+ if result["success"]:
464
+ return {
465
+ "success": True,
466
+ "context": context or "current",
467
+ "message": f"Restore '{name}' created from backup '{backup_name}'",
468
+ "restore_name": name,
469
+ "output": result["output"],
470
+ }
471
+ return result
472
+
473
+ restore_spec = {
474
+ "apiVersion": "velero.io/v1",
475
+ "kind": "Restore",
476
+ "metadata": {
477
+ "name": name,
478
+ "namespace": namespace,
479
+ },
480
+ "spec": {
481
+ "backupName": backup_name,
482
+ "restorePVs": restore_pvs,
483
+ }
484
+ }
485
+
486
+ if included_namespaces:
487
+ restore_spec["spec"]["includedNamespaces"] = included_namespaces
488
+ if excluded_namespaces:
489
+ restore_spec["spec"]["excludedNamespaces"] = excluded_namespaces
490
+ if included_resources:
491
+ restore_spec["spec"]["includedResources"] = included_resources
492
+ if excluded_resources:
493
+ restore_spec["spec"]["excludedResources"] = excluded_resources
494
+ if namespace_mappings:
495
+ restore_spec["spec"]["namespaceMapping"] = namespace_mappings
496
+
497
+ args = ["apply", "-f", "-"]
498
+ cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
499
+
500
+ try:
501
+ result = subprocess.run(
502
+ cmd,
503
+ input=json.dumps(restore_spec),
504
+ capture_output=True,
505
+ text=True,
506
+ timeout=60
507
+ )
508
+ if result.returncode == 0:
509
+ return {
510
+ "success": True,
511
+ "context": context or "current",
512
+ "message": f"Restore '{name}' created from backup '{backup_name}'",
513
+ "restore_name": name,
514
+ }
515
+ return {"success": False, "error": result.stderr}
516
+ except Exception as e:
517
+ return {"success": False, "error": str(e)}
518
+
519
+
520
+ def restore_get(
521
+ name: str,
522
+ namespace: str = "velero",
523
+ context: str = ""
524
+ ) -> Dict[str, Any]:
525
+ """Get detailed information about a restore.
526
+
527
+ Args:
528
+ name: Name of the restore
529
+ namespace: Velero namespace (default: velero)
530
+ context: Kubernetes context to use (optional)
531
+
532
+ Returns:
533
+ Detailed restore information
534
+ """
535
+ if not crd_exists(VELERO_RESTORE_CRD, context):
536
+ return {"success": False, "error": "Velero is not installed"}
537
+
538
+ args = ["get", "restores.velero.io", name, "-n", namespace, "-o", "json"]
539
+ result = _run_kubectl(args, context)
540
+
541
+ if result["success"]:
542
+ try:
543
+ data = json.loads(result["output"])
544
+ return {
545
+ "success": True,
546
+ "context": context or "current",
547
+ "restore": data,
548
+ }
549
+ except json.JSONDecodeError:
550
+ return {"success": False, "error": "Failed to parse response"}
551
+
552
+ return {"success": False, "error": result.get("error", "Unknown error")}
553
+
554
+
555
+ def backup_locations_list(
556
+ namespace: str = "velero",
557
+ context: str = ""
558
+ ) -> Dict[str, Any]:
559
+ """List Velero backup storage locations.
560
+
561
+ Args:
562
+ namespace: Velero namespace (default: velero)
563
+ context: Kubernetes context to use (optional)
564
+
565
+ Returns:
566
+ List of backup storage locations
567
+ """
568
+ if not crd_exists(VELERO_BSL_CRD, context):
569
+ return {
570
+ "success": False,
571
+ "error": "Velero is not installed"
572
+ }
573
+
574
+ locations = []
575
+ for item in _get_resources("backupstoragelocations.velero.io", namespace, context):
576
+ status = item.get("status", {})
577
+ spec = item.get("spec", {})
578
+
579
+ locations.append({
580
+ "name": item["metadata"]["name"],
581
+ "namespace": item["metadata"]["namespace"],
582
+ "phase": status.get("phase", "Unknown"),
583
+ "last_sync": status.get("lastSyncedTime", ""),
584
+ "provider": spec.get("provider", ""),
585
+ "bucket": spec.get("objectStorage", {}).get("bucket", ""),
586
+ "prefix": spec.get("objectStorage", {}).get("prefix", ""),
587
+ "default": spec.get("default", False),
588
+ "access_mode": status.get("accessMode", ""),
589
+ })
590
+
591
+ return {
592
+ "context": context or "current",
593
+ "total": len(locations),
594
+ "locations": locations,
595
+ }
596
+
597
+
598
+ def backup_schedules_list(
599
+ namespace: str = "velero",
600
+ context: str = ""
601
+ ) -> Dict[str, Any]:
602
+ """List Velero backup schedules.
603
+
604
+ Args:
605
+ namespace: Velero namespace (default: velero)
606
+ context: Kubernetes context to use (optional)
607
+
608
+ Returns:
609
+ List of backup schedules
610
+ """
611
+ if not crd_exists(VELERO_SCHEDULE_CRD, context):
612
+ return {
613
+ "success": False,
614
+ "error": "Velero is not installed"
615
+ }
616
+
617
+ schedules = []
618
+ for item in _get_resources("schedules.velero.io", namespace, context):
619
+ status = item.get("status", {})
620
+ spec = item.get("spec", {})
621
+ template = spec.get("template", {})
622
+
623
+ schedules.append({
624
+ "name": item["metadata"]["name"],
625
+ "namespace": item["metadata"]["namespace"],
626
+ "phase": status.get("phase", "Unknown"),
627
+ "schedule": spec.get("schedule", ""),
628
+ "last_backup": status.get("lastBackup", ""),
629
+ "paused": spec.get("paused", False),
630
+ "included_namespaces": template.get("includedNamespaces", []),
631
+ "excluded_namespaces": template.get("excludedNamespaces", []),
632
+ "ttl": template.get("ttl", ""),
633
+ "storage_location": template.get("storageLocation", ""),
634
+ })
635
+
636
+ return {
637
+ "context": context or "current",
638
+ "total": len(schedules),
639
+ "schedules": schedules,
640
+ }
641
+
642
+
643
+ def backup_schedule_create(
644
+ name: str,
645
+ schedule: str,
646
+ namespace: str = "velero",
647
+ included_namespaces: List[str] = None,
648
+ excluded_namespaces: List[str] = None,
649
+ ttl: str = "720h",
650
+ storage_location: str = "",
651
+ context: str = ""
652
+ ) -> Dict[str, Any]:
653
+ """Create a backup schedule.
654
+
655
+ Args:
656
+ name: Schedule name
657
+ schedule: Cron schedule (e.g., "0 1 * * *" for daily at 1am)
658
+ namespace: Velero namespace (default: velero)
659
+ included_namespaces: Namespaces to include
660
+ excluded_namespaces: Namespaces to exclude
661
+ ttl: Backup TTL (default: 720h / 30 days)
662
+ storage_location: Backup storage location
663
+ context: Kubernetes context to use (optional)
664
+
665
+ Returns:
666
+ Schedule creation result
667
+ """
668
+ if not crd_exists(VELERO_SCHEDULE_CRD, context):
669
+ return {"success": False, "error": "Velero is not installed"}
670
+
671
+ if _velero_cli_available():
672
+ args = ["schedule", "create", name, "--schedule", schedule, "-n", namespace]
673
+
674
+ if included_namespaces:
675
+ args.extend(["--include-namespaces", ",".join(included_namespaces)])
676
+ if excluded_namespaces:
677
+ args.extend(["--exclude-namespaces", ",".join(excluded_namespaces)])
678
+ if ttl:
679
+ args.extend(["--ttl", ttl])
680
+ if storage_location:
681
+ args.extend(["--storage-location", storage_location])
682
+
683
+ result = _run_velero(args, context)
684
+ if result["success"]:
685
+ return {
686
+ "success": True,
687
+ "context": context or "current",
688
+ "message": f"Schedule '{name}' created",
689
+ "schedule_name": name,
690
+ }
691
+ return result
692
+
693
+ schedule_spec = {
694
+ "apiVersion": "velero.io/v1",
695
+ "kind": "Schedule",
696
+ "metadata": {
697
+ "name": name,
698
+ "namespace": namespace,
699
+ },
700
+ "spec": {
701
+ "schedule": schedule,
702
+ "template": {
703
+ "ttl": ttl,
704
+ }
705
+ }
706
+ }
707
+
708
+ if included_namespaces:
709
+ schedule_spec["spec"]["template"]["includedNamespaces"] = included_namespaces
710
+ if excluded_namespaces:
711
+ schedule_spec["spec"]["template"]["excludedNamespaces"] = excluded_namespaces
712
+ if storage_location:
713
+ schedule_spec["spec"]["template"]["storageLocation"] = storage_location
714
+
715
+ args = ["apply", "-f", "-"]
716
+ cmd = ["kubectl"] + _get_kubectl_context_args(context) + args
717
+
718
+ try:
719
+ result = subprocess.run(
720
+ cmd,
721
+ input=json.dumps(schedule_spec),
722
+ capture_output=True,
723
+ text=True,
724
+ timeout=60
725
+ )
726
+ if result.returncode == 0:
727
+ return {
728
+ "success": True,
729
+ "context": context or "current",
730
+ "message": f"Schedule '{name}' created",
731
+ "schedule_name": name,
732
+ }
733
+ return {"success": False, "error": result.stderr}
734
+ except Exception as e:
735
+ return {"success": False, "error": str(e)}
736
+
737
+
738
+ def backup_detect(context: str = "") -> Dict[str, Any]:
739
+ """Detect if Velero is installed and its components.
740
+
741
+ Args:
742
+ context: Kubernetes context to use (optional)
743
+
744
+ Returns:
745
+ Detection results for Velero
746
+ """
747
+ return {
748
+ "context": context or "current",
749
+ "installed": crd_exists(VELERO_BACKUP_CRD, context),
750
+ "cli_available": _velero_cli_available(),
751
+ "crds": {
752
+ "backups": crd_exists(VELERO_BACKUP_CRD, context),
753
+ "restores": crd_exists(VELERO_RESTORE_CRD, context),
754
+ "schedules": crd_exists(VELERO_SCHEDULE_CRD, context),
755
+ "backup_storage_locations": crd_exists(VELERO_BSL_CRD, context),
756
+ "volume_snapshot_locations": crd_exists(VELERO_VSL_CRD, context),
757
+ },
758
+ }
759
+
760
+
761
+ def register_backup_tools(mcp: FastMCP, non_destructive: bool = False):
762
+ """Register backup tools with the MCP server."""
763
+
764
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
765
+ def backup_list_tool(
766
+ namespace: str = "velero",
767
+ context: str = "",
768
+ label_selector: str = ""
769
+ ) -> str:
770
+ """List Velero backups."""
771
+ return json.dumps(backup_list(namespace, context, label_selector), indent=2)
772
+
773
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
774
+ def backup_get_tool(
775
+ name: str,
776
+ namespace: str = "velero",
777
+ context: str = ""
778
+ ) -> str:
779
+ """Get detailed information about a backup."""
780
+ return json.dumps(backup_get(name, namespace, context), indent=2)
781
+
782
+ @mcp.tool()
783
+ def backup_create_tool(
784
+ name: str = "",
785
+ namespace: str = "velero",
786
+ included_namespaces: str = "",
787
+ excluded_namespaces: str = "",
788
+ ttl: str = "720h",
789
+ snapshot_volumes: bool = True,
790
+ context: str = ""
791
+ ) -> str:
792
+ """Create a new Velero backup."""
793
+ if non_destructive:
794
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
795
+ inc_ns = [n.strip() for n in included_namespaces.split(",") if n.strip()] if included_namespaces else None
796
+ exc_ns = [n.strip() for n in excluded_namespaces.split(",") if n.strip()] if excluded_namespaces else None
797
+ return json.dumps(backup_create(name, namespace, inc_ns, exc_ns, None, None, "", "", ttl, snapshot_volumes, context), indent=2)
798
+
799
+ @mcp.tool()
800
+ def backup_delete_tool(
801
+ name: str,
802
+ namespace: str = "velero",
803
+ context: str = ""
804
+ ) -> str:
805
+ """Delete a Velero backup."""
806
+ if non_destructive:
807
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
808
+ return json.dumps(backup_delete(name, namespace, context), indent=2)
809
+
810
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
811
+ def restore_list_tool(
812
+ namespace: str = "velero",
813
+ context: str = "",
814
+ label_selector: str = ""
815
+ ) -> str:
816
+ """List Velero restores."""
817
+ return json.dumps(restore_list(namespace, context, label_selector), indent=2)
818
+
819
+ @mcp.tool()
820
+ def restore_create_tool(
821
+ backup_name: str,
822
+ name: str = "",
823
+ namespace: str = "velero",
824
+ included_namespaces: str = "",
825
+ excluded_namespaces: str = "",
826
+ restore_pvs: bool = True,
827
+ context: str = ""
828
+ ) -> str:
829
+ """Create a restore from a backup."""
830
+ if non_destructive:
831
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
832
+ inc_ns = [n.strip() for n in included_namespaces.split(",") if n.strip()] if included_namespaces else None
833
+ exc_ns = [n.strip() for n in excluded_namespaces.split(",") if n.strip()] if excluded_namespaces else None
834
+ return json.dumps(restore_create(backup_name, name, namespace, inc_ns, exc_ns, None, None, None, restore_pvs, context), indent=2)
835
+
836
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
837
+ def restore_get_tool(
838
+ name: str,
839
+ namespace: str = "velero",
840
+ context: str = ""
841
+ ) -> str:
842
+ """Get detailed information about a restore."""
843
+ return json.dumps(restore_get(name, namespace, context), indent=2)
844
+
845
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
846
+ def backup_locations_list_tool(
847
+ namespace: str = "velero",
848
+ context: str = ""
849
+ ) -> str:
850
+ """List Velero backup storage locations."""
851
+ return json.dumps(backup_locations_list(namespace, context), indent=2)
852
+
853
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
854
+ def backup_schedules_list_tool(
855
+ namespace: str = "velero",
856
+ context: str = ""
857
+ ) -> str:
858
+ """List Velero backup schedules."""
859
+ return json.dumps(backup_schedules_list(namespace, context), indent=2)
860
+
861
+ @mcp.tool()
862
+ def backup_schedule_create_tool(
863
+ name: str,
864
+ schedule: str,
865
+ namespace: str = "velero",
866
+ included_namespaces: str = "",
867
+ excluded_namespaces: str = "",
868
+ ttl: str = "720h",
869
+ context: str = ""
870
+ ) -> str:
871
+ """Create a backup schedule."""
872
+ if non_destructive:
873
+ return json.dumps({"success": False, "error": "Operation blocked: non-destructive mode"})
874
+ inc_ns = [n.strip() for n in included_namespaces.split(",") if n.strip()] if included_namespaces else None
875
+ exc_ns = [n.strip() for n in excluded_namespaces.split(",") if n.strip()] if excluded_namespaces else None
876
+ return json.dumps(backup_schedule_create(name, schedule, namespace, inc_ns, exc_ns, ttl, "", context), indent=2)
877
+
878
+ @mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
879
+ def backup_detect_tool(context: str = "") -> str:
880
+ """Detect if Velero is installed and its components."""
881
+ return json.dumps(backup_detect(context), indent=2)