@calltelemetry/ct-lab-mcp 0.1.4 → 0.2.0
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.
- package/dist/bin/ct-lab-mcp.d.ts.map +1 -1
- package/dist/bin/ct-lab-mcp.js +21 -7
- package/dist/bin/ct-lab-mcp.js.map +1 -1
- package/dist/src/appliance/commands.d.ts.map +1 -1
- package/dist/src/appliance/commands.js +24 -0
- package/dist/src/appliance/commands.js.map +1 -1
- package/dist/src/appliance/executor.d.ts.map +1 -1
- package/dist/src/appliance/executor.js +7 -1
- package/dist/src/appliance/executor.js.map +1 -1
- package/dist/src/appliance/mock.d.ts +6 -0
- package/dist/src/appliance/mock.d.ts.map +1 -1
- package/dist/src/appliance/mock.js +27 -6
- package/dist/src/appliance/mock.js.map +1 -1
- package/dist/src/appliance/types.d.ts +20 -1
- package/dist/src/appliance/types.d.ts.map +1 -1
- package/dist/src/appliance/types.js.map +1 -1
- package/dist/src/credentials/errors.d.ts +16 -0
- package/dist/src/credentials/errors.d.ts.map +1 -0
- package/dist/src/credentials/errors.js +21 -0
- package/dist/src/credentials/errors.js.map +1 -0
- package/dist/src/credentials/resolver.d.ts +16 -0
- package/dist/src/credentials/resolver.d.ts.map +1 -1
- package/dist/src/credentials/resolver.js +104 -10
- package/dist/src/credentials/resolver.js.map +1 -1
- package/dist/src/credentials/types.d.ts +8 -0
- package/dist/src/credentials/types.d.ts.map +1 -1
- package/dist/src/diagnostics/collector.d.ts +37 -0
- package/dist/src/diagnostics/collector.d.ts.map +1 -0
- package/dist/src/diagnostics/collector.js +452 -0
- package/dist/src/diagnostics/collector.js.map +1 -0
- package/dist/src/diagnostics/types.d.ts +103 -0
- package/dist/src/diagnostics/types.d.ts.map +1 -0
- package/dist/src/diagnostics/types.js +8 -0
- package/dist/src/diagnostics/types.js.map +1 -0
- package/dist/src/hypervisors/esxi/client.d.ts +1 -0
- package/dist/src/hypervisors/esxi/client.d.ts.map +1 -1
- package/dist/src/hypervisors/esxi/client.js +15 -3
- package/dist/src/hypervisors/esxi/client.js.map +1 -1
- package/dist/src/hypervisors/esxi/mock.d.ts +1 -0
- package/dist/src/hypervisors/esxi/mock.d.ts.map +1 -1
- package/dist/src/hypervisors/esxi/mock.js +4 -0
- package/dist/src/hypervisors/esxi/mock.js.map +1 -1
- package/dist/src/hypervisors/manager.d.ts.map +1 -1
- package/dist/src/hypervisors/manager.js +37 -3
- package/dist/src/hypervisors/manager.js.map +1 -1
- package/dist/src/hypervisors/proxmox/client.d.ts +18 -1
- package/dist/src/hypervisors/proxmox/client.d.ts.map +1 -1
- package/dist/src/hypervisors/proxmox/client.js +563 -3
- package/dist/src/hypervisors/proxmox/client.js.map +1 -1
- package/dist/src/hypervisors/proxmox/mock.d.ts +1 -0
- package/dist/src/hypervisors/proxmox/mock.d.ts.map +1 -1
- package/dist/src/hypervisors/proxmox/mock.js +4 -0
- package/dist/src/hypervisors/proxmox/mock.js.map +1 -1
- package/dist/src/hypervisors/types.d.ts +159 -1
- package/dist/src/hypervisors/types.d.ts.map +1 -1
- package/dist/src/proxmox/mock.d.ts +2 -1
- package/dist/src/proxmox/mock.d.ts.map +1 -1
- package/dist/src/proxmox/mock.js +202 -0
- package/dist/src/proxmox/mock.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +2 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/slot/manager.d.ts +26 -1
- package/dist/src/slot/manager.d.ts.map +1 -1
- package/dist/src/slot/manager.js +278 -1
- package/dist/src/slot/manager.js.map +1 -1
- package/dist/src/slot/types.d.ts +49 -0
- package/dist/src/slot/types.d.ts.map +1 -1
- package/dist/src/slot/types.js +18 -0
- package/dist/src/slot/types.js.map +1 -1
- package/dist/src/tools/credential-status.d.ts +29 -0
- package/dist/src/tools/credential-status.d.ts.map +1 -0
- package/dist/src/tools/credential-status.js +81 -0
- package/dist/src/tools/credential-status.js.map +1 -0
- package/dist/src/tools/cube-provision.d.ts +288 -0
- package/dist/src/tools/cube-provision.d.ts.map +1 -0
- package/dist/src/tools/cube-provision.js +892 -0
- package/dist/src/tools/cube-provision.js.map +1 -0
- package/dist/src/tools/cube-tools.d.ts +137 -0
- package/dist/src/tools/cube-tools.d.ts.map +1 -0
- package/dist/src/tools/cube-tools.js +292 -0
- package/dist/src/tools/cube-tools.js.map +1 -0
- package/dist/src/tools/cucm-emulator-tool.d.ts +85 -0
- package/dist/src/tools/cucm-emulator-tool.d.ts.map +1 -0
- package/dist/src/tools/cucm-emulator-tool.js +191 -0
- package/dist/src/tools/cucm-emulator-tool.js.map +1 -0
- package/dist/src/tools/cucm-trunk.d.ts +165 -0
- package/dist/src/tools/cucm-trunk.d.ts.map +1 -0
- package/dist/src/tools/cucm-trunk.js +354 -0
- package/dist/src/tools/cucm-trunk.js.map +1 -0
- package/dist/src/tools/diagnostics.d.ts +58 -0
- package/dist/src/tools/diagnostics.d.ts.map +1 -0
- package/dist/src/tools/diagnostics.js +212 -0
- package/dist/src/tools/diagnostics.js.map +1 -0
- package/dist/src/tools/guest-agent.d.ts +34 -0
- package/dist/src/tools/guest-agent.d.ts.map +1 -0
- package/dist/src/tools/guest-agent.js +133 -0
- package/dist/src/tools/guest-agent.js.map +1 -0
- package/dist/src/tools/index.d.ts +11 -0
- package/dist/src/tools/index.d.ts.map +1 -1
- package/dist/src/tools/index.js +111 -1
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/list-hosts.d.ts +9 -0
- package/dist/src/tools/list-hosts.d.ts.map +1 -1
- package/dist/src/tools/list-hosts.js.map +1 -1
- package/dist/src/tools/network-tools.d.ts +65 -0
- package/dist/src/tools/network-tools.d.ts.map +1 -0
- package/dist/src/tools/network-tools.js +200 -0
- package/dist/src/tools/network-tools.js.map +1 -0
- package/dist/src/tools/provision-tools.d.ts.map +1 -1
- package/dist/src/tools/provision-tools.js +19 -0
- package/dist/src/tools/provision-tools.js.map +1 -1
- package/dist/src/tools/server-management.d.ts +51 -0
- package/dist/src/tools/server-management.d.ts.map +1 -0
- package/dist/src/tools/server-management.js +175 -0
- package/dist/src/tools/server-management.js.map +1 -0
- package/dist/src/tools/storage-tools.d.ts +250 -0
- package/dist/src/tools/storage-tools.d.ts.map +1 -0
- package/dist/src/tools/storage-tools.js +685 -0
- package/dist/src/tools/storage-tools.js.map +1 -0
- package/dist/src/tools/update-cli.d.ts +62 -0
- package/dist/src/tools/update-cli.d.ts.map +1 -0
- package/dist/src/tools/update-cli.js +142 -0
- package/dist/src/tools/update-cli.js.map +1 -0
- package/dist/src/tools/vm-action.d.ts +6 -6
- package/dist/src/tools/vm-action.d.ts.map +1 -1
- package/dist/src/tools/vm-action.js +3 -1
- package/dist/src/tools/vm-action.js.map +1 -1
- package/dist/src/types/mcp.d.ts +5 -1
- package/dist/src/types/mcp.d.ts.map +1 -1
- package/dist/src/types/mcp.js +14 -0
- package/dist/src/types/mcp.js.map +1 -1
- package/dist/src/types/proxmox.d.ts +1 -0
- package/dist/src/types/proxmox.d.ts.map +1 -1
- package/dist/src/utils/formatters.d.ts +7 -0
- package/dist/src/utils/formatters.d.ts.map +1 -1
- package/dist/src/utils/formatters.js +48 -5
- package/dist/src/utils/formatters.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage & Datastore Management MCP Tools
|
|
3
|
+
*
|
|
4
|
+
* Tools:
|
|
5
|
+
* 1. list_storage - Query datastores, pools, free space, and volume contents
|
|
6
|
+
* 2. list_snapshots - Query VM snapshots across Proxmox and ESXi hosts
|
|
7
|
+
* 3. clean_storage - Delete unreferenced disks, stale ISOs, or snapshots with safety guardrails
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { ProxmoxErrorCode, createToolErrorResult } from "../types/mcp.js";
|
|
11
|
+
import { logger } from "../utils/logger.js";
|
|
12
|
+
import { telemetry } from "../telemetry/registry.js";
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// 1. list_storage
|
|
15
|
+
// ============================================================================
|
|
16
|
+
export const ListStorageInputShape = {
|
|
17
|
+
host: z
|
|
18
|
+
.enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "all"])
|
|
19
|
+
.optional()
|
|
20
|
+
.default("all")
|
|
21
|
+
.describe("Specific host or 'all' to inspect."),
|
|
22
|
+
includeContent: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.default(true)
|
|
26
|
+
.describe("Whether to list detailed volume contents (ISOs, VM disks, backups) inside storage pools."),
|
|
27
|
+
storage: z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Filter to a specific storage pool name (e.g. 'local', 'local-lvm', 'synology-cisco-isos').")
|
|
31
|
+
};
|
|
32
|
+
export const ListStorageInputSchema = z.object(ListStorageInputShape);
|
|
33
|
+
export async function handleListStorage(args, context) {
|
|
34
|
+
const parsed = ListStorageInputSchema.safeParse(args || {});
|
|
35
|
+
if (!parsed.success) {
|
|
36
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for list_storage.");
|
|
37
|
+
}
|
|
38
|
+
const { host, includeContent, storage: targetStorage } = parsed.data;
|
|
39
|
+
const drivers = context.hypervisorManager?.getAllDrivers() || [];
|
|
40
|
+
const matchedDrivers = drivers.filter((d) => {
|
|
41
|
+
if (host === "all")
|
|
42
|
+
return true;
|
|
43
|
+
return d.hostName === host || (host === "proxmox-mini" && d.hostName === "proxmox-minis-lab");
|
|
44
|
+
});
|
|
45
|
+
if (matchedDrivers.length === 0) {
|
|
46
|
+
return createToolErrorResult(ProxmoxErrorCode.HOST_NOT_FOUND, `No matching hypervisor driver found for host '${host}'.`);
|
|
47
|
+
}
|
|
48
|
+
const allPools = [];
|
|
49
|
+
const allContents = [];
|
|
50
|
+
for (const driver of matchedDrivers) {
|
|
51
|
+
if (driver.listStorage) {
|
|
52
|
+
try {
|
|
53
|
+
const pools = await driver.listStorage();
|
|
54
|
+
for (const p of pools) {
|
|
55
|
+
if (!targetStorage || p.storage.toLowerCase() === targetStorage.toLowerCase()) {
|
|
56
|
+
allPools.push(p);
|
|
57
|
+
if (includeContent && driver.listStorageContent && p.active) {
|
|
58
|
+
try {
|
|
59
|
+
const contents = await driver.listStorageContent(p.storage);
|
|
60
|
+
allContents.push(...contents);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
logger.warn(`Failed to query storage content for pool ${p.storage} on ${driver.hostName}:`, err.message);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
logger.error(`Failed to list storage for ${driver.hostName}:`, err.message);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const markdown = formatStorageReport(allPools, allContents);
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: markdown }],
|
|
77
|
+
structuredContent: {
|
|
78
|
+
success: true,
|
|
79
|
+
totalPools: allPools.length,
|
|
80
|
+
pools: allPools,
|
|
81
|
+
contents: allContents
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function formatStorageReport(pools, contents) {
|
|
86
|
+
if (pools.length === 0) {
|
|
87
|
+
return "### 💾 Lab Storage Pools\n\nNo storage pools found.";
|
|
88
|
+
}
|
|
89
|
+
const poolRows = pools.map((p) => {
|
|
90
|
+
const totalGb = (p.totalBytes / (1024 * 1024 * 1024)).toFixed(1);
|
|
91
|
+
const usedGb = (p.usedBytes / (1024 * 1024 * 1024)).toFixed(1);
|
|
92
|
+
const availGb = (p.availableBytes / (1024 * 1024 * 1024)).toFixed(1);
|
|
93
|
+
const pct = Math.round(p.usedFraction * 100);
|
|
94
|
+
const statusIcon = pct > 85 ? "🔴" : pct > 70 ? "🟡" : "🟢";
|
|
95
|
+
return `| **\`${p.hostName}\`** | **\`${p.storage}\`** | \`${p.type}\` | ${totalGb} GB | ${usedGb} GB (${pct}%) | **${availGb} GB** | ${statusIcon} ${p.active ? "Active" : "Inactive"} | \`${p.content}\` |`;
|
|
96
|
+
}).join("\n");
|
|
97
|
+
let contentSection = "";
|
|
98
|
+
if (contents.length > 0) {
|
|
99
|
+
const contentRows = contents.map((c) => {
|
|
100
|
+
const sizeGb = (c.sizeBytes / (1024 * 1024 * 1024)).toFixed(2);
|
|
101
|
+
const vmidStr = c.vmid ? `VM ${c.vmid}` : "N/A (Host/ISO)";
|
|
102
|
+
return `| \`${c.hostName}\` | \`${c.storage}\` | **\`${c.volid}\`** | ${sizeGb} GB | \`${c.content}\` | ${vmidStr} |`;
|
|
103
|
+
}).join("\n");
|
|
104
|
+
contentSection = `\n\n#### 📦 Storage Volume & ISO Content Breakdown\n\n| Host | Storage | Volume ID / ISO | Size | Type | Owner |\n|---|---|---|:---:|:---:|:---:|\n${contentRows}`;
|
|
105
|
+
}
|
|
106
|
+
return `### 💾 Lab Storage Pools & Datastore Analysis
|
|
107
|
+
|
|
108
|
+
| Host | Storage Pool | Type | Total | Used (%) | Free Space | Status | Content Types |
|
|
109
|
+
|---|---|:---:|:---:|:---:|:---:|:---:|---|
|
|
110
|
+
${poolRows}${contentSection}`;
|
|
111
|
+
}
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// 2. list_snapshots
|
|
114
|
+
// ============================================================================
|
|
115
|
+
export const ListSnapshotsInputShape = {
|
|
116
|
+
host: z
|
|
117
|
+
.enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "all"])
|
|
118
|
+
.optional()
|
|
119
|
+
.default("all")
|
|
120
|
+
.describe("Specific host or 'all'."),
|
|
121
|
+
vmId: z
|
|
122
|
+
.string()
|
|
123
|
+
.optional()
|
|
124
|
+
.describe("Specific VM ID or name to inspect snapshots for.")
|
|
125
|
+
};
|
|
126
|
+
export const ListSnapshotsInputSchema = z.object(ListSnapshotsInputShape);
|
|
127
|
+
export async function handleListSnapshots(args, context) {
|
|
128
|
+
const parsed = ListSnapshotsInputSchema.safeParse(args || {});
|
|
129
|
+
if (!parsed.success) {
|
|
130
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for list_snapshots.");
|
|
131
|
+
}
|
|
132
|
+
const { host, vmId } = parsed.data;
|
|
133
|
+
const drivers = context.hypervisorManager?.getAllDrivers() || [];
|
|
134
|
+
const matchedDrivers = drivers.filter((d) => {
|
|
135
|
+
if (host === "all")
|
|
136
|
+
return true;
|
|
137
|
+
return d.hostName === host || (host === "proxmox-mini" && d.hostName === "proxmox-minis-lab");
|
|
138
|
+
});
|
|
139
|
+
const allSnapshots = [];
|
|
140
|
+
for (const driver of matchedDrivers) {
|
|
141
|
+
if (driver.listSnapshots) {
|
|
142
|
+
if (vmId) {
|
|
143
|
+
try {
|
|
144
|
+
const snaps = await driver.listSnapshots(vmId);
|
|
145
|
+
allSnapshots.push(...snaps);
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
logger.warn(`Failed to get snapshots for VM ${vmId} on ${driver.hostName}:`, err.message);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const vms = await driver.listVms();
|
|
153
|
+
for (const vm of vms) {
|
|
154
|
+
try {
|
|
155
|
+
const snaps = await driver.listSnapshots(vm.id);
|
|
156
|
+
allSnapshots.push(...snaps);
|
|
157
|
+
}
|
|
158
|
+
catch { }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const rows = allSnapshots.map((s) => {
|
|
164
|
+
const dateStr = s.snapdate ? s.snapdate.split("T")[0] : "Unknown";
|
|
165
|
+
return `| \`${s.hostName}\` | **${s.vmName}** (\`${s.vmId}\`) | **\`${s.name}\`** | ${dateStr} | ${s.description || "None"} | ${s.vmstate ? "RAM Included" : "Disk Only"} |`;
|
|
166
|
+
}).join("\n");
|
|
167
|
+
const markdown = allSnapshots.length === 0
|
|
168
|
+
? "### 📸 VM Snapshots Analysis\n\nNo VM snapshots found across inspected hosts."
|
|
169
|
+
: `### 📸 VM Snapshots Analysis (${allSnapshots.length} Found)\n\n| Host | VM Name | Snapshot Name | Date | Description | State |\n|---|---|---|:---:|---|:---:|\n${rows}`;
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: "text", text: markdown }],
|
|
172
|
+
structuredContent: {
|
|
173
|
+
success: true,
|
|
174
|
+
totalSnapshots: allSnapshots.length,
|
|
175
|
+
snapshots: allSnapshots
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// 3. clean_storage
|
|
181
|
+
// ============================================================================
|
|
182
|
+
export const CleanStorageInputShape = {
|
|
183
|
+
host: z
|
|
184
|
+
.enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176"])
|
|
185
|
+
.describe("Target hypervisor host where the volume or snapshot resides."),
|
|
186
|
+
action: z
|
|
187
|
+
.enum(["delete_volume", "delete_snapshot"])
|
|
188
|
+
.describe("Action to perform: 'delete_volume' (e.g. stale ISO/disk) or 'delete_snapshot'."),
|
|
189
|
+
volumeId: z
|
|
190
|
+
.string()
|
|
191
|
+
.optional()
|
|
192
|
+
.describe("Volume ID to delete (e.g. 'local:iso/AlmaLinux-9-latest-x86_64-dvd.iso' or 'local-lvm:vm-105-disk-0'). Required if action is 'delete_volume'."),
|
|
193
|
+
vmId: z
|
|
194
|
+
.string()
|
|
195
|
+
.optional()
|
|
196
|
+
.describe("VM ID for snapshot deletion. Required if action is 'delete_snapshot'."),
|
|
197
|
+
snapshotName: z
|
|
198
|
+
.string()
|
|
199
|
+
.optional()
|
|
200
|
+
.describe("Snapshot name to delete. Required if action is 'delete_snapshot'.")
|
|
201
|
+
};
|
|
202
|
+
export const CleanStorageInputSchema = z.object(CleanStorageInputShape);
|
|
203
|
+
export async function handleCleanStorage(args, context) {
|
|
204
|
+
const parsed = CleanStorageInputSchema.safeParse(args);
|
|
205
|
+
if (!parsed.success) {
|
|
206
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for clean_storage.");
|
|
207
|
+
}
|
|
208
|
+
const { host, action, volumeId, vmId, snapshotName } = parsed.data;
|
|
209
|
+
const driver = context.hypervisorManager?.getDriver(host);
|
|
210
|
+
if (!driver) {
|
|
211
|
+
return createToolErrorResult(ProxmoxErrorCode.HOST_NOT_FOUND, `Hypervisor host '${host}' not found.`);
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
if (action === "delete_volume") {
|
|
215
|
+
if (!volumeId) {
|
|
216
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "'volumeId' is required for action 'delete_volume'.");
|
|
217
|
+
}
|
|
218
|
+
if (!driver.deleteVolume) {
|
|
219
|
+
return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${host}' does not support direct volume deletion.`);
|
|
220
|
+
}
|
|
221
|
+
logger.info(`Deleting volume '${volumeId}' on host '${host}'...`);
|
|
222
|
+
await driver.deleteVolume(volumeId);
|
|
223
|
+
telemetry.emit({
|
|
224
|
+
event: "storage.delete_volume",
|
|
225
|
+
actor: "ct-lab-mcp",
|
|
226
|
+
status: "success",
|
|
227
|
+
hostId: host,
|
|
228
|
+
metadata: { volumeId }
|
|
229
|
+
});
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: "text", text: `### 🗑️ Volume Deleted Successfully\n\nVolume **\`${volumeId}\`** was deleted from host **\`${host}\`**.` }],
|
|
232
|
+
structuredContent: { success: true, host, volumeId }
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (action === "delete_snapshot") {
|
|
236
|
+
if (!vmId || !snapshotName) {
|
|
237
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "'vmId' and 'snapshotName' are required for action 'delete_snapshot'.");
|
|
238
|
+
}
|
|
239
|
+
if (!driver.deleteSnapshot) {
|
|
240
|
+
return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${host}' does not support snapshot deletion.`);
|
|
241
|
+
}
|
|
242
|
+
logger.info(`Deleting snapshot '${snapshotName}' on VM '${vmId}' (${host})...`);
|
|
243
|
+
await driver.deleteSnapshot(vmId, snapshotName);
|
|
244
|
+
telemetry.emit({
|
|
245
|
+
event: "storage.delete_snapshot",
|
|
246
|
+
actor: "ct-lab-mcp",
|
|
247
|
+
status: "success",
|
|
248
|
+
hostId: host,
|
|
249
|
+
vmId,
|
|
250
|
+
metadata: { snapshotName }
|
|
251
|
+
});
|
|
252
|
+
return {
|
|
253
|
+
content: [{ type: "text", text: `### 🗑️ Snapshot Deleted Successfully\n\nSnapshot **\`${snapshotName}\`** on VM **\`${vmId}\`** (${host}) was deleted.` }],
|
|
254
|
+
structuredContent: { success: true, host, vmId, snapshotName }
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, `Unknown action '${action}'.`);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
261
|
+
logger.error(`clean_storage failed on ${host}: ${msg}`);
|
|
262
|
+
return createToolErrorResult(ProxmoxErrorCode.VM_ACTION_FAILED, `Failed to clean storage on '${host}': ${msg}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// ============================================================================
|
|
266
|
+
// 4. get_vm_storage
|
|
267
|
+
// ============================================================================
|
|
268
|
+
export const GetVmStorageInputShape = {
|
|
269
|
+
target: z
|
|
270
|
+
.union([z.string(), z.number()])
|
|
271
|
+
.optional()
|
|
272
|
+
.describe("VM ID or name to query storage for (e.g. '105' or 'ct-appliance-slot2-thin')."),
|
|
273
|
+
vmIdOrName: z
|
|
274
|
+
.union([z.string(), z.number()])
|
|
275
|
+
.optional()
|
|
276
|
+
.describe("Alias for target."),
|
|
277
|
+
host: z
|
|
278
|
+
.enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "auto"])
|
|
279
|
+
.optional()
|
|
280
|
+
.default("auto")
|
|
281
|
+
.describe("Hypervisor host or 'auto' to auto-discover.")
|
|
282
|
+
};
|
|
283
|
+
export const GetVmStorageInputSchema = z
|
|
284
|
+
.object(GetVmStorageInputShape)
|
|
285
|
+
.refine((data) => Boolean(data.target || data.vmIdOrName), { message: "VM target ('target' or 'vmIdOrName') is required." });
|
|
286
|
+
export async function handleGetVmStorage(args, context) {
|
|
287
|
+
const parsed = GetVmStorageInputSchema.safeParse(args || {});
|
|
288
|
+
if (!parsed.success) {
|
|
289
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for get_vm_storage.");
|
|
290
|
+
}
|
|
291
|
+
const target = parsed.data.target || parsed.data.vmIdOrName;
|
|
292
|
+
const targetHost = parsed.data.host;
|
|
293
|
+
const drivers = context.hypervisorManager?.getAllDrivers() || [];
|
|
294
|
+
let foundDriver = targetHost && targetHost !== "auto"
|
|
295
|
+
? drivers.find(d => d.hostName === targetHost || (targetHost === "proxmox-mini" && d.hostName === "proxmox-minis-lab"))
|
|
296
|
+
: null;
|
|
297
|
+
let vmRecord = null;
|
|
298
|
+
if (foundDriver) {
|
|
299
|
+
vmRecord = await foundDriver.getVm(target);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
for (const d of drivers) {
|
|
303
|
+
const vm = await d.getVm(target);
|
|
304
|
+
if (vm) {
|
|
305
|
+
foundDriver = d;
|
|
306
|
+
vmRecord = vm;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!foundDriver || !vmRecord) {
|
|
312
|
+
return createToolErrorResult(ProxmoxErrorCode.VM_NOT_FOUND, `VM '${target}' was not found on any hypervisor host.`);
|
|
313
|
+
}
|
|
314
|
+
if (!foundDriver.getVmStorage) {
|
|
315
|
+
return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${foundDriver.hostName}' does not support getVmStorage.`);
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const report = await foundDriver.getVmStorage(vmRecord.id);
|
|
319
|
+
// Format Markdown tables
|
|
320
|
+
const diskRows = report.disks.map((d) => {
|
|
321
|
+
const sizeStr = d.sizeGb ? `${d.sizeGb.toFixed(1)} GB` : "Unknown";
|
|
322
|
+
const flags = [d.discard ? "discard" : null, d.ssd ? "ssd" : null].filter(Boolean).join(", ");
|
|
323
|
+
return `| **\`${d.drive}\`** | \`${d.storage}\` | **\`${d.volid}\`** | ${sizeStr} | \`${d.format}\` | ${flags || "standard"} |`;
|
|
324
|
+
}).join("\n");
|
|
325
|
+
const cdromRows = report.cdroms.map((c) => {
|
|
326
|
+
const mediaStr = c.volid === "none" || !c.volid ? "*(Empty / Ejected)*" : `**\`${c.volid}\`**`;
|
|
327
|
+
return `| **\`${c.drive}\`** | ${mediaStr} | \`${c.storage}\` | \`cdrom\` |`;
|
|
328
|
+
}).join("\n");
|
|
329
|
+
let fsSection = "";
|
|
330
|
+
if (report.guestFilesystems && report.guestFilesystems.length > 0) {
|
|
331
|
+
const fsRows = report.guestFilesystems.map((fs) => {
|
|
332
|
+
const totalGb = (fs.totalBytes / (1024 * 1024 * 1024)).toFixed(2);
|
|
333
|
+
const usedGb = (fs.usedBytes / (1024 * 1024 * 1024)).toFixed(2);
|
|
334
|
+
const availGb = (fs.availableBytes / (1024 * 1024 * 1024)).toFixed(2);
|
|
335
|
+
const icon = fs.usedPercent > 85 ? "🔴" : fs.usedPercent > 70 ? "🟡" : "🟢";
|
|
336
|
+
return `| **\`${fs.mountPoint}\`** | \`${fs.fsType}\` | ${totalGb} GB | ${usedGb} GB (${fs.usedPercent}%) | **${availGb} GB** | ${icon} | \`${fs.diskDevice || "N/A"}\` |`;
|
|
337
|
+
}).join("\n");
|
|
338
|
+
fsSection = `\n\n#### 💽 Live Guest OS Filesystems (QEMU Agent)\n\n| Mount Point | Type | Total | Used (%) | Free | Status | Device |\n|---|:---:|:---:|:---:|:---:|:---:|:---:|\n${fsRows}`;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
fsSection = `\n\n*Guest Filesystem Info: ${report.status === "running" ? "QEMU Agent not responding / initializing" : "VM is stopped"}*`;
|
|
342
|
+
}
|
|
343
|
+
const markdown = `### 💾 VM Storage & Filesystem Analysis: **${report.vmName}** (\`${report.vmId}\`)
|
|
344
|
+
|
|
345
|
+
*Host:* \`${report.hostName}\` | *Status:* **${report.status}** | *Guest Agent:* **${report.guestAgentActive ? "🟢 Active" : "⚪ Offline"}**
|
|
346
|
+
|
|
347
|
+
#### 🗄️ Hypervisor Virtual Disks
|
|
348
|
+
| Drive | Storage Pool | Volume ID | Size | Format | Flags |
|
|
349
|
+
|---|---|---|:---:|:---:|:---:|
|
|
350
|
+
${diskRows || "| - | No virtual disks attached | - | - | - | - |"}
|
|
351
|
+
|
|
352
|
+
#### 💿 Optical Drives & Mounted Media (CD-ROM)
|
|
353
|
+
| Drive | Mounted Media / ISO | Storage Pool | Type |
|
|
354
|
+
|---|---|---|:---:|
|
|
355
|
+
${cdromRows || "| - | No CD-ROM drives configured | - | - |"}${fsSection}`;
|
|
356
|
+
return {
|
|
357
|
+
content: [{ type: "text", text: markdown }],
|
|
358
|
+
structuredContent: {
|
|
359
|
+
success: true,
|
|
360
|
+
report
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
366
|
+
return createToolErrorResult(ProxmoxErrorCode.UPSTREAM_API_ERROR, `Failed to query VM storage for '${target}': ${msg}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// ============================================================================
|
|
370
|
+
// 5. mount_iso
|
|
371
|
+
// ============================================================================
|
|
372
|
+
export const MountIsoInputShape = {
|
|
373
|
+
target: z
|
|
374
|
+
.union([z.string(), z.number()])
|
|
375
|
+
.optional()
|
|
376
|
+
.describe("Target VM ID or name to mount ISO onto (e.g. '105' or 'ct-appliance-slot2-thin')."),
|
|
377
|
+
vmIdOrName: z
|
|
378
|
+
.union([z.string(), z.number()])
|
|
379
|
+
.optional()
|
|
380
|
+
.describe("Alias for target."),
|
|
381
|
+
iso: z
|
|
382
|
+
.string()
|
|
383
|
+
.describe("ISO Volume ID to mount (e.g. 'synology-cisco-isos:iso/c8000v-universalk9.17.18.02.iso' or 'local:iso/cube-pmx-internal-bootstrap.iso')."),
|
|
384
|
+
drive: z
|
|
385
|
+
.string()
|
|
386
|
+
.optional()
|
|
387
|
+
.default("ide2")
|
|
388
|
+
.describe("CD-ROM drive identifier (default: 'ide2')."),
|
|
389
|
+
host: z
|
|
390
|
+
.enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "auto"])
|
|
391
|
+
.optional()
|
|
392
|
+
.default("auto")
|
|
393
|
+
.describe("Hypervisor host or 'auto'.")
|
|
394
|
+
};
|
|
395
|
+
export const MountIsoInputSchema = z
|
|
396
|
+
.object(MountIsoInputShape)
|
|
397
|
+
.refine((data) => Boolean(data.target || data.vmIdOrName), { message: "VM target ('target' or 'vmIdOrName') is required." });
|
|
398
|
+
export async function handleMountIso(args, context) {
|
|
399
|
+
const parsed = MountIsoInputSchema.safeParse(args || {});
|
|
400
|
+
if (!parsed.success) {
|
|
401
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for mount_iso.");
|
|
402
|
+
}
|
|
403
|
+
const target = parsed.data.target || parsed.data.vmIdOrName;
|
|
404
|
+
const { iso, drive, host: targetHost } = parsed.data;
|
|
405
|
+
const drivers = context.hypervisorManager?.getAllDrivers() || [];
|
|
406
|
+
let foundDriver = targetHost && targetHost !== "auto"
|
|
407
|
+
? drivers.find(d => d.hostName === targetHost || (targetHost === "proxmox-mini" && d.hostName === "proxmox-minis-lab"))
|
|
408
|
+
: null;
|
|
409
|
+
let vmRecord = null;
|
|
410
|
+
if (foundDriver) {
|
|
411
|
+
vmRecord = await foundDriver.getVm(target);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
for (const d of drivers) {
|
|
415
|
+
const vm = await d.getVm(target);
|
|
416
|
+
if (vm) {
|
|
417
|
+
foundDriver = d;
|
|
418
|
+
vmRecord = vm;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (!foundDriver || !vmRecord) {
|
|
424
|
+
return createToolErrorResult(ProxmoxErrorCode.VM_NOT_FOUND, `VM '${target}' not found.`);
|
|
425
|
+
}
|
|
426
|
+
if (!foundDriver.mountIso) {
|
|
427
|
+
return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${foundDriver.hostName}' does not support mountIso.`);
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
await foundDriver.mountIso(vmRecord.id, iso, drive);
|
|
431
|
+
telemetry.emit({
|
|
432
|
+
event: "storage.mount_iso",
|
|
433
|
+
actor: "ct-lab-mcp",
|
|
434
|
+
status: "success",
|
|
435
|
+
hostId: foundDriver.hostName,
|
|
436
|
+
vmId: vmRecord.id,
|
|
437
|
+
metadata: { iso, drive }
|
|
438
|
+
});
|
|
439
|
+
return {
|
|
440
|
+
content: [{ type: "text", text: `### 💿 ISO Mounted Successfully\n\nISO **\`${iso}\`** was mounted to drive **\`${drive}\`** on VM **${vmRecord.name}** (\`${vmRecord.id}\`) on host \`${foundDriver.hostName}\`.` }],
|
|
441
|
+
structuredContent: { success: true, host: foundDriver.hostName, vmId: vmRecord.id, vmName: vmRecord.name, iso, drive }
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
446
|
+
return createToolErrorResult(ProxmoxErrorCode.VM_ACTION_FAILED, `Failed to mount ISO on VM '${target}': ${msg}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// ============================================================================
|
|
450
|
+
// 6. unmount_iso
|
|
451
|
+
// ============================================================================
|
|
452
|
+
export const UnmountIsoInputShape = {
|
|
453
|
+
target: z
|
|
454
|
+
.union([z.string(), z.number()])
|
|
455
|
+
.optional()
|
|
456
|
+
.describe("Target VM ID or name to unmount ISO from (e.g. '105')."),
|
|
457
|
+
vmIdOrName: z
|
|
458
|
+
.union([z.string(), z.number()])
|
|
459
|
+
.optional()
|
|
460
|
+
.describe("Alias for target."),
|
|
461
|
+
drive: z
|
|
462
|
+
.string()
|
|
463
|
+
.optional()
|
|
464
|
+
.default("ide2")
|
|
465
|
+
.describe("CD-ROM drive identifier (default: 'ide2')."),
|
|
466
|
+
host: z
|
|
467
|
+
.enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "auto"])
|
|
468
|
+
.optional()
|
|
469
|
+
.default("auto")
|
|
470
|
+
.describe("Hypervisor host or 'auto'.")
|
|
471
|
+
};
|
|
472
|
+
export const UnmountIsoInputSchema = z
|
|
473
|
+
.object(UnmountIsoInputShape)
|
|
474
|
+
.refine((data) => Boolean(data.target || data.vmIdOrName), { message: "VM target ('target' or 'vmIdOrName') is required." });
|
|
475
|
+
export async function handleUnmountIso(args, context) {
|
|
476
|
+
const parsed = UnmountIsoInputSchema.safeParse(args || {});
|
|
477
|
+
if (!parsed.success) {
|
|
478
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for unmount_iso.");
|
|
479
|
+
}
|
|
480
|
+
const target = parsed.data.target || parsed.data.vmIdOrName;
|
|
481
|
+
const { drive, host: targetHost } = parsed.data;
|
|
482
|
+
const drivers = context.hypervisorManager?.getAllDrivers() || [];
|
|
483
|
+
let foundDriver = targetHost && targetHost !== "auto"
|
|
484
|
+
? drivers.find(d => d.hostName === targetHost || (targetHost === "proxmox-mini" && d.hostName === "proxmox-minis-lab"))
|
|
485
|
+
: null;
|
|
486
|
+
let vmRecord = null;
|
|
487
|
+
if (foundDriver) {
|
|
488
|
+
vmRecord = await foundDriver.getVm(target);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
for (const d of drivers) {
|
|
492
|
+
const vm = await d.getVm(target);
|
|
493
|
+
if (vm) {
|
|
494
|
+
foundDriver = d;
|
|
495
|
+
vmRecord = vm;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (!foundDriver || !vmRecord) {
|
|
501
|
+
return createToolErrorResult(ProxmoxErrorCode.VM_NOT_FOUND, `VM '${target}' not found.`);
|
|
502
|
+
}
|
|
503
|
+
if (!foundDriver.unmountIso) {
|
|
504
|
+
return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${foundDriver.hostName}' does not support unmountIso.`);
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
await foundDriver.unmountIso(vmRecord.id, drive);
|
|
508
|
+
telemetry.emit({
|
|
509
|
+
event: "storage.unmount_iso",
|
|
510
|
+
actor: "ct-lab-mcp",
|
|
511
|
+
status: "success",
|
|
512
|
+
hostId: foundDriver.hostName,
|
|
513
|
+
vmId: vmRecord.id,
|
|
514
|
+
metadata: { drive }
|
|
515
|
+
});
|
|
516
|
+
return {
|
|
517
|
+
content: [{ type: "text", text: `### ⏏️ CD-ROM Drive Ejected\n\nDrive **\`${drive}\`** on VM **${vmRecord.name}** (\`${vmRecord.id}\`) on host \`${foundDriver.hostName}\` was unmounted / ejected.` }],
|
|
518
|
+
structuredContent: { success: true, host: foundDriver.hostName, vmId: vmRecord.id, vmName: vmRecord.name, drive }
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
523
|
+
return createToolErrorResult(ProxmoxErrorCode.VM_ACTION_FAILED, `Failed to unmount ISO on VM '${target}': ${msg}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// ============================================================================
|
|
527
|
+
// 7. create_snapshot
|
|
528
|
+
// ============================================================================
|
|
529
|
+
export const CreateSnapshotInputShape = {
|
|
530
|
+
target: z
|
|
531
|
+
.union([z.string(), z.number()])
|
|
532
|
+
.optional()
|
|
533
|
+
.describe("Target VM ID or name to snapshot (e.g. '105' or 'ct-appliance-slot2-thin')."),
|
|
534
|
+
vmIdOrName: z
|
|
535
|
+
.union([z.string(), z.number()])
|
|
536
|
+
.optional()
|
|
537
|
+
.describe("Alias for target."),
|
|
538
|
+
snapshotName: z
|
|
539
|
+
.string()
|
|
540
|
+
.describe("Name for the new snapshot (e.g. 'pre-upgrade', 'clean-base')."),
|
|
541
|
+
description: z
|
|
542
|
+
.string()
|
|
543
|
+
.optional()
|
|
544
|
+
.describe("Optional human-readable description for the snapshot."),
|
|
545
|
+
includeRam: z
|
|
546
|
+
.boolean()
|
|
547
|
+
.optional()
|
|
548
|
+
.default(false)
|
|
549
|
+
.describe("Whether to include RAM / live memory state in the snapshot (default: false)."),
|
|
550
|
+
host: z
|
|
551
|
+
.enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "auto"])
|
|
552
|
+
.optional()
|
|
553
|
+
.default("auto")
|
|
554
|
+
.describe("Hypervisor host or 'auto'.")
|
|
555
|
+
};
|
|
556
|
+
export const CreateSnapshotInputSchema = z
|
|
557
|
+
.object(CreateSnapshotInputShape)
|
|
558
|
+
.refine((data) => Boolean(data.target || data.vmIdOrName), { message: "VM target ('target' or 'vmIdOrName') is required." });
|
|
559
|
+
export async function handleCreateSnapshot(args, context) {
|
|
560
|
+
const parsed = CreateSnapshotInputSchema.safeParse(args || {});
|
|
561
|
+
if (!parsed.success) {
|
|
562
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for create_snapshot.");
|
|
563
|
+
}
|
|
564
|
+
const target = parsed.data.target || parsed.data.vmIdOrName;
|
|
565
|
+
const { snapshotName, description, includeRam, host: targetHost } = parsed.data;
|
|
566
|
+
const drivers = context.hypervisorManager?.getAllDrivers() || [];
|
|
567
|
+
let foundDriver = targetHost && targetHost !== "auto"
|
|
568
|
+
? drivers.find(d => d.hostName === targetHost || (targetHost === "proxmox-mini" && d.hostName === "proxmox-minis-lab"))
|
|
569
|
+
: null;
|
|
570
|
+
let vmRecord = null;
|
|
571
|
+
if (foundDriver) {
|
|
572
|
+
vmRecord = await foundDriver.getVm(target);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
for (const d of drivers) {
|
|
576
|
+
const vm = await d.getVm(target);
|
|
577
|
+
if (vm) {
|
|
578
|
+
foundDriver = d;
|
|
579
|
+
vmRecord = vm;
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (!foundDriver || !vmRecord) {
|
|
585
|
+
return createToolErrorResult(ProxmoxErrorCode.VM_NOT_FOUND, `VM '${target}' not found.`);
|
|
586
|
+
}
|
|
587
|
+
if (!foundDriver.createSnapshot) {
|
|
588
|
+
return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${foundDriver.hostName}' does not support createSnapshot.`);
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
await foundDriver.createSnapshot(vmRecord.id, snapshotName, description, includeRam);
|
|
592
|
+
telemetry.emit({
|
|
593
|
+
event: "storage.create_snapshot",
|
|
594
|
+
actor: "ct-lab-mcp",
|
|
595
|
+
status: "success",
|
|
596
|
+
hostId: foundDriver.hostName,
|
|
597
|
+
vmId: vmRecord.id,
|
|
598
|
+
metadata: { snapshotName, includeRam }
|
|
599
|
+
});
|
|
600
|
+
return {
|
|
601
|
+
content: [{ type: "text", text: `### 📸 Snapshot Created Successfully\n\nSnapshot **\`${snapshotName}\`** was created for VM **${vmRecord.name}** (\`${vmRecord.id}\`) on host \`${foundDriver.hostName}\`.\n*RAM State:* ${includeRam ? "Included" : "Disk Only"}\n*Description:* ${description || "None"}` }],
|
|
602
|
+
structuredContent: { success: true, host: foundDriver.hostName, vmId: vmRecord.id, vmName: vmRecord.name, snapshotName, includeRam, description }
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
catch (err) {
|
|
606
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
607
|
+
return createToolErrorResult(ProxmoxErrorCode.VM_ACTION_FAILED, `Failed to create snapshot '${snapshotName}' on VM '${target}': ${msg}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// ============================================================================
|
|
611
|
+
// 8. rollback_snapshot
|
|
612
|
+
// ============================================================================
|
|
613
|
+
export const RollbackSnapshotInputShape = {
|
|
614
|
+
target: z
|
|
615
|
+
.union([z.string(), z.number()])
|
|
616
|
+
.optional()
|
|
617
|
+
.describe("Target VM ID or name to rollback (e.g. '105')."),
|
|
618
|
+
vmIdOrName: z
|
|
619
|
+
.union([z.string(), z.number()])
|
|
620
|
+
.optional()
|
|
621
|
+
.describe("Alias for target."),
|
|
622
|
+
snapshotName: z
|
|
623
|
+
.string()
|
|
624
|
+
.describe("Name of the snapshot to rollback / revert to (e.g. 'clean-base')."),
|
|
625
|
+
host: z
|
|
626
|
+
.enum(["proxmox-lab", "proxmox-mini", "proxmox-minis-lab", "esxi-intel-192-168-123-176", "auto"])
|
|
627
|
+
.optional()
|
|
628
|
+
.default("auto")
|
|
629
|
+
.describe("Hypervisor host or 'auto'.")
|
|
630
|
+
};
|
|
631
|
+
export const RollbackSnapshotInputSchema = z
|
|
632
|
+
.object(RollbackSnapshotInputShape)
|
|
633
|
+
.refine((data) => Boolean(data.target || data.vmIdOrName), { message: "VM target ('target' or 'vmIdOrName') is required." });
|
|
634
|
+
export async function handleRollbackSnapshot(args, context) {
|
|
635
|
+
const parsed = RollbackSnapshotInputSchema.safeParse(args || {});
|
|
636
|
+
if (!parsed.success) {
|
|
637
|
+
return createToolErrorResult(ProxmoxErrorCode.VALIDATION_ERROR, "Invalid arguments for rollback_snapshot.");
|
|
638
|
+
}
|
|
639
|
+
const target = parsed.data.target || parsed.data.vmIdOrName;
|
|
640
|
+
const { snapshotName, host: targetHost } = parsed.data;
|
|
641
|
+
const drivers = context.hypervisorManager?.getAllDrivers() || [];
|
|
642
|
+
let foundDriver = targetHost && targetHost !== "auto"
|
|
643
|
+
? drivers.find(d => d.hostName === targetHost || (targetHost === "proxmox-mini" && d.hostName === "proxmox-minis-lab"))
|
|
644
|
+
: null;
|
|
645
|
+
let vmRecord = null;
|
|
646
|
+
if (foundDriver) {
|
|
647
|
+
vmRecord = await foundDriver.getVm(target);
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
for (const d of drivers) {
|
|
651
|
+
const vm = await d.getVm(target);
|
|
652
|
+
if (vm) {
|
|
653
|
+
foundDriver = d;
|
|
654
|
+
vmRecord = vm;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (!foundDriver || !vmRecord) {
|
|
660
|
+
return createToolErrorResult(ProxmoxErrorCode.VM_NOT_FOUND, `VM '${target}' not found.`);
|
|
661
|
+
}
|
|
662
|
+
if (!foundDriver.rollbackSnapshot) {
|
|
663
|
+
return createToolErrorResult(ProxmoxErrorCode.OPERATION_NOT_SUPPORTED, `Host '${foundDriver.hostName}' does not support rollbackSnapshot.`);
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
await foundDriver.rollbackSnapshot(vmRecord.id, snapshotName);
|
|
667
|
+
telemetry.emit({
|
|
668
|
+
event: "storage.rollback_snapshot",
|
|
669
|
+
actor: "ct-lab-mcp",
|
|
670
|
+
status: "success",
|
|
671
|
+
hostId: foundDriver.hostName,
|
|
672
|
+
vmId: vmRecord.id,
|
|
673
|
+
metadata: { snapshotName }
|
|
674
|
+
});
|
|
675
|
+
return {
|
|
676
|
+
content: [{ type: "text", text: `### 🔄 Snapshot Rollback Completed\n\nVM **${vmRecord.name}** (\`${vmRecord.id}\`) on host \`${foundDriver.hostName}\` was reverted to snapshot **\`${snapshotName}\`**.` }],
|
|
677
|
+
structuredContent: { success: true, host: foundDriver.hostName, vmId: vmRecord.id, vmName: vmRecord.name, snapshotName }
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
682
|
+
return createToolErrorResult(ProxmoxErrorCode.VM_ACTION_FAILED, `Failed to rollback snapshot '${snapshotName}' on VM '${target}': ${msg}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
//# sourceMappingURL=storage-tools.js.map
|