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.
- kubectl_mcp_server-1.16.0.dist-info/METADATA +1047 -0
- kubectl_mcp_server-1.16.0.dist-info/RECORD +61 -0
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/crd_detector.py +247 -0
- kubectl_mcp_tool/k8s_config.py +304 -63
- kubectl_mcp_tool/mcp_server.py +27 -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 +395 -121
- kubectl_mcp_tool/tools/core.py +157 -60
- kubectl_mcp_tool/tools/cost.py +97 -41
- kubectl_mcp_tool/tools/deployments.py +173 -56
- kubectl_mcp_tool/tools/diagnostics.py +40 -13
- kubectl_mcp_tool/tools/gitops.py +552 -0
- kubectl_mcp_tool/tools/helm.py +133 -46
- 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/networking.py +106 -32
- kubectl_mcp_tool/tools/operations.py +176 -50
- kubectl_mcp_tool/tools/pods.py +162 -50
- kubectl_mcp_tool/tools/policy.py +554 -0
- kubectl_mcp_tool/tools/rollouts.py +790 -0
- kubectl_mcp_tool/tools/security.py +89 -36
- kubectl_mcp_tool/tools/storage.py +35 -16
- tests/test_browser.py +2 -2
- tests/test_ecosystem.py +331 -0
- tests/test_tools.py +73 -10
- kubectl_mcp_server-1.14.0.dist-info/METADATA +0 -780
- kubectl_mcp_server-1.14.0.dist-info/RECORD +0 -49
- {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.14.0.dist-info → kubectl_mcp_server-1.16.0.dist-info}/licenses/LICENSE +0 -0
- {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)
|