@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.
Files changed (139) hide show
  1. package/dist/bin/ct-lab-mcp.d.ts.map +1 -1
  2. package/dist/bin/ct-lab-mcp.js +21 -7
  3. package/dist/bin/ct-lab-mcp.js.map +1 -1
  4. package/dist/src/appliance/commands.d.ts.map +1 -1
  5. package/dist/src/appliance/commands.js +24 -0
  6. package/dist/src/appliance/commands.js.map +1 -1
  7. package/dist/src/appliance/executor.d.ts.map +1 -1
  8. package/dist/src/appliance/executor.js +7 -1
  9. package/dist/src/appliance/executor.js.map +1 -1
  10. package/dist/src/appliance/mock.d.ts +6 -0
  11. package/dist/src/appliance/mock.d.ts.map +1 -1
  12. package/dist/src/appliance/mock.js +27 -6
  13. package/dist/src/appliance/mock.js.map +1 -1
  14. package/dist/src/appliance/types.d.ts +20 -1
  15. package/dist/src/appliance/types.d.ts.map +1 -1
  16. package/dist/src/appliance/types.js.map +1 -1
  17. package/dist/src/credentials/errors.d.ts +16 -0
  18. package/dist/src/credentials/errors.d.ts.map +1 -0
  19. package/dist/src/credentials/errors.js +21 -0
  20. package/dist/src/credentials/errors.js.map +1 -0
  21. package/dist/src/credentials/resolver.d.ts +16 -0
  22. package/dist/src/credentials/resolver.d.ts.map +1 -1
  23. package/dist/src/credentials/resolver.js +104 -10
  24. package/dist/src/credentials/resolver.js.map +1 -1
  25. package/dist/src/credentials/types.d.ts +8 -0
  26. package/dist/src/credentials/types.d.ts.map +1 -1
  27. package/dist/src/diagnostics/collector.d.ts +37 -0
  28. package/dist/src/diagnostics/collector.d.ts.map +1 -0
  29. package/dist/src/diagnostics/collector.js +452 -0
  30. package/dist/src/diagnostics/collector.js.map +1 -0
  31. package/dist/src/diagnostics/types.d.ts +103 -0
  32. package/dist/src/diagnostics/types.d.ts.map +1 -0
  33. package/dist/src/diagnostics/types.js +8 -0
  34. package/dist/src/diagnostics/types.js.map +1 -0
  35. package/dist/src/hypervisors/esxi/client.d.ts +1 -0
  36. package/dist/src/hypervisors/esxi/client.d.ts.map +1 -1
  37. package/dist/src/hypervisors/esxi/client.js +15 -3
  38. package/dist/src/hypervisors/esxi/client.js.map +1 -1
  39. package/dist/src/hypervisors/esxi/mock.d.ts +1 -0
  40. package/dist/src/hypervisors/esxi/mock.d.ts.map +1 -1
  41. package/dist/src/hypervisors/esxi/mock.js +4 -0
  42. package/dist/src/hypervisors/esxi/mock.js.map +1 -1
  43. package/dist/src/hypervisors/manager.d.ts.map +1 -1
  44. package/dist/src/hypervisors/manager.js +37 -3
  45. package/dist/src/hypervisors/manager.js.map +1 -1
  46. package/dist/src/hypervisors/proxmox/client.d.ts +18 -1
  47. package/dist/src/hypervisors/proxmox/client.d.ts.map +1 -1
  48. package/dist/src/hypervisors/proxmox/client.js +563 -3
  49. package/dist/src/hypervisors/proxmox/client.js.map +1 -1
  50. package/dist/src/hypervisors/proxmox/mock.d.ts +1 -0
  51. package/dist/src/hypervisors/proxmox/mock.d.ts.map +1 -1
  52. package/dist/src/hypervisors/proxmox/mock.js +4 -0
  53. package/dist/src/hypervisors/proxmox/mock.js.map +1 -1
  54. package/dist/src/hypervisors/types.d.ts +159 -1
  55. package/dist/src/hypervisors/types.d.ts.map +1 -1
  56. package/dist/src/proxmox/mock.d.ts +2 -1
  57. package/dist/src/proxmox/mock.d.ts.map +1 -1
  58. package/dist/src/proxmox/mock.js +202 -0
  59. package/dist/src/proxmox/mock.js.map +1 -1
  60. package/dist/src/server.d.ts.map +1 -1
  61. package/dist/src/server.js +2 -0
  62. package/dist/src/server.js.map +1 -1
  63. package/dist/src/slot/manager.d.ts +26 -1
  64. package/dist/src/slot/manager.d.ts.map +1 -1
  65. package/dist/src/slot/manager.js +278 -1
  66. package/dist/src/slot/manager.js.map +1 -1
  67. package/dist/src/slot/types.d.ts +49 -0
  68. package/dist/src/slot/types.d.ts.map +1 -1
  69. package/dist/src/slot/types.js +18 -0
  70. package/dist/src/slot/types.js.map +1 -1
  71. package/dist/src/tools/credential-status.d.ts +29 -0
  72. package/dist/src/tools/credential-status.d.ts.map +1 -0
  73. package/dist/src/tools/credential-status.js +81 -0
  74. package/dist/src/tools/credential-status.js.map +1 -0
  75. package/dist/src/tools/cube-provision.d.ts +288 -0
  76. package/dist/src/tools/cube-provision.d.ts.map +1 -0
  77. package/dist/src/tools/cube-provision.js +892 -0
  78. package/dist/src/tools/cube-provision.js.map +1 -0
  79. package/dist/src/tools/cube-tools.d.ts +137 -0
  80. package/dist/src/tools/cube-tools.d.ts.map +1 -0
  81. package/dist/src/tools/cube-tools.js +292 -0
  82. package/dist/src/tools/cube-tools.js.map +1 -0
  83. package/dist/src/tools/cucm-emulator-tool.d.ts +85 -0
  84. package/dist/src/tools/cucm-emulator-tool.d.ts.map +1 -0
  85. package/dist/src/tools/cucm-emulator-tool.js +191 -0
  86. package/dist/src/tools/cucm-emulator-tool.js.map +1 -0
  87. package/dist/src/tools/cucm-trunk.d.ts +165 -0
  88. package/dist/src/tools/cucm-trunk.d.ts.map +1 -0
  89. package/dist/src/tools/cucm-trunk.js +354 -0
  90. package/dist/src/tools/cucm-trunk.js.map +1 -0
  91. package/dist/src/tools/diagnostics.d.ts +58 -0
  92. package/dist/src/tools/diagnostics.d.ts.map +1 -0
  93. package/dist/src/tools/diagnostics.js +212 -0
  94. package/dist/src/tools/diagnostics.js.map +1 -0
  95. package/dist/src/tools/guest-agent.d.ts +34 -0
  96. package/dist/src/tools/guest-agent.d.ts.map +1 -0
  97. package/dist/src/tools/guest-agent.js +133 -0
  98. package/dist/src/tools/guest-agent.js.map +1 -0
  99. package/dist/src/tools/index.d.ts +11 -0
  100. package/dist/src/tools/index.d.ts.map +1 -1
  101. package/dist/src/tools/index.js +111 -1
  102. package/dist/src/tools/index.js.map +1 -1
  103. package/dist/src/tools/list-hosts.d.ts +9 -0
  104. package/dist/src/tools/list-hosts.d.ts.map +1 -1
  105. package/dist/src/tools/list-hosts.js.map +1 -1
  106. package/dist/src/tools/network-tools.d.ts +65 -0
  107. package/dist/src/tools/network-tools.d.ts.map +1 -0
  108. package/dist/src/tools/network-tools.js +200 -0
  109. package/dist/src/tools/network-tools.js.map +1 -0
  110. package/dist/src/tools/provision-tools.d.ts.map +1 -1
  111. package/dist/src/tools/provision-tools.js +19 -0
  112. package/dist/src/tools/provision-tools.js.map +1 -1
  113. package/dist/src/tools/server-management.d.ts +51 -0
  114. package/dist/src/tools/server-management.d.ts.map +1 -0
  115. package/dist/src/tools/server-management.js +175 -0
  116. package/dist/src/tools/server-management.js.map +1 -0
  117. package/dist/src/tools/storage-tools.d.ts +250 -0
  118. package/dist/src/tools/storage-tools.d.ts.map +1 -0
  119. package/dist/src/tools/storage-tools.js +685 -0
  120. package/dist/src/tools/storage-tools.js.map +1 -0
  121. package/dist/src/tools/update-cli.d.ts +62 -0
  122. package/dist/src/tools/update-cli.d.ts.map +1 -0
  123. package/dist/src/tools/update-cli.js +142 -0
  124. package/dist/src/tools/update-cli.js.map +1 -0
  125. package/dist/src/tools/vm-action.d.ts +6 -6
  126. package/dist/src/tools/vm-action.d.ts.map +1 -1
  127. package/dist/src/tools/vm-action.js +3 -1
  128. package/dist/src/tools/vm-action.js.map +1 -1
  129. package/dist/src/types/mcp.d.ts +5 -1
  130. package/dist/src/types/mcp.d.ts.map +1 -1
  131. package/dist/src/types/mcp.js +14 -0
  132. package/dist/src/types/mcp.js.map +1 -1
  133. package/dist/src/types/proxmox.d.ts +1 -0
  134. package/dist/src/types/proxmox.d.ts.map +1 -1
  135. package/dist/src/utils/formatters.d.ts +7 -0
  136. package/dist/src/utils/formatters.d.ts.map +1 -1
  137. package/dist/src/utils/formatters.js +48 -5
  138. package/dist/src/utils/formatters.js.map +1 -1
  139. 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;