@calltelemetry/ct-lab-mcp 0.1.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/LICENSE +21 -0
- package/README.md +309 -0
- package/dist/bin/ct-lab-mcp.d.ts +60 -0
- package/dist/bin/ct-lab-mcp.d.ts.map +1 -0
- package/dist/bin/ct-lab-mcp.js +261 -0
- package/dist/bin/ct-lab-mcp.js.map +1 -0
- package/dist/bin/proxmox-mcp.d.ts +58 -0
- package/dist/bin/proxmox-mcp.d.ts.map +1 -0
- package/dist/bin/proxmox-mcp.js +248 -0
- package/dist/bin/proxmox-mcp.js.map +1 -0
- package/dist/src/appliance/commands.d.ts +33 -0
- package/dist/src/appliance/commands.d.ts.map +1 -0
- package/dist/src/appliance/commands.js +241 -0
- package/dist/src/appliance/commands.js.map +1 -0
- package/dist/src/appliance/executor.d.ts +34 -0
- package/dist/src/appliance/executor.d.ts.map +1 -0
- package/dist/src/appliance/executor.js +214 -0
- package/dist/src/appliance/executor.js.map +1 -0
- package/dist/src/appliance/index.d.ts +8 -0
- package/dist/src/appliance/index.d.ts.map +1 -0
- package/dist/src/appliance/index.js +8 -0
- package/dist/src/appliance/index.js.map +1 -0
- package/dist/src/appliance/mock.d.ts +66 -0
- package/dist/src/appliance/mock.d.ts.map +1 -0
- package/dist/src/appliance/mock.js +339 -0
- package/dist/src/appliance/mock.js.map +1 -0
- package/dist/src/appliance/types.d.ts +83 -0
- package/dist/src/appliance/types.d.ts.map +1 -0
- package/dist/src/appliance/types.js +58 -0
- package/dist/src/appliance/types.js.map +1 -0
- package/dist/src/credentials/doppler.d.ts +25 -0
- package/dist/src/credentials/doppler.d.ts.map +1 -0
- package/dist/src/credentials/doppler.js +68 -0
- package/dist/src/credentials/doppler.js.map +1 -0
- package/dist/src/credentials/index.d.ts +4 -0
- package/dist/src/credentials/index.d.ts.map +1 -0
- package/dist/src/credentials/index.js +4 -0
- package/dist/src/credentials/index.js.map +1 -0
- package/dist/src/credentials/resolver.d.ts +36 -0
- package/dist/src/credentials/resolver.d.ts.map +1 -0
- package/dist/src/credentials/resolver.js +427 -0
- package/dist/src/credentials/resolver.js.map +1 -0
- package/dist/src/credentials/types.d.ts +73 -0
- package/dist/src/credentials/types.d.ts.map +1 -0
- package/dist/src/credentials/types.js +5 -0
- package/dist/src/credentials/types.js.map +1 -0
- package/dist/src/guardrails/index.d.ts +3 -0
- package/dist/src/guardrails/index.d.ts.map +1 -0
- package/dist/src/guardrails/index.js +3 -0
- package/dist/src/guardrails/index.js.map +1 -0
- package/dist/src/guardrails/safety.d.ts +45 -0
- package/dist/src/guardrails/safety.d.ts.map +1 -0
- package/dist/src/guardrails/safety.js +217 -0
- package/dist/src/guardrails/safety.js.map +1 -0
- package/dist/src/guardrails/types.d.ts +36 -0
- package/dist/src/guardrails/types.d.ts.map +1 -0
- package/dist/src/guardrails/types.js +20 -0
- package/dist/src/guardrails/types.js.map +1 -0
- package/dist/src/hypervisors/esxi/client.d.ts +103 -0
- package/dist/src/hypervisors/esxi/client.d.ts.map +1 -0
- package/dist/src/hypervisors/esxi/client.js +395 -0
- package/dist/src/hypervisors/esxi/client.js.map +1 -0
- package/dist/src/hypervisors/esxi/mock.d.ts +45 -0
- package/dist/src/hypervisors/esxi/mock.d.ts.map +1 -0
- package/dist/src/hypervisors/esxi/mock.js +202 -0
- package/dist/src/hypervisors/esxi/mock.js.map +1 -0
- package/dist/src/hypervisors/index.d.ts +10 -0
- package/dist/src/hypervisors/index.d.ts.map +1 -0
- package/dist/src/hypervisors/index.js +10 -0
- package/dist/src/hypervisors/index.js.map +1 -0
- package/dist/src/hypervisors/manager.d.ts +78 -0
- package/dist/src/hypervisors/manager.d.ts.map +1 -0
- package/dist/src/hypervisors/manager.js +415 -0
- package/dist/src/hypervisors/manager.js.map +1 -0
- package/dist/src/hypervisors/proxmox/client.d.ts +29 -0
- package/dist/src/hypervisors/proxmox/client.d.ts.map +1 -0
- package/dist/src/hypervisors/proxmox/client.js +278 -0
- package/dist/src/hypervisors/proxmox/client.js.map +1 -0
- package/dist/src/hypervisors/proxmox/mock.d.ts +17 -0
- package/dist/src/hypervisors/proxmox/mock.d.ts.map +1 -0
- package/dist/src/hypervisors/proxmox/mock.js +26 -0
- package/dist/src/hypervisors/proxmox/mock.js.map +1 -0
- package/dist/src/hypervisors/types.d.ts +159 -0
- package/dist/src/hypervisors/types.d.ts.map +1 -0
- package/dist/src/hypervisors/types.js +6 -0
- package/dist/src/hypervisors/types.js.map +1 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +19 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/proxmox/client.d.ts +37 -0
- package/dist/src/proxmox/client.d.ts.map +1 -0
- package/dist/src/proxmox/client.js +282 -0
- package/dist/src/proxmox/client.js.map +1 -0
- package/dist/src/proxmox/index.d.ts +53 -0
- package/dist/src/proxmox/index.d.ts.map +1 -0
- package/dist/src/proxmox/index.js +109 -0
- package/dist/src/proxmox/index.js.map +1 -0
- package/dist/src/proxmox/mock.d.ts +40 -0
- package/dist/src/proxmox/mock.d.ts.map +1 -0
- package/dist/src/proxmox/mock.js +362 -0
- package/dist/src/proxmox/mock.js.map +1 -0
- package/dist/src/proxmox/poller.d.ts +29 -0
- package/dist/src/proxmox/poller.d.ts.map +1 -0
- package/dist/src/proxmox/poller.js +107 -0
- package/dist/src/proxmox/poller.js.map +1 -0
- package/dist/src/server/index.d.ts +2 -0
- package/dist/src/server/index.d.ts.map +1 -0
- package/dist/src/server/index.js +2 -0
- package/dist/src/server/index.js.map +1 -0
- package/dist/src/server/mcp-server.d.ts +5 -0
- package/dist/src/server/mcp-server.d.ts.map +1 -0
- package/dist/src/server/mcp-server.js +5 -0
- package/dist/src/server/mcp-server.js.map +1 -0
- package/dist/src/server.d.ts +35 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +95 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/slot/gc.d.ts +42 -0
- package/dist/src/slot/gc.d.ts.map +1 -0
- package/dist/src/slot/gc.js +79 -0
- package/dist/src/slot/gc.js.map +1 -0
- package/dist/src/slot/index.d.ts +4 -0
- package/dist/src/slot/index.d.ts.map +1 -0
- package/dist/src/slot/index.js +4 -0
- package/dist/src/slot/index.js.map +1 -0
- package/dist/src/slot/manager.d.ts +92 -0
- package/dist/src/slot/manager.d.ts.map +1 -0
- package/dist/src/slot/manager.js +650 -0
- package/dist/src/slot/manager.js.map +1 -0
- package/dist/src/slot/types.d.ts +184 -0
- package/dist/src/slot/types.d.ts.map +1 -0
- package/dist/src/slot/types.js +67 -0
- package/dist/src/slot/types.js.map +1 -0
- package/dist/src/tools/appliance-exec.d.ts +81 -0
- package/dist/src/tools/appliance-exec.d.ts.map +1 -0
- package/dist/src/tools/appliance-exec.js +134 -0
- package/dist/src/tools/appliance-exec.js.map +1 -0
- package/dist/src/tools/claim-slot.d.ts +5 -0
- package/dist/src/tools/claim-slot.d.ts.map +1 -0
- package/dist/src/tools/claim-slot.js +5 -0
- package/dist/src/tools/claim-slot.js.map +1 -0
- package/dist/src/tools/index.d.ts +26 -0
- package/dist/src/tools/index.d.ts.map +1 -0
- package/dist/src/tools/index.js +63 -0
- package/dist/src/tools/index.js.map +1 -0
- package/dist/src/tools/list-hosts.d.ts +41 -0
- package/dist/src/tools/list-hosts.d.ts.map +1 -0
- package/dist/src/tools/list-hosts.js +91 -0
- package/dist/src/tools/list-hosts.js.map +1 -0
- package/dist/src/tools/list-inventory.d.ts +50 -0
- package/dist/src/tools/list-inventory.d.ts.map +1 -0
- package/dist/src/tools/list-inventory.js +112 -0
- package/dist/src/tools/list-inventory.js.map +1 -0
- package/dist/src/tools/list-slots.d.ts +5 -0
- package/dist/src/tools/list-slots.d.ts.map +1 -0
- package/dist/src/tools/list-slots.js +5 -0
- package/dist/src/tools/list-slots.js.map +1 -0
- package/dist/src/tools/provision-tools.d.ts +231 -0
- package/dist/src/tools/provision-tools.d.ts.map +1 -0
- package/dist/src/tools/provision-tools.js +289 -0
- package/dist/src/tools/provision-tools.js.map +1 -0
- package/dist/src/tools/release-slot.d.ts +5 -0
- package/dist/src/tools/release-slot.d.ts.map +1 -0
- package/dist/src/tools/release-slot.js +5 -0
- package/dist/src/tools/release-slot.js.map +1 -0
- package/dist/src/tools/slot-tools.d.ts +173 -0
- package/dist/src/tools/slot-tools.d.ts.map +1 -0
- package/dist/src/tools/slot-tools.js +296 -0
- package/dist/src/tools/slot-tools.js.map +1 -0
- package/dist/src/tools/vm-action.d.ts +189 -0
- package/dist/src/tools/vm-action.d.ts.map +1 -0
- package/dist/src/tools/vm-action.js +300 -0
- package/dist/src/tools/vm-action.js.map +1 -0
- package/dist/src/transports/index.d.ts +3 -0
- package/dist/src/transports/index.d.ts.map +1 -0
- package/dist/src/transports/index.js +3 -0
- package/dist/src/transports/index.js.map +1 -0
- package/dist/src/transports/sse.d.ts +38 -0
- package/dist/src/transports/sse.d.ts.map +1 -0
- package/dist/src/transports/sse.js +167 -0
- package/dist/src/transports/sse.js.map +1 -0
- package/dist/src/transports/stdio.d.ts +15 -0
- package/dist/src/transports/stdio.d.ts.map +1 -0
- package/dist/src/transports/stdio.js +34 -0
- package/dist/src/transports/stdio.js.map +1 -0
- package/dist/src/types/config.d.ts +17 -0
- package/dist/src/types/config.d.ts.map +1 -0
- package/dist/src/types/config.js +5 -0
- package/dist/src/types/config.js.map +1 -0
- package/dist/src/types/index.d.ts +4 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +4 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/mcp.d.ts +38 -0
- package/dist/src/types/mcp.d.ts.map +1 -0
- package/dist/src/types/mcp.js +46 -0
- package/dist/src/types/mcp.js.map +1 -0
- package/dist/src/types/proxmox.d.ts +181 -0
- package/dist/src/types/proxmox.d.ts.map +1 -0
- package/dist/src/types/proxmox.js +5 -0
- package/dist/src/types/proxmox.js.map +1 -0
- package/dist/src/types/slot.d.ts +5 -0
- package/dist/src/types/slot.d.ts.map +1 -0
- package/dist/src/types/slot.js +5 -0
- package/dist/src/types/slot.js.map +1 -0
- package/dist/src/utils/formatters.d.ts +71 -0
- package/dist/src/utils/formatters.d.ts.map +1 -0
- package/dist/src/utils/formatters.js +480 -0
- package/dist/src/utils/formatters.js.map +1 -0
- package/dist/src/utils/index.d.ts +6 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +6 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/logger.d.ts +28 -0
- package/dist/src/utils/logger.d.ts.map +1 -0
- package/dist/src/utils/logger.js +74 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/mutex.d.ts +32 -0
- package/dist/src/utils/mutex.d.ts.map +1 -0
- package/dist/src/utils/mutex.js +86 -0
- package/dist/src/utils/mutex.js.map +1 -0
- package/dist/src/utils/redact.d.ts +6 -0
- package/dist/src/utils/redact.d.ts.map +1 -0
- package/dist/src/utils/redact.js +6 -0
- package/dist/src/utils/redact.js.map +1 -0
- package/dist/src/utils/redactor.d.ts +50 -0
- package/dist/src/utils/redactor.d.ts.map +1 -0
- package/dist/src/utils/redactor.js +170 -0
- package/dist/src/utils/redactor.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Slot Allocation & Capacity Partitioning Engine
|
|
3
|
+
* Manages 8GB appliance slots across Proxmox VE and VMware ESXi hosts with hard partition limits,
|
|
4
|
+
* atomic concurrency locking (AsyncMutex), TTL lease management, and candidate slot ranking.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { CapacityExceededError, GlobalCapacityExceededError, SlotNotFoundError, LeaseCeilingExceededError, InvalidSlotOperationError, DatastoreCapacityExceededError } from "./types.js";
|
|
8
|
+
import { AsyncMutex } from "../utils/mutex.js";
|
|
9
|
+
export * from "./types.js";
|
|
10
|
+
const DEFAULT_HOST_CONFIGS = [
|
|
11
|
+
{
|
|
12
|
+
hostId: "proxmox-lab",
|
|
13
|
+
name: "Proxmox Lab (Primary)",
|
|
14
|
+
hypervisorType: "proxmox",
|
|
15
|
+
totalMemoryMb: 32768, // 32 GB
|
|
16
|
+
reservedHeadroomMb: 16384, // 16 GB reserved for CUCM and system
|
|
17
|
+
maxSlots: 2, // Hard limit: 2 slots (16 GB)
|
|
18
|
+
slotSizeMb: 8192, // 8 GB per slot
|
|
19
|
+
freeDiskGb: 280,
|
|
20
|
+
totalDiskGb: 500
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
hostId: "proxmox-mini",
|
|
24
|
+
name: "Proxmox Mini (Minisforum)",
|
|
25
|
+
hypervisorType: "proxmox",
|
|
26
|
+
totalMemoryMb: 65536, // 64 GB
|
|
27
|
+
reservedHeadroomMb: 32768, // 32 GB reserved
|
|
28
|
+
maxSlots: 4, // Hard limit: 4 slots (32 GB)
|
|
29
|
+
slotSizeMb: 8192, // 8 GB per slot
|
|
30
|
+
freeDiskGb: 650,
|
|
31
|
+
totalDiskGb: 1000
|
|
32
|
+
}
|
|
33
|
+
];
|
|
34
|
+
export const DEFAULT_ESXI_HOST_CONFIG = {
|
|
35
|
+
hostId: "esxi-intel-192-168-123-176",
|
|
36
|
+
name: "ESXi Intel Lab (192.168.123.176)",
|
|
37
|
+
hypervisorType: "esxi",
|
|
38
|
+
totalMemoryMb: 131072, // 128 GB
|
|
39
|
+
reservedHeadroomMb: 65536, // 64 GB reserved
|
|
40
|
+
maxSlots: 8, // 8 slots (64 GB)
|
|
41
|
+
slotSizeMb: 8192,
|
|
42
|
+
freeDiskGb: 1200,
|
|
43
|
+
totalDiskGb: 1860,
|
|
44
|
+
datastoreFreeBytes: 1200 * 1024 * 1024 * 1024
|
|
45
|
+
};
|
|
46
|
+
export class SlotManager {
|
|
47
|
+
hosts = new Map();
|
|
48
|
+
slots = new Map();
|
|
49
|
+
mutex = new AsyncMutex();
|
|
50
|
+
defaultDurationMinutes;
|
|
51
|
+
proxmoxManager;
|
|
52
|
+
constructor(options = {}) {
|
|
53
|
+
this.defaultDurationMinutes = options.defaultDurationMinutes ?? 60;
|
|
54
|
+
this.proxmoxManager = options.proxmoxManager;
|
|
55
|
+
const hostConfigs = options.hosts && options.hosts.length > 0
|
|
56
|
+
? options.hosts
|
|
57
|
+
: [...DEFAULT_HOST_CONFIGS];
|
|
58
|
+
if (options.includeEsxi && !hostConfigs.some((h) => h.hostId === "esxi-intel-192-168-123-176")) {
|
|
59
|
+
hostConfigs.push(DEFAULT_ESXI_HOST_CONFIG);
|
|
60
|
+
}
|
|
61
|
+
for (const host of hostConfigs) {
|
|
62
|
+
this.registerHost(host);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Register a host and initialize its slot definitions.
|
|
67
|
+
*/
|
|
68
|
+
registerHost(host) {
|
|
69
|
+
const normalizedId = this.normalizeHostId(host.hostId);
|
|
70
|
+
const normalizedHost = {
|
|
71
|
+
...host,
|
|
72
|
+
hostId: normalizedId,
|
|
73
|
+
hypervisorType: host.hypervisorType || (normalizedId.startsWith("esxi") ? "esxi" : "proxmox")
|
|
74
|
+
};
|
|
75
|
+
this.hosts.set(normalizedId, normalizedHost);
|
|
76
|
+
for (let index = 1; index <= normalizedHost.maxSlots; index++) {
|
|
77
|
+
const slotId = `slot-${normalizedId}-${index}`;
|
|
78
|
+
if (!this.slots.has(slotId)) {
|
|
79
|
+
this.slots.set(slotId, {
|
|
80
|
+
slotId,
|
|
81
|
+
hostId: normalizedId,
|
|
82
|
+
hostName: normalizedHost.name,
|
|
83
|
+
hypervisorType: normalizedHost.hypervisorType,
|
|
84
|
+
slotIndex: index,
|
|
85
|
+
memoryMb: normalizedHost.slotSizeMb,
|
|
86
|
+
allocatedMemoryMb: normalizedHost.slotSizeMb,
|
|
87
|
+
allocatedMemoryBytes: normalizedHost.slotSizeMb * 1024 * 1024,
|
|
88
|
+
status: "AVAILABLE",
|
|
89
|
+
claimId: null,
|
|
90
|
+
claimedAt: null,
|
|
91
|
+
expiresAt: null,
|
|
92
|
+
durationMinutes: null,
|
|
93
|
+
ttlMinutes: null,
|
|
94
|
+
ttlSeconds: null,
|
|
95
|
+
tag: null,
|
|
96
|
+
requester: null,
|
|
97
|
+
linearIssue: null,
|
|
98
|
+
sessionId: null,
|
|
99
|
+
purpose: null,
|
|
100
|
+
vmId: null,
|
|
101
|
+
vmName: null,
|
|
102
|
+
metadata: {}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Normalizes host ID strings (e.g. mapping aliases like proxmox-minis-lab -> proxmox-mini).
|
|
109
|
+
*/
|
|
110
|
+
normalizeHostId(hostId) {
|
|
111
|
+
if (hostId === "proxmox-minis-lab")
|
|
112
|
+
return "proxmox-mini";
|
|
113
|
+
return hostId;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Internal lazy expiration sweep (must be called inside mutex or synchronous method).
|
|
117
|
+
*/
|
|
118
|
+
sweepExpiredSlotsInternal(now = Date.now()) {
|
|
119
|
+
let expiredCount = 0;
|
|
120
|
+
for (const slot of this.slots.values()) {
|
|
121
|
+
if ((slot.status === "CLAIMED" || slot.status === "active") && slot.expiresAt) {
|
|
122
|
+
const expiresTime = new Date(slot.expiresAt).getTime();
|
|
123
|
+
if (expiresTime <= now) {
|
|
124
|
+
slot.status = "AVAILABLE";
|
|
125
|
+
slot.claimId = null;
|
|
126
|
+
slot.claimedAt = null;
|
|
127
|
+
slot.expiresAt = null;
|
|
128
|
+
slot.durationMinutes = null;
|
|
129
|
+
slot.ttlMinutes = null;
|
|
130
|
+
slot.ttlSeconds = null;
|
|
131
|
+
slot.tag = null;
|
|
132
|
+
slot.requester = null;
|
|
133
|
+
slot.linearIssue = null;
|
|
134
|
+
slot.sessionId = null;
|
|
135
|
+
slot.purpose = null;
|
|
136
|
+
slot.vmId = null;
|
|
137
|
+
slot.vmName = null;
|
|
138
|
+
slot.metadata = {};
|
|
139
|
+
expiredCount++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return expiredCount;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Atomically claims an 8GB appliance slot.
|
|
147
|
+
*/
|
|
148
|
+
async claimSlot(request) {
|
|
149
|
+
return this.mutex.runExclusive(async () => {
|
|
150
|
+
// 1. Reclaim any expired leases first
|
|
151
|
+
this.sweepExpiredSlotsInternal();
|
|
152
|
+
const tagInput = request.tag || request.linearIssue || request.requester;
|
|
153
|
+
if (!tagInput || typeof tagInput !== "string" || tagInput.trim() === "") {
|
|
154
|
+
throw new InvalidSlotOperationError("A valid non-empty 'tag', 'linearIssue', or 'requester' is required to claim a slot.");
|
|
155
|
+
}
|
|
156
|
+
const durationMinutes = request.durationMinutes ?? request.ttlMinutes ?? (request.ttlSeconds ? Math.ceil(request.ttlSeconds / 60) : this.defaultDurationMinutes);
|
|
157
|
+
if (durationMinutes < 1 || durationMinutes > 1440) {
|
|
158
|
+
throw new InvalidSlotOperationError(`Claim duration must be between 1 and 1440 minutes (requested: ${durationMinutes}).`);
|
|
159
|
+
}
|
|
160
|
+
const rawRequestedHost = request.host || request.targetHost;
|
|
161
|
+
const requestedHost = rawRequestedHost ? this.normalizeHostId(rawRequestedHost) : undefined;
|
|
162
|
+
const hypervisorTypePref = request.hypervisorType || "any";
|
|
163
|
+
// If requestedHost is ESXi and not yet registered, register default ESXi config
|
|
164
|
+
if (requestedHost === "esxi-intel-192-168-123-176" && !this.hosts.has("esxi-intel-192-168-123-176")) {
|
|
165
|
+
this.registerHost(DEFAULT_ESXI_HOST_CONFIG);
|
|
166
|
+
}
|
|
167
|
+
let targetSlot;
|
|
168
|
+
if (requestedHost && requestedHost !== "auto") {
|
|
169
|
+
// Specific host claim
|
|
170
|
+
const hostConfig = this.hosts.get(requestedHost);
|
|
171
|
+
if (!hostConfig) {
|
|
172
|
+
const validHosts = Array.from(this.hosts.keys()).join(", ");
|
|
173
|
+
throw new InvalidSlotOperationError(`Unknown host '${requestedHost}' for slot allocation. Available: [${validHosts}]`);
|
|
174
|
+
}
|
|
175
|
+
// ESXi Datastore check (>= 10GB free required)
|
|
176
|
+
if (hostConfig.hypervisorType === "esxi") {
|
|
177
|
+
const freeDisk = hostConfig.freeDiskGb ?? 100;
|
|
178
|
+
if (freeDisk < 10) {
|
|
179
|
+
throw new DatastoreCapacityExceededError(requestedHost, 10, freeDisk);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const hostSlots = Array.from(this.slots.values())
|
|
183
|
+
.filter((s) => s.hostId === requestedHost)
|
|
184
|
+
.sort((a, b) => a.slotIndex - b.slotIndex);
|
|
185
|
+
const availableSlot = hostSlots.find((s) => s.status === "AVAILABLE");
|
|
186
|
+
if (!availableSlot) {
|
|
187
|
+
const allocatedCount = hostSlots.filter((s) => s.status === "CLAIMED" || s.status === "active").length;
|
|
188
|
+
if (requestedHost === "proxmox-lab") {
|
|
189
|
+
throw new CapacityExceededError(`Host 'proxmox-lab' has reached its hard quota limit of 2 concurrent 8GB slots (16GB allocated). 16GB is strictly reserved for CUCM and core lab infrastructure.`, "proxmox-lab", hostConfig.maxSlots, allocatedCount);
|
|
190
|
+
}
|
|
191
|
+
else if (requestedHost === "proxmox-mini") {
|
|
192
|
+
throw new CapacityExceededError(`Host 'proxmox-mini' has reached its hard quota limit of 4 concurrent 8GB slots (32GB allocated).`, "proxmox-mini", hostConfig.maxSlots, allocatedCount);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
throw new CapacityExceededError(`Host '${requestedHost}' has reached its hard quota limit of ${hostConfig.maxSlots} concurrent slots.`, requestedHost, hostConfig.maxSlots, allocatedCount);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
targetSlot = availableSlot;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Deterministic Auto-Placement
|
|
202
|
+
// If hypervisorType is specified as esxi, auto-register default ESXi if not present
|
|
203
|
+
if (hypervisorTypePref === "esxi" && !this.hosts.has("esxi-intel-192-168-123-176")) {
|
|
204
|
+
this.registerHost(DEFAULT_ESXI_HOST_CONFIG);
|
|
205
|
+
}
|
|
206
|
+
// Filter candidate hosts based on hypervisor preference
|
|
207
|
+
const candidateHostConfigs = Array.from(this.hosts.values()).filter((h) => {
|
|
208
|
+
if (hypervisorTypePref === "proxmox" && h.hypervisorType !== "proxmox")
|
|
209
|
+
return false;
|
|
210
|
+
if (hypervisorTypePref === "esxi" && h.hypervisorType !== "esxi")
|
|
211
|
+
return false;
|
|
212
|
+
return true;
|
|
213
|
+
});
|
|
214
|
+
// Evaluate available slots and datastore health per host
|
|
215
|
+
const hostStats = candidateHostConfigs.map((h) => {
|
|
216
|
+
const hostSlots = Array.from(this.slots.values()).filter((s) => s.hostId === h.hostId);
|
|
217
|
+
const availableSlots = hostSlots.filter((s) => s.status === "AVAILABLE").length;
|
|
218
|
+
const freeDiskGb = h.freeDiskGb ?? 100;
|
|
219
|
+
const datastoreOk = h.hypervisorType !== "esxi" || freeDiskGb >= 10;
|
|
220
|
+
return { host: h, availableSlots, hostSlots, datastoreOk };
|
|
221
|
+
});
|
|
222
|
+
const totalAvailable = hostStats.reduce((sum, h) => sum + (h.datastoreOk ? h.availableSlots : 0), 0);
|
|
223
|
+
if (totalAvailable <= 0) {
|
|
224
|
+
if (hypervisorTypePref === "proxmox") {
|
|
225
|
+
throw new CapacityExceededError("All Proxmox cluster slots are currently allocated (2/2 on proxmox-lab, 4/4 on proxmox-mini).", "proxmox-cluster", 6, 6);
|
|
226
|
+
}
|
|
227
|
+
throw new GlobalCapacityExceededError("All 6 cluster slots are currently allocated (2/2 on proxmox-lab, 4/4 on proxmox-mini).", 6, 6);
|
|
228
|
+
}
|
|
229
|
+
// Sort candidates:
|
|
230
|
+
// 1. Only datastoreOk hosts with availableSlots > 0
|
|
231
|
+
// 2. Descending by availableSlots (least loaded first)
|
|
232
|
+
// 3. Tie-Breaker: prefer proxmox-mini if tied with proxmox-lab
|
|
233
|
+
const validCandidates = hostStats.filter((h) => h.datastoreOk && h.availableSlots > 0);
|
|
234
|
+
validCandidates.sort((a, b) => {
|
|
235
|
+
if (b.availableSlots !== a.availableSlots) {
|
|
236
|
+
return b.availableSlots - a.availableSlots;
|
|
237
|
+
}
|
|
238
|
+
// Tie breaker: prefer proxmox-mini
|
|
239
|
+
if (a.host.hostId === "proxmox-mini" && b.host.hostId !== "proxmox-mini")
|
|
240
|
+
return -1;
|
|
241
|
+
if (b.host.hostId === "proxmox-mini" && a.host.hostId !== "proxmox-mini")
|
|
242
|
+
return 1;
|
|
243
|
+
return 0;
|
|
244
|
+
});
|
|
245
|
+
const winningHostStat = validCandidates[0];
|
|
246
|
+
if (!winningHostStat) {
|
|
247
|
+
throw new GlobalCapacityExceededError("All 6 cluster slots are currently allocated (2/2 on proxmox-lab, 4/4 on proxmox-mini).", 6, 6);
|
|
248
|
+
}
|
|
249
|
+
const availableSlotsOnWinner = winningHostStat.hostSlots
|
|
250
|
+
.filter((s) => s.status === "AVAILABLE")
|
|
251
|
+
.sort((a, b) => a.slotIndex - b.slotIndex);
|
|
252
|
+
targetSlot = availableSlotsOnWinner[0];
|
|
253
|
+
}
|
|
254
|
+
if (!targetSlot) {
|
|
255
|
+
throw new GlobalCapacityExceededError("All 6 cluster slots are currently allocated (2/2 on proxmox-lab, 4/4 on proxmox-mini).", 6, 6);
|
|
256
|
+
}
|
|
257
|
+
// Assign claim details
|
|
258
|
+
const claimId = `claim-${randomUUID().substring(0, 8)}`;
|
|
259
|
+
const now = new Date();
|
|
260
|
+
const claimedAt = now.toISOString();
|
|
261
|
+
const expiresAt = new Date(now.getTime() + durationMinutes * 60 * 1000).toISOString();
|
|
262
|
+
targetSlot.status = "CLAIMED";
|
|
263
|
+
targetSlot.claimId = claimId;
|
|
264
|
+
targetSlot.claimedAt = claimedAt;
|
|
265
|
+
targetSlot.expiresAt = expiresAt;
|
|
266
|
+
targetSlot.durationMinutes = durationMinutes;
|
|
267
|
+
targetSlot.ttlMinutes = durationMinutes;
|
|
268
|
+
targetSlot.ttlSeconds = durationMinutes * 60;
|
|
269
|
+
targetSlot.tag = (request.tag || tagInput).trim();
|
|
270
|
+
targetSlot.requester = request.requester || request.tag || null;
|
|
271
|
+
targetSlot.linearIssue = request.linearIssue || (request.tag && request.tag.startsWith("CTUAT-") ? request.tag : null);
|
|
272
|
+
targetSlot.sessionId = request.sessionId || null;
|
|
273
|
+
targetSlot.purpose = request.purpose || null;
|
|
274
|
+
targetSlot.metadata = request.metadata || {};
|
|
275
|
+
return {
|
|
276
|
+
claimId,
|
|
277
|
+
slotId: targetSlot.slotId,
|
|
278
|
+
hostId: targetSlot.hostId,
|
|
279
|
+
hostName: targetSlot.hostName || targetSlot.hostId,
|
|
280
|
+
hypervisorType: targetSlot.hypervisorType || (targetSlot.hostId.startsWith("esxi") ? "esxi" : "proxmox"),
|
|
281
|
+
slotIndex: targetSlot.slotIndex,
|
|
282
|
+
memoryMb: targetSlot.memoryMb,
|
|
283
|
+
allocatedMemoryMb: targetSlot.memoryMb,
|
|
284
|
+
allocatedMemoryBytes: targetSlot.memoryMb * 1024 * 1024,
|
|
285
|
+
tag: targetSlot.tag,
|
|
286
|
+
requester: targetSlot.requester || undefined,
|
|
287
|
+
linearIssue: targetSlot.linearIssue || undefined,
|
|
288
|
+
claimedAt,
|
|
289
|
+
expiresAt,
|
|
290
|
+
durationMinutes,
|
|
291
|
+
ttlMinutes: durationMinutes,
|
|
292
|
+
ttlSeconds: durationMinutes * 60,
|
|
293
|
+
status: "active",
|
|
294
|
+
purpose: targetSlot.purpose || undefined,
|
|
295
|
+
sessionId: targetSlot.sessionId || undefined
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Releases one or more slots matching claimId, tag, slotId, requester, or linearIssue.
|
|
301
|
+
*/
|
|
302
|
+
async releaseSlot(optionsOrClaimId, destroyVmFlag) {
|
|
303
|
+
return this.mutex.runExclusive(async () => {
|
|
304
|
+
this.sweepExpiredSlotsInternal();
|
|
305
|
+
let claimId;
|
|
306
|
+
let tag;
|
|
307
|
+
let slotId;
|
|
308
|
+
let sessionId;
|
|
309
|
+
let requester;
|
|
310
|
+
let linearIssue;
|
|
311
|
+
let destroyVm = destroyVmFlag;
|
|
312
|
+
if (typeof optionsOrClaimId === "string") {
|
|
313
|
+
const str = optionsOrClaimId.trim();
|
|
314
|
+
if (str.startsWith("claim-") || str.includes("-")) {
|
|
315
|
+
// Check if claimId exists first
|
|
316
|
+
const existsAsClaim = Array.from(this.slots.values()).some((s) => s.claimId === str);
|
|
317
|
+
if (existsAsClaim) {
|
|
318
|
+
claimId = str;
|
|
319
|
+
}
|
|
320
|
+
else if (str.startsWith("CTUAT-")) {
|
|
321
|
+
linearIssue = str;
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
claimId = str;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
tag = str;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else if (optionsOrClaimId && typeof optionsOrClaimId === "object") {
|
|
332
|
+
claimId = optionsOrClaimId.claimId;
|
|
333
|
+
tag = optionsOrClaimId.tag;
|
|
334
|
+
slotId = optionsOrClaimId.slotId;
|
|
335
|
+
sessionId = optionsOrClaimId.sessionId;
|
|
336
|
+
requester = optionsOrClaimId.requester;
|
|
337
|
+
linearIssue = optionsOrClaimId.linearIssue;
|
|
338
|
+
if (optionsOrClaimId.destroyVm !== undefined) {
|
|
339
|
+
destroyVm = optionsOrClaimId.destroyVm;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (!claimId && !tag && !slotId && !requester && !linearIssue) {
|
|
343
|
+
throw new InvalidSlotOperationError("Must specify claimId, tag, slotId, requester, or linearIssue to release a slot.");
|
|
344
|
+
}
|
|
345
|
+
const matchingSlots = [];
|
|
346
|
+
for (const slot of this.slots.values()) {
|
|
347
|
+
if (slot.status !== "CLAIMED" && slot.status !== "active")
|
|
348
|
+
continue;
|
|
349
|
+
let match = false;
|
|
350
|
+
if (claimId && slot.claimId === claimId)
|
|
351
|
+
match = true;
|
|
352
|
+
if (slotId && slot.slotId === slotId)
|
|
353
|
+
match = true;
|
|
354
|
+
if (tag && slot.tag === tag)
|
|
355
|
+
match = true;
|
|
356
|
+
if (requester && (slot.requester === requester || slot.tag === requester))
|
|
357
|
+
match = true;
|
|
358
|
+
if (linearIssue && (slot.linearIssue === linearIssue || slot.tag === linearIssue))
|
|
359
|
+
match = true;
|
|
360
|
+
if (match) {
|
|
361
|
+
if (sessionId && slot.sessionId && slot.sessionId !== sessionId) {
|
|
362
|
+
throw new InvalidSlotOperationError(`Unauthorized: session '${sessionId}' does not own slot '${slot.slotId}' (owned by '${slot.sessionId}').`);
|
|
363
|
+
}
|
|
364
|
+
matchingSlots.push(slot);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (matchingSlots.length === 0 && claimId) {
|
|
368
|
+
throw new SlotNotFoundError(`No active claimed slot found matching claimId '${claimId}'.`);
|
|
369
|
+
}
|
|
370
|
+
const releasedSnapshots = [];
|
|
371
|
+
const releasedClaims = [];
|
|
372
|
+
const destroyedVms = [];
|
|
373
|
+
let freedMemoryMb = 0;
|
|
374
|
+
for (const slot of matchingSlots) {
|
|
375
|
+
releasedSnapshots.push({ ...slot, status: "RELEASED" });
|
|
376
|
+
if (slot.claimId) {
|
|
377
|
+
releasedClaims.push(slot.claimId);
|
|
378
|
+
}
|
|
379
|
+
freedMemoryMb += slot.memoryMb;
|
|
380
|
+
if (destroyVm && slot.vmId !== null && slot.vmId !== undefined) {
|
|
381
|
+
destroyedVms.push(slot.vmId);
|
|
382
|
+
}
|
|
383
|
+
slot.status = "AVAILABLE";
|
|
384
|
+
slot.claimId = null;
|
|
385
|
+
slot.claimedAt = null;
|
|
386
|
+
slot.expiresAt = null;
|
|
387
|
+
slot.durationMinutes = null;
|
|
388
|
+
slot.ttlMinutes = null;
|
|
389
|
+
slot.ttlSeconds = null;
|
|
390
|
+
slot.tag = null;
|
|
391
|
+
slot.requester = null;
|
|
392
|
+
slot.linearIssue = null;
|
|
393
|
+
slot.sessionId = null;
|
|
394
|
+
slot.purpose = null;
|
|
395
|
+
slot.vmId = null;
|
|
396
|
+
slot.vmName = null;
|
|
397
|
+
slot.metadata = {};
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
releasedCount: releasedSnapshots.length,
|
|
401
|
+
releasedSlots: releasedSnapshots,
|
|
402
|
+
releasedClaims,
|
|
403
|
+
destroyedVms,
|
|
404
|
+
freedMemoryMb,
|
|
405
|
+
message: `Successfully released ${releasedSnapshots.length} slot(s).`
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Extends the lease of an active slot up to the 24h cumulative ceiling.
|
|
411
|
+
*/
|
|
412
|
+
async extendLease(claimId, additionalMinutes, sessionId) {
|
|
413
|
+
return this.mutex.runExclusive(async () => {
|
|
414
|
+
this.sweepExpiredSlotsInternal();
|
|
415
|
+
if (!claimId) {
|
|
416
|
+
throw new InvalidSlotOperationError("A valid 'claimId' is required to extend a lease.");
|
|
417
|
+
}
|
|
418
|
+
if (!additionalMinutes || additionalMinutes <= 0) {
|
|
419
|
+
throw new InvalidSlotOperationError(`additionalMinutes must be greater than 0 (requested: ${additionalMinutes}).`);
|
|
420
|
+
}
|
|
421
|
+
const slot = Array.from(this.slots.values()).find((s) => s.claimId === claimId && (s.status === "CLAIMED" || s.status === "active"));
|
|
422
|
+
if (!slot || !slot.claimedAt || !slot.expiresAt) {
|
|
423
|
+
throw new SlotNotFoundError(`Active slot with claimId '${claimId}' not found.`);
|
|
424
|
+
}
|
|
425
|
+
if (sessionId && slot.sessionId && slot.sessionId !== sessionId) {
|
|
426
|
+
throw new InvalidSlotOperationError(`Unauthorized: session '${sessionId}' does not match slot owner '${slot.sessionId}'.`);
|
|
427
|
+
}
|
|
428
|
+
const initialTime = new Date(slot.claimedAt).getTime();
|
|
429
|
+
const currentExpiresTime = new Date(slot.expiresAt).getTime();
|
|
430
|
+
const newExpiresTime = currentExpiresTime + additionalMinutes * 60 * 1000;
|
|
431
|
+
const totalDurationMinutes = Math.round((newExpiresTime - initialTime) / (60 * 1000));
|
|
432
|
+
if (totalDurationMinutes > 1440) {
|
|
433
|
+
throw new LeaseCeilingExceededError(`Lease extension of ${additionalMinutes}m would exceed maximum 24h (1440m) ceiling. Total requested: ${totalDurationMinutes}m.`, 1440);
|
|
434
|
+
}
|
|
435
|
+
const previousExpiresAt = slot.expiresAt;
|
|
436
|
+
const newExpiresAt = new Date(newExpiresTime).toISOString();
|
|
437
|
+
slot.expiresAt = newExpiresAt;
|
|
438
|
+
slot.durationMinutes = totalDurationMinutes;
|
|
439
|
+
slot.ttlMinutes = totalDurationMinutes;
|
|
440
|
+
slot.ttlSeconds = totalDurationMinutes * 60;
|
|
441
|
+
return {
|
|
442
|
+
claimId,
|
|
443
|
+
slotId: slot.slotId,
|
|
444
|
+
hostId: slot.hostId,
|
|
445
|
+
slotIndex: slot.slotIndex,
|
|
446
|
+
previousExpiresAt,
|
|
447
|
+
newExpiresAt,
|
|
448
|
+
extendedByMinutes: additionalMinutes,
|
|
449
|
+
totalDurationMinutes
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* List slots with optional filtering and computed remaining TTL.
|
|
455
|
+
*/
|
|
456
|
+
async listSlots(options = {}) {
|
|
457
|
+
return this.mutex.runExclusive(async () => {
|
|
458
|
+
this.sweepExpiredSlotsInternal();
|
|
459
|
+
const now = Date.now();
|
|
460
|
+
const results = [];
|
|
461
|
+
const filterHost = options.host || options.targetHost;
|
|
462
|
+
const normalizedFilterHost = filterHost ? this.normalizeHostId(filterHost) : undefined;
|
|
463
|
+
for (const slot of this.slots.values()) {
|
|
464
|
+
if (normalizedFilterHost && slot.hostId !== normalizedFilterHost)
|
|
465
|
+
continue;
|
|
466
|
+
if (options.tag && slot.tag !== options.tag)
|
|
467
|
+
continue;
|
|
468
|
+
if (options.requester && slot.requester !== options.requester && slot.tag !== options.requester)
|
|
469
|
+
continue;
|
|
470
|
+
if (options.linearIssue && slot.linearIssue !== options.linearIssue && slot.tag !== options.linearIssue)
|
|
471
|
+
continue;
|
|
472
|
+
if (options.activeOnly && slot.status !== "CLAIMED" && slot.status !== "active")
|
|
473
|
+
continue;
|
|
474
|
+
if (options.hypervisorType &&
|
|
475
|
+
options.hypervisorType !== "any" &&
|
|
476
|
+
options.hypervisorType !== "all" &&
|
|
477
|
+
slot.hypervisorType !== options.hypervisorType) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
let remainingTtlSeconds = null;
|
|
481
|
+
if ((slot.status === "CLAIMED" || slot.status === "active") && slot.expiresAt) {
|
|
482
|
+
const diffMs = new Date(slot.expiresAt).getTime() - now;
|
|
483
|
+
remainingTtlSeconds = Math.max(0, Math.floor(diffMs / 1000));
|
|
484
|
+
}
|
|
485
|
+
results.push({
|
|
486
|
+
...slot,
|
|
487
|
+
remainingTtlSeconds
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
// Sort by hostId then slotIndex
|
|
491
|
+
return results.sort((a, b) => {
|
|
492
|
+
if (a.hostId !== b.hostId)
|
|
493
|
+
return a.hostId.localeCompare(b.hostId);
|
|
494
|
+
return a.slotIndex - b.slotIndex;
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* List host capacities or global cluster summary.
|
|
500
|
+
*/
|
|
501
|
+
async listHosts(hostId) {
|
|
502
|
+
return this.mutex.runExclusive(async () => {
|
|
503
|
+
this.sweepExpiredSlotsInternal();
|
|
504
|
+
const normalizedHostId = hostId && hostId !== "all" ? this.normalizeHostId(hostId) : hostId;
|
|
505
|
+
// If specific ESXi host requested and not registered, auto-register
|
|
506
|
+
if (normalizedHostId === "esxi-intel-192-168-123-176" && !this.hosts.has("esxi-intel-192-168-123-176")) {
|
|
507
|
+
this.registerHost(DEFAULT_ESXI_HOST_CONFIG);
|
|
508
|
+
}
|
|
509
|
+
const now = Date.now();
|
|
510
|
+
const hostsSummary = {};
|
|
511
|
+
let clusterMaxSlots = 0;
|
|
512
|
+
let clusterAllocatedSlots = 0;
|
|
513
|
+
let clusterAvailableSlots = 0;
|
|
514
|
+
for (const [id, config] of this.hosts.entries()) {
|
|
515
|
+
const hostSlots = Array.from(this.slots.values())
|
|
516
|
+
.filter((s) => s.hostId === id)
|
|
517
|
+
.map((s) => {
|
|
518
|
+
let remainingTtlSeconds = null;
|
|
519
|
+
if ((s.status === "CLAIMED" || s.status === "active") && s.expiresAt) {
|
|
520
|
+
const diffMs = new Date(s.expiresAt).getTime() - now;
|
|
521
|
+
remainingTtlSeconds = Math.max(0, Math.floor(diffMs / 1000));
|
|
522
|
+
}
|
|
523
|
+
return { ...s, remainingTtlSeconds };
|
|
524
|
+
})
|
|
525
|
+
.sort((a, b) => a.slotIndex - b.slotIndex);
|
|
526
|
+
const activeSlots = hostSlots.filter((s) => s.status === "CLAIMED" || s.status === "active");
|
|
527
|
+
const allocatedSlots = activeSlots.length;
|
|
528
|
+
const availableSlots = config.maxSlots - allocatedSlots;
|
|
529
|
+
clusterMaxSlots += config.maxSlots;
|
|
530
|
+
clusterAllocatedSlots += allocatedSlots;
|
|
531
|
+
clusterAvailableSlots += availableSlots;
|
|
532
|
+
hostsSummary[id] = {
|
|
533
|
+
hostId: id,
|
|
534
|
+
name: config.name,
|
|
535
|
+
hypervisorType: config.hypervisorType,
|
|
536
|
+
totalMemoryMb: config.totalMemoryMb,
|
|
537
|
+
reservedHeadroomMb: config.reservedHeadroomMb,
|
|
538
|
+
maxSlots: config.maxSlots,
|
|
539
|
+
allocatedSlots,
|
|
540
|
+
availableSlots,
|
|
541
|
+
slotSizeMb: config.slotSizeMb,
|
|
542
|
+
freeDiskGb: config.freeDiskGb,
|
|
543
|
+
totalDiskGb: config.totalDiskGb,
|
|
544
|
+
datastoreFreeBytes: config.datastoreFreeBytes,
|
|
545
|
+
activeSlots
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (normalizedHostId && normalizedHostId !== "all") {
|
|
549
|
+
const single = hostsSummary[normalizedHostId];
|
|
550
|
+
if (!single) {
|
|
551
|
+
throw new InvalidSlotOperationError(`Host '${normalizedHostId}' not found.`);
|
|
552
|
+
}
|
|
553
|
+
return single;
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
clusterMaxSlots,
|
|
557
|
+
clusterAllocatedSlots,
|
|
558
|
+
clusterAvailableSlots,
|
|
559
|
+
hosts: hostsSummary
|
|
560
|
+
};
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Returns available slot capacity report conforming to ISlotManager.getAvailableSlots.
|
|
565
|
+
*/
|
|
566
|
+
async getAvailableSlots(hostName) {
|
|
567
|
+
const summary = await this.listHosts(hostName);
|
|
568
|
+
if ("allocatedSlots" in summary) {
|
|
569
|
+
return summary;
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
maxSlots: summary.clusterMaxSlots,
|
|
573
|
+
usedSlots: summary.clusterAllocatedSlots,
|
|
574
|
+
allocatedSlots: summary.clusterAllocatedSlots,
|
|
575
|
+
availableSlots: summary.clusterAvailableSlots,
|
|
576
|
+
slotSizeBytes: 8192 * 1024 * 1024,
|
|
577
|
+
slotSizeMb: 8192,
|
|
578
|
+
hosts: summary.hosts
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Reclaim all expired slots and return count.
|
|
583
|
+
*/
|
|
584
|
+
async reclaimExpired() {
|
|
585
|
+
return this.mutex.runExclusive(async () => {
|
|
586
|
+
return this.sweepExpiredSlotsInternal();
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Attach a created VMID or VM name to a claimed slot.
|
|
591
|
+
*/
|
|
592
|
+
async attachVm(claimId, vmId, vmName) {
|
|
593
|
+
return this.mutex.runExclusive(async () => {
|
|
594
|
+
this.sweepExpiredSlotsInternal();
|
|
595
|
+
const slot = Array.from(this.slots.values()).find((s) => s.claimId === claimId && (s.status === "CLAIMED" || s.status === "active"));
|
|
596
|
+
if (!slot) {
|
|
597
|
+
throw new SlotNotFoundError(`Active slot with claimId '${claimId}' not found.`);
|
|
598
|
+
}
|
|
599
|
+
slot.vmId = vmId;
|
|
600
|
+
if (vmName)
|
|
601
|
+
slot.vmName = vmName;
|
|
602
|
+
return { ...slot };
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Lookup single slot by slotId.
|
|
607
|
+
*/
|
|
608
|
+
async getSlot(slotId) {
|
|
609
|
+
return this.mutex.runExclusive(async () => {
|
|
610
|
+
this.sweepExpiredSlotsInternal();
|
|
611
|
+
const slot = this.slots.get(slotId);
|
|
612
|
+
return slot ? { ...slot } : null;
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Lookup slot by active claimId.
|
|
617
|
+
*/
|
|
618
|
+
async getSlotByClaimId(claimId) {
|
|
619
|
+
return this.mutex.runExclusive(async () => {
|
|
620
|
+
this.sweepExpiredSlotsInternal();
|
|
621
|
+
const slot = Array.from(this.slots.values()).find((s) => s.claimId === claimId && (s.status === "CLAIMED" || s.status === "active"));
|
|
622
|
+
return slot ? { ...slot } : null;
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Clears all claims (primarily for test resets).
|
|
627
|
+
*/
|
|
628
|
+
async clearAll() {
|
|
629
|
+
await this.mutex.runExclusive(async () => {
|
|
630
|
+
for (const slot of this.slots.values()) {
|
|
631
|
+
slot.status = "AVAILABLE";
|
|
632
|
+
slot.claimId = null;
|
|
633
|
+
slot.claimedAt = null;
|
|
634
|
+
slot.expiresAt = null;
|
|
635
|
+
slot.durationMinutes = null;
|
|
636
|
+
slot.ttlMinutes = null;
|
|
637
|
+
slot.ttlSeconds = null;
|
|
638
|
+
slot.tag = null;
|
|
639
|
+
slot.requester = null;
|
|
640
|
+
slot.linearIssue = null;
|
|
641
|
+
slot.sessionId = null;
|
|
642
|
+
slot.purpose = null;
|
|
643
|
+
slot.vmId = null;
|
|
644
|
+
slot.vmName = null;
|
|
645
|
+
slot.metadata = {};
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
//# sourceMappingURL=manager.js.map
|