@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
|
@@ -10,11 +10,13 @@ export class ProxmoxDriver {
|
|
|
10
10
|
hostName;
|
|
11
11
|
hypervisorType = "proxmox";
|
|
12
12
|
hostConfig;
|
|
13
|
+
isMock;
|
|
13
14
|
client;
|
|
14
15
|
safetyEngine;
|
|
15
16
|
constructor(hostConfig, clientOrPoller, safetyEngine = defaultSafetyGuardrailEngine) {
|
|
16
17
|
this.hostConfig = hostConfig;
|
|
17
18
|
this.hostName = hostConfig.id;
|
|
19
|
+
this.isMock = hostConfig.isMock === true;
|
|
18
20
|
this.safetyEngine = safetyEngine;
|
|
19
21
|
if (clientOrPoller && "listVms" in clientOrPoller) {
|
|
20
22
|
this.client = clientOrPoller;
|
|
@@ -100,7 +102,8 @@ export class ProxmoxDriver {
|
|
|
100
102
|
},
|
|
101
103
|
endpoint: this.hostConfig.baseUrl,
|
|
102
104
|
runningVms,
|
|
103
|
-
totalVms
|
|
105
|
+
totalVms,
|
|
106
|
+
mock: this.isMock
|
|
104
107
|
};
|
|
105
108
|
}
|
|
106
109
|
catch (err) {
|
|
@@ -122,7 +125,9 @@ export class ProxmoxDriver {
|
|
|
122
125
|
usedSlots: 0,
|
|
123
126
|
availableSlots: 0,
|
|
124
127
|
slotSizeBytes: (this.hostConfig.slotSizeMb || 8192) * 1024 * 1024
|
|
125
|
-
}
|
|
128
|
+
},
|
|
129
|
+
endpoint: this.hostConfig.baseUrl,
|
|
130
|
+
mock: this.isMock
|
|
126
131
|
};
|
|
127
132
|
}
|
|
128
133
|
}
|
|
@@ -158,6 +163,7 @@ export class ProxmoxDriver {
|
|
|
158
163
|
return found || null;
|
|
159
164
|
}
|
|
160
165
|
async cloneVm(params) {
|
|
166
|
+
const startTime = Date.now();
|
|
161
167
|
const vms = await this.listVms();
|
|
162
168
|
// 1. Resolve source template VM
|
|
163
169
|
const sourceStr = String(params.sourceVmIdOrName).toLowerCase().trim();
|
|
@@ -209,7 +215,13 @@ export class ProxmoxDriver {
|
|
|
209
215
|
slotClaimId: params.slotClaimId,
|
|
210
216
|
taskRef: cloneResult.upid,
|
|
211
217
|
taskUpid: cloneResult.upid,
|
|
212
|
-
createdAt: new Date().toISOString()
|
|
218
|
+
createdAt: new Date().toISOString(),
|
|
219
|
+
mock: this.isMock,
|
|
220
|
+
transportEvidence: {
|
|
221
|
+
endpoint: this.hostConfig.baseUrl,
|
|
222
|
+
elapsedMs: Date.now() - startTime,
|
|
223
|
+
real: !this.isMock
|
|
224
|
+
}
|
|
213
225
|
};
|
|
214
226
|
}
|
|
215
227
|
async startVm(vmIdOrName) {
|
|
@@ -257,6 +269,554 @@ export class ProxmoxDriver {
|
|
|
257
269
|
throw new Error(`Failed to destroy VM ${vmid}: ${res.error || res.exitStatus}`);
|
|
258
270
|
}
|
|
259
271
|
}
|
|
272
|
+
async listStorage() {
|
|
273
|
+
const nodeName = await this.ensureNodeName();
|
|
274
|
+
const rawList = await this.client.request("GET", `/nodes/${nodeName}/storage`);
|
|
275
|
+
return rawList.map((item) => ({
|
|
276
|
+
storage: item.storage,
|
|
277
|
+
type: item.type || "unknown",
|
|
278
|
+
totalBytes: item.total || 0,
|
|
279
|
+
usedBytes: item.used || 0,
|
|
280
|
+
availableBytes: item.avail || 0,
|
|
281
|
+
usedFraction: item.used_fraction || (item.total ? item.used / item.total : 0),
|
|
282
|
+
active: Boolean(item.active),
|
|
283
|
+
enabled: Boolean(item.enabled),
|
|
284
|
+
shared: Boolean(item.shared),
|
|
285
|
+
content: item.content || "",
|
|
286
|
+
hostName: this.hostName,
|
|
287
|
+
hypervisor: "proxmox"
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
async listStorageContent(storage) {
|
|
291
|
+
const nodeName = await this.ensureNodeName();
|
|
292
|
+
const rawList = await this.client.request("GET", `/nodes/${nodeName}/storage/${storage}/content`);
|
|
293
|
+
return rawList.map((item) => ({
|
|
294
|
+
volid: item.volid,
|
|
295
|
+
storage: storage,
|
|
296
|
+
vmid: item.vmid,
|
|
297
|
+
sizeBytes: item.size || 0,
|
|
298
|
+
format: item.format,
|
|
299
|
+
content: item.content || "unknown",
|
|
300
|
+
notes: item.notes,
|
|
301
|
+
hostName: this.hostName,
|
|
302
|
+
hypervisor: "proxmox"
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
async listSnapshots(vmIdOrName) {
|
|
306
|
+
const vm = await this.getVm(vmIdOrName);
|
|
307
|
+
if (!vm)
|
|
308
|
+
return [];
|
|
309
|
+
const nodeName = await this.ensureNodeName();
|
|
310
|
+
const vmid = Number(vm.id);
|
|
311
|
+
const rawList = await this.client.request("GET", `/nodes/${nodeName}/qemu/${vmid}/snapshot`);
|
|
312
|
+
return rawList
|
|
313
|
+
.filter((s) => s.name !== "current")
|
|
314
|
+
.map((s) => ({
|
|
315
|
+
name: s.name,
|
|
316
|
+
description: s.description,
|
|
317
|
+
snaptime: s.snaptime,
|
|
318
|
+
snapdate: s.snaptime ? new Date(s.snaptime * 1000).toISOString() : undefined,
|
|
319
|
+
vmstate: Boolean(s.vmstate),
|
|
320
|
+
vmId: vmid,
|
|
321
|
+
vmName: vm.name,
|
|
322
|
+
hostName: this.hostName,
|
|
323
|
+
hypervisor: "proxmox"
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
async createSnapshot(vmIdOrName, snapName, description, includeRam = false) {
|
|
327
|
+
const vm = await this.getVm(vmIdOrName);
|
|
328
|
+
if (!vm) {
|
|
329
|
+
throw new Error(`VM '${vmIdOrName}' not found on Proxmox host '${this.hostName}'.`);
|
|
330
|
+
}
|
|
331
|
+
const nodeName = await this.ensureNodeName();
|
|
332
|
+
const vmid = Number(vm.id);
|
|
333
|
+
const body = {
|
|
334
|
+
snapname: snapName,
|
|
335
|
+
description: description || "",
|
|
336
|
+
vmstate: includeRam ? 1 : 0
|
|
337
|
+
};
|
|
338
|
+
const task = await this.client.request("POST", `/nodes/${nodeName}/qemu/${vmid}/snapshot`, body);
|
|
339
|
+
if (task && typeof task === "string") {
|
|
340
|
+
try {
|
|
341
|
+
await defaultTaskPoller.waitForTask(this.client, nodeName, task);
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
logger.debug(`Snapshot task wait: ${err.message}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async rollbackSnapshot(vmIdOrName, snapName) {
|
|
349
|
+
const vm = await this.getVm(vmIdOrName);
|
|
350
|
+
if (!vm) {
|
|
351
|
+
throw new Error(`VM '${vmIdOrName}' not found on Proxmox host '${this.hostName}'.`);
|
|
352
|
+
}
|
|
353
|
+
if (vm.isProtected) {
|
|
354
|
+
throw new Error(`SAFETY_GUARDRAIL: Cannot rollback snapshot on protected infrastructure VM '${vm.name}' (${vm.id}).`);
|
|
355
|
+
}
|
|
356
|
+
const nodeName = await this.ensureNodeName();
|
|
357
|
+
const vmid = Number(vm.id);
|
|
358
|
+
const task = await this.client.request("POST", `/nodes/${nodeName}/qemu/${vmid}/snapshot/${snapName}/rollback`);
|
|
359
|
+
if (task && typeof task === "string") {
|
|
360
|
+
try {
|
|
361
|
+
await defaultTaskPoller.waitForTask(this.client, nodeName, task);
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
logger.debug(`Snapshot rollback task wait: ${err.message}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async deleteSnapshot(vmIdOrName, snapName) {
|
|
369
|
+
const vm = await this.getVm(vmIdOrName);
|
|
370
|
+
if (!vm) {
|
|
371
|
+
throw new Error(`VM '${vmIdOrName}' not found on Proxmox host '${this.hostName}'.`);
|
|
372
|
+
}
|
|
373
|
+
const nodeName = await this.ensureNodeName();
|
|
374
|
+
const vmid = Number(vm.id);
|
|
375
|
+
const task = await this.client.request("DELETE", `/nodes/${nodeName}/qemu/${vmid}/snapshot/${snapName}`);
|
|
376
|
+
if (task && typeof task === "string") {
|
|
377
|
+
try {
|
|
378
|
+
await defaultTaskPoller.waitForTask(this.client, nodeName, task);
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
logger.debug(`Snapshot delete task wait: ${err.message}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async getVmStorage(vmIdOrName) {
|
|
386
|
+
const vm = await this.getVm(vmIdOrName);
|
|
387
|
+
if (!vm) {
|
|
388
|
+
throw new Error(`VM '${vmIdOrName}' not found on Proxmox host '${this.hostName}'.`);
|
|
389
|
+
}
|
|
390
|
+
const nodeName = await this.ensureNodeName();
|
|
391
|
+
const vmid = Number(vm.id);
|
|
392
|
+
const config = await this.client.getVmConfig(vmid, nodeName);
|
|
393
|
+
const disks = [];
|
|
394
|
+
const cdroms = [];
|
|
395
|
+
for (const [key, val] of Object.entries(config)) {
|
|
396
|
+
if (key.startsWith("scsi") ||
|
|
397
|
+
key.startsWith("virtio") ||
|
|
398
|
+
key.startsWith("sata") ||
|
|
399
|
+
key.startsWith("ide")) {
|
|
400
|
+
if (typeof val !== "string")
|
|
401
|
+
continue;
|
|
402
|
+
const parts = val.split(",");
|
|
403
|
+
const volid = parts[0] || "";
|
|
404
|
+
const [storage] = volid.split(":");
|
|
405
|
+
const isCdrom = val.includes("media=cdrom");
|
|
406
|
+
let sizeGb;
|
|
407
|
+
let sizeBytes;
|
|
408
|
+
const sizeMatch = val.match(/size=(\d+(?:\.\d+)?)([KMGTP]?)/i);
|
|
409
|
+
if (sizeMatch) {
|
|
410
|
+
const num = parseFloat(sizeMatch[1]);
|
|
411
|
+
const unit = (sizeMatch[2] || "G").toUpperCase();
|
|
412
|
+
const multipliers = {
|
|
413
|
+
K: 1024,
|
|
414
|
+
M: 1024 * 1024,
|
|
415
|
+
G: 1024 * 1024 * 1024,
|
|
416
|
+
T: 1024 * 1024 * 1024 * 1024
|
|
417
|
+
};
|
|
418
|
+
sizeBytes = num * (multipliers[unit] || multipliers.G);
|
|
419
|
+
sizeGb = sizeBytes / (1024 * 1024 * 1024);
|
|
420
|
+
}
|
|
421
|
+
const diskInfo = {
|
|
422
|
+
drive: key,
|
|
423
|
+
storage: storage || "unknown",
|
|
424
|
+
volid,
|
|
425
|
+
sizeBytes,
|
|
426
|
+
sizeGb,
|
|
427
|
+
format: volid.endsWith(".iso") ? "iso" : "raw",
|
|
428
|
+
discard: val.includes("discard=on"),
|
|
429
|
+
ssd: val.includes("ssd=1"),
|
|
430
|
+
isCdrom,
|
|
431
|
+
media: isCdrom ? "cdrom" : "disk"
|
|
432
|
+
};
|
|
433
|
+
if (isCdrom) {
|
|
434
|
+
cdroms.push(diskInfo);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
disks.push(diskInfo);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
let guestFilesystems;
|
|
442
|
+
let guestAgentActive = false;
|
|
443
|
+
if (vm.status === "running") {
|
|
444
|
+
try {
|
|
445
|
+
const fsRes = await this.client.request("GET", `/nodes/${nodeName}/qemu/${vmid}/agent/get-fsinfo`);
|
|
446
|
+
const fsList = fsRes?.result || fsRes;
|
|
447
|
+
if (Array.isArray(fsList)) {
|
|
448
|
+
guestAgentActive = true;
|
|
449
|
+
guestFilesystems = fsList.map((fs) => {
|
|
450
|
+
const total = Number(fs["total-bytes"] || fs.totalBytes || 0);
|
|
451
|
+
const used = Number(fs["used-bytes"] || fs.usedBytes || 0);
|
|
452
|
+
const avail = total > used ? total - used : 0;
|
|
453
|
+
const pct = total > 0 ? Math.round((used / total) * 100) : 0;
|
|
454
|
+
return {
|
|
455
|
+
mountPoint: String(fs.mountpoint || fs.mountPoint || "/"),
|
|
456
|
+
fsType: String(fs.type || fs.fsType || "unknown"),
|
|
457
|
+
totalBytes: total,
|
|
458
|
+
usedBytes: used,
|
|
459
|
+
availableBytes: avail,
|
|
460
|
+
usedPercent: pct,
|
|
461
|
+
diskDevice: fs.name || fs.disk?.[0]?.dev || undefined
|
|
462
|
+
};
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// Guest agent might be offline or not responding
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
vmId: String(vmid),
|
|
472
|
+
vmName: vm.name,
|
|
473
|
+
hostName: this.hostName,
|
|
474
|
+
hypervisor: "proxmox",
|
|
475
|
+
status: vm.status,
|
|
476
|
+
disks,
|
|
477
|
+
cdroms,
|
|
478
|
+
guestFilesystems,
|
|
479
|
+
guestAgentActive
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
async mountIso(vmIdOrName, isoVolid, drive = "ide2") {
|
|
483
|
+
const vm = await this.getVm(vmIdOrName);
|
|
484
|
+
if (!vm) {
|
|
485
|
+
throw new Error(`VM '${vmIdOrName}' not found on Proxmox host '${this.hostName}'.`);
|
|
486
|
+
}
|
|
487
|
+
const nodeName = await this.ensureNodeName();
|
|
488
|
+
const vmid = Number(vm.id);
|
|
489
|
+
const body = {
|
|
490
|
+
[drive]: `${isoVolid},media=cdrom`
|
|
491
|
+
};
|
|
492
|
+
await this.client.request("PUT", `/nodes/${nodeName}/qemu/${vmid}/config`, body);
|
|
493
|
+
}
|
|
494
|
+
async unmountIso(vmIdOrName, drive = "ide2") {
|
|
495
|
+
const vm = await this.getVm(vmIdOrName);
|
|
496
|
+
if (!vm) {
|
|
497
|
+
throw new Error(`VM '${vmIdOrName}' not found on Proxmox host '${this.hostName}'.`);
|
|
498
|
+
}
|
|
499
|
+
const nodeName = await this.ensureNodeName();
|
|
500
|
+
const vmid = Number(vm.id);
|
|
501
|
+
const body = {
|
|
502
|
+
[drive]: "none,media=cdrom"
|
|
503
|
+
};
|
|
504
|
+
await this.client.request("PUT", `/nodes/${nodeName}/qemu/${vmid}/config`, body);
|
|
505
|
+
}
|
|
506
|
+
async deleteVolume(volumeId) {
|
|
507
|
+
const nodeName = await this.ensureNodeName();
|
|
508
|
+
const [storage] = volumeId.split(":");
|
|
509
|
+
if (!storage) {
|
|
510
|
+
throw new Error(`Invalid volume ID format: '${volumeId}'. Expected 'storage:path' (e.g. 'local:iso/foo.iso').`);
|
|
511
|
+
}
|
|
512
|
+
for (const protId of this.hostConfig.protectedVmids) {
|
|
513
|
+
if (volumeId.includes(`vm-${protId}-`)) {
|
|
514
|
+
throw new Error(`SAFETY_GUARDRAIL: Cannot delete volume '${volumeId}' because it belongs to protected VM ${protId}.`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
await this.client.request("DELETE", `/nodes/${nodeName}/storage/${storage}/content/${volumeId}`);
|
|
518
|
+
}
|
|
519
|
+
async getVmNetwork(vmIdOrName) {
|
|
520
|
+
const vmid = await this.resolveVmid(vmIdOrName);
|
|
521
|
+
const vm = await this.getVm(vmid);
|
|
522
|
+
const nodeName = await this.ensureNodeName();
|
|
523
|
+
const config = await this.client.getVmConfig(vmid, nodeName);
|
|
524
|
+
const nics = [];
|
|
525
|
+
for (const [key, value] of Object.entries(config)) {
|
|
526
|
+
if (/^net\d+$/.test(key) && typeof value === "string") {
|
|
527
|
+
const parts = value.split(",");
|
|
528
|
+
let model = "virtio";
|
|
529
|
+
let macAddress = "";
|
|
530
|
+
let bridge = "vmbr0";
|
|
531
|
+
let vlan;
|
|
532
|
+
let firewall = false;
|
|
533
|
+
let connected = true;
|
|
534
|
+
let rateLimitMbs;
|
|
535
|
+
for (const part of parts) {
|
|
536
|
+
const [k, v] = part.split("=");
|
|
537
|
+
if (["virtio", "e1000", "vmxnet3", "rtl8139"].includes(k)) {
|
|
538
|
+
model = k;
|
|
539
|
+
macAddress = v || "";
|
|
540
|
+
}
|
|
541
|
+
else if (k === "bridge") {
|
|
542
|
+
bridge = v || "vmbr0";
|
|
543
|
+
}
|
|
544
|
+
else if (k === "tag") {
|
|
545
|
+
vlan = Number(v);
|
|
546
|
+
}
|
|
547
|
+
else if (k === "firewall") {
|
|
548
|
+
firewall = v === "1";
|
|
549
|
+
}
|
|
550
|
+
else if (k === "link_down") {
|
|
551
|
+
connected = v !== "1";
|
|
552
|
+
}
|
|
553
|
+
else if (k === "rate") {
|
|
554
|
+
rateLimitMbs = Number(v);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
nics.push({
|
|
558
|
+
nic: key,
|
|
559
|
+
model,
|
|
560
|
+
macAddress,
|
|
561
|
+
bridge,
|
|
562
|
+
vlan,
|
|
563
|
+
firewall,
|
|
564
|
+
connected,
|
|
565
|
+
rateLimitMbs
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
let guestInterfaces;
|
|
570
|
+
let guestAgentActive = false;
|
|
571
|
+
try {
|
|
572
|
+
const res = await this.client.request("GET", `/nodes/${nodeName}/qemu/${vmid}/agent/network-get-interfaces`);
|
|
573
|
+
if (res && Array.isArray(res.result)) {
|
|
574
|
+
guestAgentActive = true;
|
|
575
|
+
guestInterfaces = res.result.map((iface) => ({
|
|
576
|
+
name: iface.name,
|
|
577
|
+
macAddress: iface["hardware-address"],
|
|
578
|
+
ipAddresses: (iface["ip-addresses"] || []).map((addr) => ({
|
|
579
|
+
ip: addr["ip-address"],
|
|
580
|
+
type: addr["ip-address-type"] === "ipv6" ? "ipv6" : "ipv4",
|
|
581
|
+
prefix: addr.prefix
|
|
582
|
+
}))
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// Guest agent offline or not responding
|
|
588
|
+
}
|
|
589
|
+
let primaryMac = nics[0]?.macAddress;
|
|
590
|
+
let primaryIp;
|
|
591
|
+
if (guestInterfaces && guestInterfaces.length > 0) {
|
|
592
|
+
if (primaryMac) {
|
|
593
|
+
const match = guestInterfaces.find((gi) => gi.macAddress?.toLowerCase() === primaryMac?.toLowerCase());
|
|
594
|
+
if (match) {
|
|
595
|
+
const v4 = match.ipAddresses.find((a) => a.type === "ipv4" && !a.ip.startsWith("127."));
|
|
596
|
+
if (v4)
|
|
597
|
+
primaryIp = v4.ip;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (!primaryIp) {
|
|
601
|
+
for (const gi of guestInterfaces) {
|
|
602
|
+
if (gi.name.startsWith("docker") || gi.name.startsWith("br-") || gi.name.startsWith("veth") || gi.name === "lo") {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const v4 = gi.ipAddresses.find((a) => a.type === "ipv4" && !a.ip.startsWith("127."));
|
|
606
|
+
if (v4) {
|
|
607
|
+
primaryIp = v4.ip;
|
|
608
|
+
if (!primaryMac && gi.macAddress)
|
|
609
|
+
primaryMac = gi.macAddress;
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (!primaryIp && vm?.ipAddresses && vm.ipAddresses.length > 0) {
|
|
616
|
+
primaryIp = vm.ipAddresses[0];
|
|
617
|
+
}
|
|
618
|
+
const sshCommand = primaryIp ? `ssh -p 2222 calltelemetry@${primaryIp}` : undefined;
|
|
619
|
+
return {
|
|
620
|
+
vmId: String(vmid),
|
|
621
|
+
vmName: vm?.name || `VM-${vmid}`,
|
|
622
|
+
hostName: this.hostName,
|
|
623
|
+
hypervisor: "proxmox",
|
|
624
|
+
primaryIp,
|
|
625
|
+
primaryMac,
|
|
626
|
+
sshCommand,
|
|
627
|
+
nics,
|
|
628
|
+
guestInterfaces,
|
|
629
|
+
guestAgentActive
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async updateVmNic(vmIdOrName, nic, options) {
|
|
633
|
+
const vmid = await this.resolveVmid(vmIdOrName);
|
|
634
|
+
const nodeName = await this.ensureNodeName();
|
|
635
|
+
const config = await this.client.getVmConfig(vmid, nodeName);
|
|
636
|
+
const currentNicStr = config[nic];
|
|
637
|
+
if (!currentNicStr || typeof currentNicStr !== "string") {
|
|
638
|
+
throw new Error(`NIC '${nic}' does not exist on VM ${vmid}.`);
|
|
639
|
+
}
|
|
640
|
+
// Parse existing model and MAC (first token, e.g. virtio=BC:24:11:00:00:01)
|
|
641
|
+
const parts = currentNicStr.split(",");
|
|
642
|
+
const modelAndMac = parts[0];
|
|
643
|
+
let bridge = "vmbr0";
|
|
644
|
+
let vlan;
|
|
645
|
+
let firewall = false;
|
|
646
|
+
let linkDown = false;
|
|
647
|
+
let rate;
|
|
648
|
+
for (let i = 1; i < parts.length; i++) {
|
|
649
|
+
const [k, v] = parts[i].split("=");
|
|
650
|
+
if (k === "bridge")
|
|
651
|
+
bridge = v;
|
|
652
|
+
else if (k === "tag")
|
|
653
|
+
vlan = Number(v);
|
|
654
|
+
else if (k === "firewall")
|
|
655
|
+
firewall = v === "1";
|
|
656
|
+
else if (k === "link_down")
|
|
657
|
+
linkDown = v === "1";
|
|
658
|
+
else if (k === "rate")
|
|
659
|
+
rate = Number(v);
|
|
660
|
+
}
|
|
661
|
+
if (options.bridge !== undefined)
|
|
662
|
+
bridge = options.bridge;
|
|
663
|
+
if (options.vlan !== undefined)
|
|
664
|
+
vlan = options.vlan;
|
|
665
|
+
if (options.firewall !== undefined)
|
|
666
|
+
firewall = options.firewall;
|
|
667
|
+
if (options.connected !== undefined)
|
|
668
|
+
linkDown = !options.connected;
|
|
669
|
+
if (options.rateLimitMbs !== undefined)
|
|
670
|
+
rate = options.rateLimitMbs;
|
|
671
|
+
const newParts = [modelAndMac, `bridge=${bridge}`];
|
|
672
|
+
if (firewall)
|
|
673
|
+
newParts.push("firewall=1");
|
|
674
|
+
if (vlan !== undefined && vlan > 0)
|
|
675
|
+
newParts.push(`tag=${vlan}`);
|
|
676
|
+
if (linkDown)
|
|
677
|
+
newParts.push("link_down=1");
|
|
678
|
+
if (rate !== undefined && rate > 0)
|
|
679
|
+
newParts.push(`rate=${rate}`);
|
|
680
|
+
const updatedNicStr = newParts.join(",");
|
|
681
|
+
await this.client.request("PUT", `/nodes/${nodeName}/qemu/${vmid}/config`, { [nic]: updatedNicStr });
|
|
682
|
+
}
|
|
683
|
+
async getGuestAgentStatus(vmIdOrName) {
|
|
684
|
+
const vmid = await this.resolveVmid(vmIdOrName);
|
|
685
|
+
const vm = await this.getVm(vmid);
|
|
686
|
+
const nodeName = await this.ensureNodeName();
|
|
687
|
+
let isAlive = false;
|
|
688
|
+
let pingLatencyMs;
|
|
689
|
+
let agentVersion;
|
|
690
|
+
let osName;
|
|
691
|
+
let osVersion;
|
|
692
|
+
let kernelVersion;
|
|
693
|
+
let hostname;
|
|
694
|
+
let architecture;
|
|
695
|
+
const ipAddresses = [];
|
|
696
|
+
let guestInterfaces = [];
|
|
697
|
+
let supportedCommandsCount;
|
|
698
|
+
let error;
|
|
699
|
+
const start = Date.now();
|
|
700
|
+
try {
|
|
701
|
+
// 1. Ping
|
|
702
|
+
await this.client.request("POST", `/nodes/${nodeName}/qemu/${vmid}/agent/ping`);
|
|
703
|
+
pingLatencyMs = Date.now() - start;
|
|
704
|
+
isAlive = true;
|
|
705
|
+
// 2. Agent info
|
|
706
|
+
const infoRes = await this.client.request("GET", `/nodes/${nodeName}/qemu/${vmid}/agent/info`).catch(() => null);
|
|
707
|
+
if (infoRes && infoRes.result) {
|
|
708
|
+
agentVersion = infoRes.result.version;
|
|
709
|
+
supportedCommandsCount = infoRes.result["supported_commands"]?.length;
|
|
710
|
+
}
|
|
711
|
+
// 3. OS info
|
|
712
|
+
const osRes = await this.client.request("GET", `/nodes/${nodeName}/qemu/${vmid}/agent/get-osinfo`).catch(() => null);
|
|
713
|
+
if (osRes && osRes.result) {
|
|
714
|
+
osName = osRes.result["pretty-name"] || osRes.result.name;
|
|
715
|
+
osVersion = osRes.result.version;
|
|
716
|
+
kernelVersion = osRes.result["kernel-release"];
|
|
717
|
+
hostname = osRes.result.hostname;
|
|
718
|
+
architecture = osRes.result.machine;
|
|
719
|
+
}
|
|
720
|
+
// 4. Interfaces
|
|
721
|
+
const netRes = await this.client.request("GET", `/nodes/${nodeName}/qemu/${vmid}/agent/network-get-interfaces`).catch(() => null);
|
|
722
|
+
if (netRes && Array.isArray(netRes.result)) {
|
|
723
|
+
guestInterfaces = netRes.result.map((iface) => {
|
|
724
|
+
const ips = (iface["ip-addresses"] || []).map((addr) => {
|
|
725
|
+
const ip = addr["ip-address"];
|
|
726
|
+
if (ip && !ip.startsWith("127.") && !ip.startsWith("::1") && !ip.startsWith("fe80::")) {
|
|
727
|
+
ipAddresses.push(ip);
|
|
728
|
+
}
|
|
729
|
+
return {
|
|
730
|
+
ip,
|
|
731
|
+
type: addr["ip-address-type"] === "ipv6" ? "ipv6" : "ipv4",
|
|
732
|
+
prefix: addr.prefix
|
|
733
|
+
};
|
|
734
|
+
});
|
|
735
|
+
return {
|
|
736
|
+
name: iface.name,
|
|
737
|
+
macAddress: iface["hardware-address"],
|
|
738
|
+
ipAddresses: ips
|
|
739
|
+
};
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
error = err instanceof Error ? err.message : String(err);
|
|
745
|
+
isAlive = false;
|
|
746
|
+
}
|
|
747
|
+
let primaryIp;
|
|
748
|
+
let primaryMac;
|
|
749
|
+
if (guestInterfaces && guestInterfaces.length > 0) {
|
|
750
|
+
for (const gi of guestInterfaces) {
|
|
751
|
+
if (gi.name.startsWith("docker") || gi.name.startsWith("br-") || gi.name.startsWith("veth") || gi.name === "lo") {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const v4 = gi.ipAddresses.find((a) => a.type === "ipv4" && !a.ip.startsWith("127."));
|
|
755
|
+
if (v4) {
|
|
756
|
+
primaryIp = v4.ip;
|
|
757
|
+
if (gi.macAddress)
|
|
758
|
+
primaryMac = gi.macAddress;
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (!primaryIp && vm?.ipAddresses && vm.ipAddresses.length > 0) {
|
|
764
|
+
primaryIp = vm.ipAddresses[0];
|
|
765
|
+
}
|
|
766
|
+
const sshCommand = primaryIp ? `ssh -p 2222 calltelemetry@${primaryIp}` : undefined;
|
|
767
|
+
return {
|
|
768
|
+
vmId: String(vmid),
|
|
769
|
+
vmName: vm?.name || `VM-${vmid}`,
|
|
770
|
+
hostName: this.hostName,
|
|
771
|
+
hypervisor: "proxmox",
|
|
772
|
+
primaryIp,
|
|
773
|
+
primaryMac,
|
|
774
|
+
sshCommand,
|
|
775
|
+
isAlive,
|
|
776
|
+
pingLatencyMs,
|
|
777
|
+
agentVersion,
|
|
778
|
+
osName,
|
|
779
|
+
osVersion,
|
|
780
|
+
kernelVersion,
|
|
781
|
+
hostname,
|
|
782
|
+
architecture,
|
|
783
|
+
ipAddresses: Array.from(new Set(ipAddresses)),
|
|
784
|
+
guestInterfaces,
|
|
785
|
+
supportedCommandsCount,
|
|
786
|
+
error
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
async shutdownVm(vmIdOrName, force = false) {
|
|
790
|
+
const vm = await this.getVm(vmIdOrName);
|
|
791
|
+
if (!vm) {
|
|
792
|
+
throw new Error(`VM '${vmIdOrName}' not found on Proxmox host '${this.hostName}'.`);
|
|
793
|
+
}
|
|
794
|
+
const vmid = Number(vm.id);
|
|
795
|
+
this.safetyEngine.validateAction({ vmid, name: vm.name, template: vm.isTemplate, tags: vm.tags }, "shutdown", force);
|
|
796
|
+
const nodeName = await this.ensureNodeName();
|
|
797
|
+
const res = await this.client.shutdownVm(vmid, 60, nodeName);
|
|
798
|
+
if (!res.success) {
|
|
799
|
+
throw new Error(`Failed to shutdown VM ${vmid}: ${res.error || res.exitStatus}`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async suspendVm(vmIdOrName) {
|
|
803
|
+
const vm = await this.getVm(vmIdOrName);
|
|
804
|
+
if (!vm) {
|
|
805
|
+
throw new Error(`VM '${vmIdOrName}' not found on Proxmox host '${this.hostName}'.`);
|
|
806
|
+
}
|
|
807
|
+
const vmid = Number(vm.id);
|
|
808
|
+
const nodeName = await this.ensureNodeName();
|
|
809
|
+
await this.client.request("POST", `/nodes/${nodeName}/qemu/${vmid}/status/suspend`);
|
|
810
|
+
}
|
|
811
|
+
async resumeVm(vmIdOrName) {
|
|
812
|
+
const vm = await this.getVm(vmIdOrName);
|
|
813
|
+
if (!vm) {
|
|
814
|
+
throw new Error(`VM '${vmIdOrName}' not found on Proxmox host '${this.hostName}'.`);
|
|
815
|
+
}
|
|
816
|
+
const vmid = Number(vm.id);
|
|
817
|
+
const nodeName = await this.ensureNodeName();
|
|
818
|
+
await this.client.request("POST", `/nodes/${nodeName}/qemu/${vmid}/status/resume`);
|
|
819
|
+
}
|
|
260
820
|
async resolveVmid(vmIdOrName) {
|
|
261
821
|
if (typeof vmIdOrName === "number") {
|
|
262
822
|
return vmIdOrName;
|