@calltelemetry/ct-lab-mcp 0.8.64 → 0.8.66
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/README.md +8 -7
- package/dist/src/crd/reconciler.d.ts.map +1 -1
- package/dist/src/crd/reconciler.js +4 -1
- package/dist/src/crd/reconciler.js.map +1 -1
- package/dist/src/credentials/resolver.d.ts.map +1 -1
- package/dist/src/credentials/resolver.js +14 -16
- package/dist/src/credentials/resolver.js.map +1 -1
- package/dist/src/credentials/types.d.ts +2 -1
- package/dist/src/credentials/types.d.ts.map +1 -1
- package/dist/src/hypervisors/clone-size.d.ts +5 -0
- package/dist/src/hypervisors/clone-size.d.ts.map +1 -0
- package/dist/src/hypervisors/clone-size.js +27 -0
- package/dist/src/hypervisors/clone-size.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 +33 -6
- package/dist/src/hypervisors/esxi/client.js.map +1 -1
- package/dist/src/hypervisors/esxi/mock.d.ts.map +1 -1
- package/dist/src/hypervisors/esxi/mock.js +2 -0
- package/dist/src/hypervisors/esxi/mock.js.map +1 -1
- package/dist/src/hypervisors/manager.d.ts +6 -1
- package/dist/src/hypervisors/manager.d.ts.map +1 -1
- package/dist/src/hypervisors/manager.js +30 -9
- package/dist/src/hypervisors/manager.js.map +1 -1
- package/dist/src/hypervisors/proxmox/client.d.ts.map +1 -1
- package/dist/src/hypervisors/proxmox/client.js +29 -28
- package/dist/src/hypervisors/proxmox/client.js.map +1 -1
- package/dist/src/hypervisors/types.d.ts +3 -0
- package/dist/src/hypervisors/types.d.ts.map +1 -1
- package/dist/src/proxmox/mock.d.ts.map +1 -1
- package/dist/src/proxmox/mock.js +8 -1
- package/dist/src/proxmox/mock.js.map +1 -1
- package/dist/src/server.js +3 -3
- package/dist/src/server.js.map +1 -1
- package/dist/src/slot/manager.d.ts +22 -3
- package/dist/src/slot/manager.d.ts.map +1 -1
- package/dist/src/slot/manager.js +275 -258
- package/dist/src/slot/manager.js.map +1 -1
- package/dist/src/slot/types.d.ts +17 -0
- package/dist/src/slot/types.d.ts.map +1 -1
- package/dist/src/slot/types.js +1 -1
- package/dist/src/slot/types.js.map +1 -1
- package/dist/src/tools/claimed-import-qcow.d.ts +5 -4
- package/dist/src/tools/claimed-import-qcow.d.ts.map +1 -1
- package/dist/src/tools/claimed-import-qcow.js +29 -9
- package/dist/src/tools/claimed-import-qcow.js.map +1 -1
- package/dist/src/tools/cube-provision.d.ts.map +1 -1
- package/dist/src/tools/cube-provision.js +47 -47
- package/dist/src/tools/cube-provision.js.map +1 -1
- package/dist/src/tools/find-slot.d.ts +12 -0
- package/dist/src/tools/find-slot.d.ts.map +1 -1
- package/dist/src/tools/find-slot.js +6 -0
- package/dist/src/tools/find-slot.js.map +1 -1
- package/dist/src/tools/host-migrate-vm.d.ts +1 -1
- package/dist/src/tools/host-migrate-vm.js +1 -1
- package/dist/src/tools/import-qcow.d.ts +6 -6
- package/dist/src/tools/index.js +2 -2
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/legacy-clone-plan.d.ts +27 -0
- package/dist/src/tools/legacy-clone-plan.d.ts.map +1 -0
- package/dist/src/tools/legacy-clone-plan.js +62 -0
- package/dist/src/tools/legacy-clone-plan.js.map +1 -0
- package/dist/src/tools/list-hosts.d.ts +1 -1
- package/dist/src/tools/list-hosts.js +1 -1
- package/dist/src/tools/provision-tools.d.ts +8 -8
- package/dist/src/tools/provision-tools.d.ts.map +1 -1
- package/dist/src/tools/provision-tools.js +47 -82
- package/dist/src/tools/provision-tools.js.map +1 -1
- package/dist/src/tools/quick-provision.d.ts +2 -2
- package/dist/src/tools/sandbox-tools.d.ts +6 -6
- package/dist/src/tools/sandbox-tools.d.ts.map +1 -1
- package/dist/src/tools/sandbox-tools.js +3 -3
- package/dist/src/tools/sandbox-tools.js.map +1 -1
- package/dist/src/tools/slot-tools.d.ts +20 -8
- package/dist/src/tools/slot-tools.d.ts.map +1 -1
- package/dist/src/tools/slot-tools.js +4 -0
- package/dist/src/tools/slot-tools.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.map +1 -1
- package/dist/src/utils/formatters.js +19 -20
- package/dist/src/utils/formatters.js.map +1 -1
- package/package.json +1 -1
package/dist/src/slot/manager.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Manages
|
|
2
|
+
* Resource-based Slot Allocation Engine
|
|
3
|
+
* Manages identity-bound appliance leases using fresh physical resource admission,
|
|
4
4
|
* atomic concurrency locking (AsyncMutex), TTL lease management, candidate slot ranking,
|
|
5
5
|
* structured metadata labeling/indexing, and automated expired slot purge & reclamation.
|
|
6
6
|
*/
|
|
7
7
|
import { createHash, randomInt, randomUUID } from "node:crypto";
|
|
8
8
|
import { CapacityExceededError, GlobalCapacityExceededError, SlotNotFoundError, LeaseCeilingExceededError, InvalidSlotOperationError, DatastoreCapacityExceededError, HypervisorSlotUnsupportedError, CubeNotFoundError, CubeAlreadyClaimedError } from "./types.js";
|
|
9
|
+
import { ProxmoxDriver } from "../hypervisors/proxmox/client.js";
|
|
9
10
|
import { AsyncMutex } from "../utils/mutex.js";
|
|
10
11
|
import { defaultSafetyGuardrailEngine } from "../guardrails/safety.js";
|
|
11
12
|
import { telemetry } from "../telemetry/index.js";
|
|
@@ -16,17 +17,10 @@ export * from "./types.js";
|
|
|
16
17
|
* Capacity is derived directly from unreserved physical RAM: (total - reserved) / slotSize.
|
|
17
18
|
*/
|
|
18
19
|
export function calculatePureCapacity(host) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
else {
|
|
24
|
-
return host.maxSlots;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
const slotSize = host.slotSizeMb || 8192;
|
|
28
|
-
if (host.totalMemoryMb !== undefined) {
|
|
29
|
-
const reserved = host.reservedHeadroomMb || 0;
|
|
20
|
+
const slotSize = host.slotSizeMb ?? 8192;
|
|
21
|
+
const reserved = host.reservedHeadroomMb ?? 0;
|
|
22
|
+
if (Number.isFinite(host.totalMemoryMb) && Number.isFinite(slotSize) && slotSize > 0
|
|
23
|
+
&& Number.isFinite(reserved) && reserved >= 0) {
|
|
30
24
|
const usableMb = Math.max(0, host.totalMemoryMb - reserved);
|
|
31
25
|
return Math.floor(usableMb / slotSize);
|
|
32
26
|
}
|
|
@@ -37,36 +31,26 @@ const DEFAULT_HOST_CONFIGS = [
|
|
|
37
31
|
hostId: "proxmox-lab",
|
|
38
32
|
name: "Proxmox Lab (Primary)",
|
|
39
33
|
hypervisorType: "proxmox",
|
|
40
|
-
totalMemoryMb:
|
|
41
|
-
reservedHeadroomMb:
|
|
42
|
-
maxSlots: 2, // Pure capacity: 2 slots (16 GB usable)
|
|
34
|
+
totalMemoryMb: 0, // Unknown until a fresh hypervisor read
|
|
35
|
+
reservedHeadroomMb: 0,
|
|
43
36
|
slotSizeMb: 8192, // 8 GB per slot
|
|
44
|
-
freeDiskGb: 280,
|
|
45
|
-
totalDiskGb: 500
|
|
46
37
|
},
|
|
47
38
|
{
|
|
48
39
|
hostId: "proxmox-mini",
|
|
49
40
|
name: "Proxmox Mini (Minisforum)",
|
|
50
41
|
hypervisorType: "proxmox",
|
|
51
|
-
totalMemoryMb:
|
|
52
|
-
reservedHeadroomMb:
|
|
53
|
-
maxSlots: 4, // Pure capacity: 4 slots (32 GB usable)
|
|
42
|
+
totalMemoryMb: 0,
|
|
43
|
+
reservedHeadroomMb: 0,
|
|
54
44
|
slotSizeMb: 8192, // 8 GB per slot
|
|
55
|
-
freeDiskGb: 650,
|
|
56
|
-
totalDiskGb: 1000
|
|
57
45
|
}
|
|
58
46
|
];
|
|
59
47
|
export const DEFAULT_ESXI_HOST_CONFIG = {
|
|
60
48
|
hostId: "esxi-intel-192-168-123-176",
|
|
61
49
|
name: "ESXi Intel Lab (192.168.123.176)",
|
|
62
50
|
hypervisorType: "esxi",
|
|
63
|
-
totalMemoryMb:
|
|
64
|
-
reservedHeadroomMb:
|
|
65
|
-
maxSlots: 0, // Hard limit: 0 slots (slot leasing disabled on ESXi)
|
|
51
|
+
totalMemoryMb: 0,
|
|
52
|
+
reservedHeadroomMb: 0,
|
|
66
53
|
slotSizeMb: 8192,
|
|
67
|
-
freeDiskGb: 1200,
|
|
68
|
-
totalDiskGb: 1860,
|
|
69
|
-
datastoreFreeBytes: 1200 * 1024 * 1024 * 1024
|
|
70
54
|
};
|
|
71
55
|
export const DEFAULT_CUBES = [
|
|
72
56
|
{
|
|
@@ -119,6 +103,7 @@ export class SlotManager {
|
|
|
119
103
|
guardrailEngine;
|
|
120
104
|
telemetry;
|
|
121
105
|
allowEsxiSlots;
|
|
106
|
+
capacityReader;
|
|
122
107
|
constructor(options = {}) {
|
|
123
108
|
this.defaultDurationMinutes = options.defaultDurationMinutes ?? 120;
|
|
124
109
|
this.proxmoxManager = options.proxmoxManager;
|
|
@@ -126,6 +111,7 @@ export class SlotManager {
|
|
|
126
111
|
this.guardrailEngine = options.guardrailEngine || defaultSafetyGuardrailEngine;
|
|
127
112
|
this.telemetry = options.telemetryRegistry || telemetry;
|
|
128
113
|
this.allowEsxiSlots = options.allowEsxiSlots ?? (process.env.ALLOW_ESXI_SLOTS === "true");
|
|
114
|
+
this.capacityReader = options.capacityReader;
|
|
129
115
|
const hostConfigs = options.hosts && options.hosts.length > 0
|
|
130
116
|
? options.hosts
|
|
131
117
|
: [...DEFAULT_HOST_CONFIGS];
|
|
@@ -152,60 +138,136 @@ export class SlotManager {
|
|
|
152
138
|
this.guardrailEngine = guardrailEngine;
|
|
153
139
|
}
|
|
154
140
|
/**
|
|
155
|
-
* Register
|
|
141
|
+
* Register host identity. Leases are created on demand after resource admission.
|
|
156
142
|
*/
|
|
157
143
|
registerHost(host) {
|
|
158
144
|
const normalizedId = this.normalizeHostId(host.hostId);
|
|
159
|
-
let pureSlots = calculatePureCapacity({
|
|
160
|
-
...host,
|
|
161
|
-
allowEsxiSlots: this.allowEsxiSlots
|
|
162
|
-
});
|
|
163
|
-
if (normalizedId.startsWith("esxi") && this.allowEsxiSlots && host.maxSlots === 0) {
|
|
164
|
-
const usableMb = Math.max(0, host.totalMemoryMb - (host.reservedHeadroomMb || 0));
|
|
165
|
-
pureSlots = Math.floor(usableMb / (host.slotSizeMb || 8192));
|
|
166
|
-
}
|
|
167
145
|
const normalizedHost = {
|
|
168
146
|
...host,
|
|
169
147
|
hostId: normalizedId,
|
|
170
|
-
maxSlots: pureSlots,
|
|
171
148
|
hypervisorType: host.hypervisorType || (normalizedId.startsWith("esxi") ? "esxi" : "proxmox")
|
|
172
149
|
};
|
|
173
150
|
this.hosts.set(normalizedId, normalizedHost);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
151
|
+
}
|
|
152
|
+
createAvailableSlot(host, reuse = true) {
|
|
153
|
+
const hostSlots = Array.from(this.slots.values()).filter((slot) => slot.hostId === host.hostId);
|
|
154
|
+
const existing = hostSlots.find((slot) => slot.status === "AVAILABLE");
|
|
155
|
+
if (reuse && existing)
|
|
156
|
+
return existing;
|
|
157
|
+
const index = Math.max(0, ...hostSlots.map((slot) => slot.slotIndex)) + 1;
|
|
158
|
+
const slotId = `slot-${host.hostId}-${index}`;
|
|
159
|
+
const slot = {
|
|
160
|
+
slotId,
|
|
161
|
+
hostId: host.hostId,
|
|
162
|
+
hostName: host.name,
|
|
163
|
+
hypervisorType: host.hypervisorType,
|
|
164
|
+
slotIndex: index,
|
|
165
|
+
memoryMb: host.slotSizeMb,
|
|
166
|
+
allocatedMemoryMb: host.slotSizeMb,
|
|
167
|
+
allocatedMemoryBytes: host.slotSizeMb * 1024 * 1024,
|
|
168
|
+
status: "AVAILABLE",
|
|
169
|
+
claimId: null,
|
|
170
|
+
claimedAt: null,
|
|
171
|
+
expiresAt: null,
|
|
172
|
+
durationMinutes: null,
|
|
173
|
+
ttlMinutes: null,
|
|
174
|
+
ttlSeconds: null,
|
|
175
|
+
tag: null,
|
|
176
|
+
label: null,
|
|
177
|
+
workloadType: null,
|
|
178
|
+
targetVersion: null,
|
|
179
|
+
branch: null,
|
|
180
|
+
customTags: [],
|
|
181
|
+
requester: null,
|
|
182
|
+
linearIssue: null,
|
|
183
|
+
sessionId: null,
|
|
184
|
+
purpose: null,
|
|
185
|
+
vmId: null,
|
|
186
|
+
vmName: null,
|
|
187
|
+
metadata: {}
|
|
188
|
+
};
|
|
189
|
+
this.slots.set(slotId, slot);
|
|
190
|
+
return slot;
|
|
191
|
+
}
|
|
192
|
+
/** Fresh reads are the only admission authority; registered sizes are diagnostics. */
|
|
193
|
+
async readHostResources(host, storage, omitClaimId) {
|
|
194
|
+
const observedAt = new Date().toISOString();
|
|
195
|
+
const unknown = { status: "unknown", freeMemoryMb: 0, freeDiskGb: 0,
|
|
196
|
+
totalMemoryMb: 0, totalDiskGb: 0, cpuTotalCores: 0, observedAt };
|
|
197
|
+
try {
|
|
198
|
+
let driver;
|
|
199
|
+
try {
|
|
200
|
+
driver = this.hypervisorManager?.getDriver(host.hostId)
|
|
201
|
+
?? (this.proxmoxManager && host.hypervisorType === "proxmox"
|
|
202
|
+
? new ProxmoxDriver(this.proxmoxManager.getHostConfig(host.hostId), this.proxmoxManager.getClient(host.hostId)) : undefined);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
// A trusted injected reader can supply offline telemetry without a
|
|
206
|
+
// driver. Production has no reader/config fallback and still refuses.
|
|
207
|
+
if (!this.capacityReader)
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
const reading = this.capacityReader
|
|
211
|
+
? await this.capacityReader(host.hostId)
|
|
212
|
+
: await driver?.getHostCapacity();
|
|
213
|
+
const capacity = reading ? { ...reading } : undefined;
|
|
214
|
+
if (!capacity || this.normalizeHostId(capacity.name) !== host.hostId
|
|
215
|
+
|| capacity.type !== host.hypervisorType)
|
|
216
|
+
return unknown;
|
|
217
|
+
if (storage) {
|
|
218
|
+
const pool = (await driver?.listStorage?.())?.find((pool) => pool.storage === storage
|
|
219
|
+
&& pool.active && pool.enabled && pool.content.split(",").includes("images"));
|
|
220
|
+
if (!pool)
|
|
221
|
+
return unknown;
|
|
222
|
+
capacity.storageTotalBytes = pool.totalBytes;
|
|
223
|
+
capacity.storageFreeBytes = pool.availableBytes;
|
|
224
|
+
}
|
|
225
|
+
const numbers = [capacity.memoryTotalBytes, capacity.memoryFreeBytes,
|
|
226
|
+
capacity.storageTotalBytes, capacity.storageFreeBytes, capacity.cpuTotalCores];
|
|
227
|
+
if (numbers.some((value) => !Number.isFinite(value) || value < 0)
|
|
228
|
+
|| capacity.memoryTotalBytes <= 0 || capacity.storageTotalBytes <= 0 || capacity.cpuTotalCores <= 0
|
|
229
|
+
|| capacity.memoryFreeBytes > capacity.memoryTotalBytes
|
|
230
|
+
|| capacity.storageFreeBytes > capacity.storageTotalBytes)
|
|
231
|
+
return unknown;
|
|
232
|
+
if (capacity.status !== "online")
|
|
233
|
+
return { ...unknown, status: capacity.status };
|
|
234
|
+
// Free RAM/disk already includes running, attached VMs. Pending or stopped
|
|
235
|
+
// claims reserve their requested size under the same claim mutex, so a
|
|
236
|
+
// concurrent read cannot promise the same physical resources twice.
|
|
237
|
+
const occupied = Array.from(this.slots.values()).filter((slot) => slot.hostId === host.hostId && slot.status !== "AVAILABLE" && slot.claimId !== omitClaimId);
|
|
238
|
+
let runningIds = new Set();
|
|
239
|
+
let attachedIds = new Set();
|
|
240
|
+
if (driver && occupied.some((slot) => slot.vmId != null)) {
|
|
241
|
+
try {
|
|
242
|
+
const vms = await driver.listVms();
|
|
243
|
+
attachedIds = new Set(vms.map((vm) => String(vm.id)));
|
|
244
|
+
runningIds = new Set(vms.filter((vm) => vm.status === "running").map((vm) => String(vm.id)));
|
|
245
|
+
}
|
|
246
|
+
catch { /* Unknown attachment remains conservatively reserved. */ }
|
|
207
247
|
}
|
|
248
|
+
const reservedMemoryMb = occupied.reduce((sum, slot) => sum + (slot.vmId != null && runningIds.has(String(slot.vmId)) ? 0 : slot.memoryMb), 0);
|
|
249
|
+
const reservedDiskGb = occupied.reduce((sum, slot) => sum + (slot.vmId != null && attachedIds.has(String(slot.vmId)) ? 0 : slot.allocatedDiskGb ?? 10), 0);
|
|
250
|
+
return {
|
|
251
|
+
status: "online",
|
|
252
|
+
freeMemoryMb: Math.max(0, capacity.memoryFreeBytes / (1024 * 1024) - reservedMemoryMb),
|
|
253
|
+
freeDiskGb: Math.max(0, capacity.storageFreeBytes / (1024 ** 3) - reservedDiskGb),
|
|
254
|
+
totalMemoryMb: capacity.memoryTotalBytes / (1024 * 1024),
|
|
255
|
+
totalDiskGb: capacity.storageTotalBytes / (1024 ** 3),
|
|
256
|
+
cpuTotalCores: capacity.cpuTotalCores,
|
|
257
|
+
observedAt,
|
|
258
|
+
};
|
|
208
259
|
}
|
|
260
|
+
catch {
|
|
261
|
+
return unknown;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
availableResourceUnits(resources, memoryMb, diskGb = 10, cpuCores = 1) {
|
|
265
|
+
if (resources.status !== "online" || !Number.isFinite(memoryMb) || memoryMb <= 0
|
|
266
|
+
|| !Number.isFinite(diskGb) || diskGb <= 0 || !Number.isFinite(cpuCores)
|
|
267
|
+
|| cpuCores <= 0 || cpuCores > resources.cpuTotalCores)
|
|
268
|
+
return 0;
|
|
269
|
+
const units = Math.floor(Math.min(resources.freeMemoryMb / memoryMb, resources.freeDiskGb / diskGb));
|
|
270
|
+
return Number.isSafeInteger(units) && units >= 0 ? units : 0;
|
|
209
271
|
}
|
|
210
272
|
/**
|
|
211
273
|
* Normalizes host ID strings (e.g. mapping aliases like proxmox-minis-lab -> proxmox-mini).
|
|
@@ -299,6 +361,30 @@ export class SlotManager {
|
|
|
299
361
|
async claimSlot(request) {
|
|
300
362
|
return this.mutex.runExclusive(async () => this.claimSlotInternal(request));
|
|
301
363
|
}
|
|
364
|
+
/** Recheck the exact size immediately before public claim creation/provisioning. */
|
|
365
|
+
async assertProvisionResources(hostId, request) {
|
|
366
|
+
return this.mutex.runExclusive(async () => {
|
|
367
|
+
const host = this.hosts.get(this.normalizeHostId(hostId));
|
|
368
|
+
if (!host)
|
|
369
|
+
throw new InvalidSlotOperationError(`Unknown provisioning host '${hostId}'.`);
|
|
370
|
+
const claim = request.claimId ? Array.from(this.slots.values())
|
|
371
|
+
.find((slot) => slot.claimId === request.claimId) : undefined;
|
|
372
|
+
if (request.claimId && (!claim || claim.hostId !== host.hostId
|
|
373
|
+
|| (claim.status !== "CLAIMED" && claim.status !== "active")
|
|
374
|
+
|| !claim.expiresAt || !Number.isFinite(Date.parse(claim.expiresAt))
|
|
375
|
+
|| Date.parse(claim.expiresAt) <= Date.now()
|
|
376
|
+
|| (request.linearIssue && claim.linearIssue && request.linearIssue !== claim.linearIssue))) {
|
|
377
|
+
throw new InvalidSlotOperationError("Provisioning requires the exact active, unexpired claim on the requested host and issue.");
|
|
378
|
+
}
|
|
379
|
+
if (claim && (request.memoryMb > claim.memoryMb || request.diskGb > (claim.allocatedDiskGb ?? 10))) {
|
|
380
|
+
throw new InvalidSlotOperationError("Requested guest size exceeds the existing lease reservation; acquire a size-matched claim.");
|
|
381
|
+
}
|
|
382
|
+
const resources = await this.readHostResources(host, request.storage, request.claimId);
|
|
383
|
+
if (this.availableResourceUnits(resources, request.memoryMb, request.diskGb, request.cpuCores) < 1) {
|
|
384
|
+
throw new CapacityExceededError(`Host '${host.hostId}' lacks verified resources for the requested guest size (capacity ${resources.status}).`, host.hostId);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
302
388
|
async claimSlotInternal(request) {
|
|
303
389
|
// 1. Reclaim any expired leases first
|
|
304
390
|
this.sweepExpiredSlotsInternal();
|
|
@@ -310,7 +396,7 @@ export class SlotManager {
|
|
|
310
396
|
if (durationMinutes < 1 || durationMinutes > 1440) {
|
|
311
397
|
throw new InvalidSlotOperationError(`Claim duration must be between 1 and 1440 minutes (requested: ${durationMinutes}).`);
|
|
312
398
|
}
|
|
313
|
-
const rawRequestedHost = request.host || request.
|
|
399
|
+
const rawRequestedHost = request.host && request.host !== "auto" ? request.host : request.targetHost || request.host;
|
|
314
400
|
const requestedHost = rawRequestedHost ? this.normalizeHostId(rawRequestedHost) : undefined;
|
|
315
401
|
const hypervisorTypePref = request.hypervisorType || "any";
|
|
316
402
|
const allowEsxi = request.allowEsxiSlots ?? this.allowEsxiSlots;
|
|
@@ -327,162 +413,72 @@ export class SlotManager {
|
|
|
327
413
|
throw new HypervisorSlotUnsupportedError("Slot leasing is not supported on ESXi hypervisors. Appliance slots are exclusively managed on Proxmox VE (proxmox-lab and proxmox-mini).", requestedHost, "esxi");
|
|
328
414
|
}
|
|
329
415
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
workloadType: null,
|
|
374
|
-
targetVersion: null,
|
|
375
|
-
branch: null,
|
|
376
|
-
customTags: [],
|
|
377
|
-
requester: null,
|
|
378
|
-
linearIssue: null,
|
|
379
|
-
sessionId: null,
|
|
380
|
-
purpose: null,
|
|
381
|
-
vmId: null,
|
|
382
|
-
vmName: null,
|
|
383
|
-
metadata: {}
|
|
384
|
-
};
|
|
385
|
-
this.slots.set(slotId, availableSlot);
|
|
386
|
-
hostSlots.push(availableSlot);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
if (!availableSlot && request.purgeIfFull) {
|
|
390
|
-
const purgeRes = await this.purgeSlotsInternal({
|
|
391
|
-
host: requestedHost,
|
|
392
|
-
destroyVms: true,
|
|
393
|
-
includeOrphans: true,
|
|
394
|
-
dryRun: false
|
|
395
|
-
});
|
|
396
|
-
if (purgeRes.purgedSlotsCount > 0 || purgeRes.destroyedVmsCount > 0) {
|
|
397
|
-
const recheckedSlots = Array.from(this.slots.values())
|
|
398
|
-
.filter((s) => s.hostId === requestedHost)
|
|
399
|
-
.sort((a, b) => a.slotIndex - b.slotIndex);
|
|
400
|
-
availableSlot = recheckedSlots.find((s) => s.status === "AVAILABLE");
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (!availableSlot) {
|
|
404
|
-
const allocatedCount = hostSlots.filter((s) => s.status === "CLAIMED" || s.status === "active").length;
|
|
405
|
-
const hostLimit = hostConfig.maxSlots ?? calculatePureCapacity(hostConfig);
|
|
406
|
-
if (requestedHost === "proxmox-lab") {
|
|
407
|
-
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", hostLimit, allocatedCount);
|
|
408
|
-
}
|
|
409
|
-
else if (requestedHost === "proxmox-mini") {
|
|
410
|
-
throw new CapacityExceededError(`Host 'proxmox-mini' has reached its hard quota limit of 4 concurrent 8GB slots (32GB allocated).`, "proxmox-mini", hostLimit, allocatedCount);
|
|
411
|
-
}
|
|
412
|
-
else {
|
|
413
|
-
throw new CapacityExceededError(`Host '${requestedHost}' has reached its hard quota limit of ${hostLimit} concurrent slots.`, requestedHost, hostLimit, allocatedCount);
|
|
414
|
-
}
|
|
416
|
+
const memoryMb = request.slotSizeMb ?? 8192;
|
|
417
|
+
const diskGb = request.diskGb ?? 10;
|
|
418
|
+
const cpuCores = request.cpuCores ?? 1;
|
|
419
|
+
if (!Number.isFinite(memoryMb) || memoryMb <= 0 || !Number.isFinite(diskGb) || diskGb <= 0) {
|
|
420
|
+
throw new InvalidSlotOperationError("Requested memory and disk must be positive finite sizes.");
|
|
421
|
+
}
|
|
422
|
+
const diskForHost = (hostId) => request.hostDiskGb?.[hostId] ?? diskGb;
|
|
423
|
+
if (request.hostDiskGb && Object.values(request.hostDiskGb).some((size) => !Number.isFinite(size) || size <= 0)) {
|
|
424
|
+
throw new InvalidSlotOperationError("Inherited per-host disk profiles must be positive finite sizes.");
|
|
425
|
+
}
|
|
426
|
+
const candidates = Array.from(this.hosts.values()).filter((host) => {
|
|
427
|
+
if (request.hostDiskGb && !(host.hostId in request.hostDiskGb))
|
|
428
|
+
return false;
|
|
429
|
+
if (requestedHost && requestedHost !== "auto" && host.hostId !== requestedHost)
|
|
430
|
+
return false;
|
|
431
|
+
if (host.hypervisorType === "esxi" && !allowEsxi)
|
|
432
|
+
return false;
|
|
433
|
+
if (hypervisorTypePref === "proxmox" && host.hypervisorType !== "proxmox")
|
|
434
|
+
return false;
|
|
435
|
+
if (hypervisorTypePref === "esxi" && host.hypervisorType !== "esxi")
|
|
436
|
+
return false;
|
|
437
|
+
return true;
|
|
438
|
+
});
|
|
439
|
+
if (requestedHost && requestedHost !== "auto" && !this.hosts.has(requestedHost)) {
|
|
440
|
+
throw new InvalidSlotOperationError(`Unknown host '${requestedHost}' for slot allocation.`);
|
|
441
|
+
}
|
|
442
|
+
const readCandidates = () => Promise.all(candidates.map(async (host) => ({
|
|
443
|
+
host, diskGb: diskForHost(host.hostId), resources: await this.readHostResources(host, request.storage),
|
|
444
|
+
})));
|
|
445
|
+
let readings = await readCandidates();
|
|
446
|
+
let eligible = readings.filter(({ resources, diskGb }) => this.availableResourceUnits(resources, memoryMb, diskGb, cpuCores) > 0);
|
|
447
|
+
if (eligible.length === 0 && request.purgeIfFull) {
|
|
448
|
+
// Reclaim only permitted, healthy hosts on which the requested size can
|
|
449
|
+
// physically fit. Unknown/oversized requests never trigger cleanup.
|
|
450
|
+
const reclaimableHosts = readings.filter(({ resources, diskGb }) => resources.status === "online"
|
|
451
|
+
&& resources.totalMemoryMb >= memoryMb && resources.totalDiskGb >= diskGb
|
|
452
|
+
&& Number.isSafeInteger(cpuCores) && cpuCores > 0 && resources.cpuTotalCores >= cpuCores);
|
|
453
|
+
for (const { host } of reclaimableHosts) {
|
|
454
|
+
await this.purgeSlotsInternal({ host: host.hostId, destroyVms: true, includeOrphans: true, dryRun: false });
|
|
455
|
+
readings = await readCandidates();
|
|
456
|
+
eligible = readings.filter(({ resources, diskGb }) => this.availableResourceUnits(resources, memoryMb, diskGb, cpuCores) > 0);
|
|
457
|
+
if (eligible.length > 0)
|
|
458
|
+
break;
|
|
415
459
|
}
|
|
416
|
-
targetSlot = availableSlot;
|
|
417
460
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const pureSlots = calculatePureCapacity({ ...h, allowEsxiSlots: allowEsxi });
|
|
425
|
-
if (h.hypervisorType === "esxi" && !allowEsxi)
|
|
426
|
-
return false;
|
|
427
|
-
if (pureSlots === 0 && (!allowEsxi || h.hypervisorType !== "esxi"))
|
|
428
|
-
return false;
|
|
429
|
-
if (hypervisorTypePref === "proxmox" && h.hypervisorType !== "proxmox")
|
|
430
|
-
return false;
|
|
431
|
-
if (hypervisorTypePref === "esxi" && h.hypervisorType !== "esxi")
|
|
432
|
-
return false;
|
|
433
|
-
return true;
|
|
434
|
-
});
|
|
435
|
-
// Evaluate available slots and datastore health per host
|
|
436
|
-
const hostStats = candidateHostConfigs.map((h) => {
|
|
437
|
-
const hostSlots = Array.from(this.slots.values()).filter((s) => s.hostId === h.hostId);
|
|
438
|
-
const availableSlots = hostSlots.filter((s) => s.status === "AVAILABLE").length;
|
|
439
|
-
const freeDiskGb = h.freeDiskGb ?? 100;
|
|
440
|
-
const datastoreOk = h.hypervisorType !== "esxi" || freeDiskGb >= 10;
|
|
441
|
-
return { host: h, availableSlots, hostSlots, datastoreOk };
|
|
442
|
-
});
|
|
443
|
-
let totalAvailable = hostStats.reduce((sum, h) => sum + (h.datastoreOk ? h.availableSlots : 0), 0);
|
|
444
|
-
if (totalAvailable <= 0 && request.purgeIfFull) {
|
|
445
|
-
const purgeRes = await this.purgeSlotsInternal({
|
|
446
|
-
destroyVms: true,
|
|
447
|
-
includeOrphans: true,
|
|
448
|
-
dryRun: false
|
|
449
|
-
});
|
|
450
|
-
if (purgeRes.purgedSlotsCount > 0 || purgeRes.destroyedVmsCount > 0) {
|
|
451
|
-
for (const h of hostStats) {
|
|
452
|
-
const hSlots = Array.from(this.slots.values()).filter((s) => s.hostId === h.host.hostId);
|
|
453
|
-
h.availableSlots = hSlots.filter((s) => s.status === "AVAILABLE").length;
|
|
454
|
-
h.hostSlots = hSlots;
|
|
455
|
-
}
|
|
456
|
-
totalAvailable = hostStats.reduce((sum, h) => sum + (h.datastoreOk ? h.availableSlots : 0), 0);
|
|
461
|
+
if (eligible.length === 0) {
|
|
462
|
+
const allocated = Array.from(this.slots.values()).filter((slot) => candidates.some((host) => host.hostId === slot.hostId) && slot.status !== "AVAILABLE").length;
|
|
463
|
+
if (requestedHost && requestedHost !== "auto") {
|
|
464
|
+
const resources = readings[0]?.resources;
|
|
465
|
+
if (resources?.status === "online" && resources.freeDiskGb < diskGb) {
|
|
466
|
+
throw new DatastoreCapacityExceededError(requestedHost, diskGb, resources.freeDiskGb);
|
|
457
467
|
}
|
|
468
|
+
throw new CapacityExceededError(`Host '${requestedHost}' lacks verified free resources for ${memoryMb} MB RAM and ${diskGb} GB disk (capacity ${resources?.status ?? "unknown"}).`, requestedHost, allocated, allocated);
|
|
458
469
|
}
|
|
459
|
-
|
|
460
|
-
if (hypervisorTypePref === "proxmox") {
|
|
461
|
-
throw new CapacityExceededError("All Proxmox cluster slots are currently allocated (2/2 on proxmox-lab, 4/4 on proxmox-mini).", "proxmox-cluster", 6, 6);
|
|
462
|
-
}
|
|
463
|
-
throw new GlobalCapacityExceededError("All 6 cluster slots are currently allocated (2/2 on proxmox-lab, 4/4 on proxmox-mini).", 6, 6);
|
|
464
|
-
}
|
|
465
|
-
const validCandidates = hostStats
|
|
466
|
-
.filter((h) => h.datastoreOk && h.availableSlots > 0)
|
|
467
|
-
.sort((a, b) => a.host.hostId.localeCompare(b.host.hostId));
|
|
468
|
-
const candidateIndex = request.placementSeed
|
|
469
|
-
? createHash("sha256")
|
|
470
|
-
.update(request.placementSeed)
|
|
471
|
-
.digest()
|
|
472
|
-
.readUInt32BE(0) % validCandidates.length
|
|
473
|
-
: randomInt(validCandidates.length);
|
|
474
|
-
const winningHostStat = validCandidates[candidateIndex];
|
|
475
|
-
if (!winningHostStat) {
|
|
476
|
-
throw new GlobalCapacityExceededError("All 6 cluster slots are currently allocated (2/2 on proxmox-lab, 4/4 on proxmox-mini).", 6, 6);
|
|
477
|
-
}
|
|
478
|
-
const availableSlotsOnWinner = winningHostStat.hostSlots
|
|
479
|
-
.filter((s) => s.status === "AVAILABLE")
|
|
480
|
-
.sort((a, b) => a.slotIndex - b.slotIndex);
|
|
481
|
-
targetSlot = availableSlotsOnWinner[0];
|
|
482
|
-
}
|
|
483
|
-
if (!targetSlot) {
|
|
484
|
-
throw new GlobalCapacityExceededError("All 6 cluster slots are currently allocated (2/2 on proxmox-lab, 4/4 on proxmox-mini).", 6, 6);
|
|
470
|
+
throw new GlobalCapacityExceededError(`No eligible host has verified free resources for ${memoryMb} MB RAM and ${diskGb} GB disk.`, allocated, allocated);
|
|
485
471
|
}
|
|
472
|
+
eligible.sort((a, b) => a.host.hostId.localeCompare(b.host.hostId));
|
|
473
|
+
const preferredIndex = request.preferredHost ? eligible.findIndex(({ host }) => host.hostId === this.normalizeHostId(request.preferredHost)) : -1;
|
|
474
|
+
const index = preferredIndex >= 0 ? preferredIndex : request.placementSeed
|
|
475
|
+
? createHash("sha256").update(request.placementSeed).digest().readUInt32BE(0) % eligible.length
|
|
476
|
+
: randomInt(eligible.length);
|
|
477
|
+
const targetSlot = this.createAvailableSlot(eligible[index].host);
|
|
478
|
+
targetSlot.memoryMb = memoryMb;
|
|
479
|
+
targetSlot.allocatedMemoryMb = memoryMb;
|
|
480
|
+
targetSlot.allocatedMemoryBytes = memoryMb * 1024 * 1024;
|
|
481
|
+
targetSlot.allocatedDiskGb = eligible[index].diskGb;
|
|
486
482
|
// Assign claim details
|
|
487
483
|
const claimId = `claim-${randomUUID().substring(0, 8)}`;
|
|
488
484
|
const now = new Date();
|
|
@@ -549,6 +545,7 @@ export class SlotManager {
|
|
|
549
545
|
memoryMb: targetSlot.memoryMb,
|
|
550
546
|
allocatedMemoryMb: targetSlot.memoryMb,
|
|
551
547
|
allocatedMemoryBytes: targetSlot.memoryMb * 1024 * 1024,
|
|
548
|
+
allocatedDiskGb: targetSlot.allocatedDiskGb,
|
|
552
549
|
tag: targetSlot.tag,
|
|
553
550
|
label: targetSlot.label || undefined,
|
|
554
551
|
workloadType: targetSlot.workloadType || undefined,
|
|
@@ -840,6 +837,23 @@ export class SlotManager {
|
|
|
840
837
|
async listSlots(options = {}) {
|
|
841
838
|
return this.mutex.runExclusive(async () => {
|
|
842
839
|
this.sweepExpiredSlotsInternal();
|
|
840
|
+
// Available slot IDs are a compatibility projection of current resources,
|
|
841
|
+
// never a preallocated admission pool. Existing occupied leases are untouched.
|
|
842
|
+
for (const host of this.hosts.values()) {
|
|
843
|
+
const resources = await this.readHostResources(host);
|
|
844
|
+
const additional = host.hypervisorType === "esxi" && !this.allowEsxiSlots ? 0
|
|
845
|
+
: this.availableResourceUnits(resources, host.slotSizeMb);
|
|
846
|
+
const available = Array.from(this.slots.values()).filter((slot) => slot.hostId === host.hostId && slot.status === "AVAILABLE");
|
|
847
|
+
for (const slot of available.slice(additional))
|
|
848
|
+
this.slots.delete(slot.slotId);
|
|
849
|
+
for (let index = available.length; index < additional; index++)
|
|
850
|
+
this.createAvailableSlot(host, false);
|
|
851
|
+
for (const slot of available.slice(0, additional)) {
|
|
852
|
+
slot.memoryMb = host.slotSizeMb;
|
|
853
|
+
slot.allocatedMemoryMb = host.slotSizeMb;
|
|
854
|
+
slot.allocatedMemoryBytes = host.slotSizeMb * 1024 * 1024;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
843
857
|
const now = Date.now();
|
|
844
858
|
const results = [];
|
|
845
859
|
const filterHost = options.host || options.targetHost;
|
|
@@ -932,6 +946,9 @@ export class SlotManager {
|
|
|
932
946
|
* List host capacities or global cluster summary.
|
|
933
947
|
*/
|
|
934
948
|
async listHosts(hostId) {
|
|
949
|
+
return this.mutex.runExclusive(() => this.listHostsInternal(hostId));
|
|
950
|
+
}
|
|
951
|
+
async listHostsInternal(hostId) {
|
|
935
952
|
this.sweepExpiredSlotsInternal();
|
|
936
953
|
const normalizedHostId = hostId && hostId !== "all" ? this.normalizeHostId(hostId) : hostId;
|
|
937
954
|
// If specific ESXi host requested and not registered, auto-register
|
|
@@ -955,10 +972,12 @@ export class SlotManager {
|
|
|
955
972
|
return { ...s, remainingTtlSeconds };
|
|
956
973
|
})
|
|
957
974
|
.sort((a, b) => a.slotIndex - b.slotIndex);
|
|
958
|
-
const activeSlots = hostSlots.filter((s) => s.status
|
|
975
|
+
const activeSlots = hostSlots.filter((s) => s.status !== "AVAILABLE");
|
|
959
976
|
const allocatedSlots = activeSlots.length;
|
|
960
|
-
const
|
|
961
|
-
const availableSlots =
|
|
977
|
+
const resources = await this.readHostResources(config);
|
|
978
|
+
const availableSlots = config.hypervisorType === "esxi" && !this.allowEsxiSlots ? 0
|
|
979
|
+
: this.availableResourceUnits(resources, config.slotSizeMb);
|
|
980
|
+
const hostMaxSlots = allocatedSlots + availableSlots;
|
|
962
981
|
clusterMaxSlots += hostMaxSlots;
|
|
963
982
|
clusterAllocatedSlots += allocatedSlots;
|
|
964
983
|
clusterAvailableSlots += availableSlots;
|
|
@@ -966,15 +985,18 @@ export class SlotManager {
|
|
|
966
985
|
hostId: id,
|
|
967
986
|
name: config.name,
|
|
968
987
|
hypervisorType: config.hypervisorType,
|
|
969
|
-
totalMemoryMb:
|
|
970
|
-
reservedHeadroomMb:
|
|
988
|
+
totalMemoryMb: resources.totalMemoryMb,
|
|
989
|
+
reservedHeadroomMb: 0,
|
|
990
|
+
admissionBasis: "physical_resources",
|
|
991
|
+
operationalStatus: resources.status,
|
|
992
|
+
capacityObservedAt: resources.observedAt,
|
|
971
993
|
maxSlots: hostMaxSlots,
|
|
972
994
|
allocatedSlots,
|
|
973
995
|
availableSlots,
|
|
974
996
|
slotSizeMb: config.slotSizeMb,
|
|
975
|
-
freeDiskGb:
|
|
976
|
-
totalDiskGb:
|
|
977
|
-
datastoreFreeBytes:
|
|
997
|
+
freeDiskGb: resources.freeDiskGb,
|
|
998
|
+
totalDiskGb: resources.totalDiskGb,
|
|
999
|
+
datastoreFreeBytes: resources.freeDiskGb * 1024 ** 3,
|
|
978
1000
|
activeSlots
|
|
979
1001
|
};
|
|
980
1002
|
}
|
|
@@ -1345,23 +1367,6 @@ export class SlotManager {
|
|
|
1345
1367
|
const targetHyp = params.hypervisorType && params.hypervisorType !== "all" && params.hypervisorType !== "any"
|
|
1346
1368
|
? params.hypervisorType
|
|
1347
1369
|
: undefined;
|
|
1348
|
-
// Sync with live capacity if hypervisorManager is present
|
|
1349
|
-
if (this.hypervisorManager) {
|
|
1350
|
-
try {
|
|
1351
|
-
const liveCaps = await this.hypervisorManager.listAllHostsCapacity();
|
|
1352
|
-
for (const cap of liveCaps) {
|
|
1353
|
-
const hid = this.normalizeHostId(cap.name);
|
|
1354
|
-
const cfg = this.hosts.get(hid);
|
|
1355
|
-
if (cfg) {
|
|
1356
|
-
cfg.totalMemoryMb = Math.round(cap.memoryTotalBytes / (1024 * 1024));
|
|
1357
|
-
cfg.freeDiskGb = Math.round(cap.storageFreeBytes / (1024 * 1024 * 1024));
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
catch {
|
|
1362
|
-
// fallback to registered configs
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
1370
|
// Gather candidate hosts
|
|
1366
1371
|
const allCandidates = [];
|
|
1367
1372
|
for (const [hid, hostConfig] of this.hosts.entries()) {
|
|
@@ -1373,15 +1378,17 @@ export class SlotManager {
|
|
|
1373
1378
|
continue;
|
|
1374
1379
|
if (params.excludeHosts && params.excludeHosts.some((ex) => this.normalizeHostId(ex) === hid))
|
|
1375
1380
|
continue;
|
|
1376
|
-
|
|
1381
|
+
const resources = await this.readHostResources(hostConfig, params.storage);
|
|
1382
|
+
if (params.minFreeDiskGb && resources.freeDiskGb < params.minFreeDiskGb)
|
|
1377
1383
|
continue;
|
|
1378
1384
|
const hostSlots = Array.from(this.slots.values()).filter((s) => s.hostId === hid);
|
|
1379
|
-
const
|
|
1380
|
-
const
|
|
1381
|
-
const
|
|
1382
|
-
const
|
|
1383
|
-
const
|
|
1384
|
-
const
|
|
1385
|
+
const memoryMb = params.requiredMemoryGb !== undefined ? params.requiredMemoryGb * 1024 : hostConfig.slotSizeMb;
|
|
1386
|
+
const diskGb = params.diskGb ?? params.minFreeDiskGb ?? 10;
|
|
1387
|
+
const availCount = this.availableResourceUnits(resources, memoryMb, diskGb, params.cpuCores);
|
|
1388
|
+
const pureCapacity = hostSlots.filter((s) => s.status !== "AVAILABLE").length + availCount;
|
|
1389
|
+
const freeDisk = resources.freeDiskGb;
|
|
1390
|
+
const totalMem = resources.totalMemoryMb;
|
|
1391
|
+
const freeMem = resources.freeMemoryMb;
|
|
1385
1392
|
// Host tiering: proxmox-mini (Tier 1 Primary), proxmox-lab (Tier 2 Secondary), ESXi (Tier 3 Fallback)
|
|
1386
1393
|
let tier = "secondary";
|
|
1387
1394
|
let tierBoost = 10000;
|
|
@@ -1407,15 +1414,17 @@ export class SlotManager {
|
|
|
1407
1414
|
hostId: hid,
|
|
1408
1415
|
name: hostConfig.name,
|
|
1409
1416
|
hypervisorType: hostConfig.hypervisorType || (hid.startsWith("esxi") ? "esxi" : "proxmox"),
|
|
1410
|
-
status: "online",
|
|
1417
|
+
status: resources.status === "online" ? "online" : "offline",
|
|
1411
1418
|
tier,
|
|
1412
1419
|
availableSlots: availCount,
|
|
1413
1420
|
totalPureSlots: pureCapacity,
|
|
1414
1421
|
freeMemoryMb: freeMem,
|
|
1415
1422
|
totalMemoryMb: totalMem,
|
|
1416
1423
|
freeDiskGb: freeDisk,
|
|
1424
|
+
totalDiskGb: resources.totalDiskGb,
|
|
1425
|
+
cpuTotalCores: resources.cpuTotalCores,
|
|
1417
1426
|
cpuUsedPercent: 0,
|
|
1418
|
-
runningVms: hostSlots.
|
|
1427
|
+
runningVms: hostSlots.filter((slot) => slot.status !== "AVAILABLE").length,
|
|
1419
1428
|
score,
|
|
1420
1429
|
recommendationReason
|
|
1421
1430
|
});
|
|
@@ -1479,6 +1488,10 @@ export class SlotManager {
|
|
|
1479
1488
|
branch: params.branch,
|
|
1480
1489
|
host: winningCandidate.hostId,
|
|
1481
1490
|
durationMinutes: params.durationMinutes,
|
|
1491
|
+
slotSizeMb: params.requiredMemoryGb !== undefined ? params.requiredMemoryGb * 1024 : undefined,
|
|
1492
|
+
diskGb: params.diskGb ?? params.minFreeDiskGb,
|
|
1493
|
+
cpuCores: params.cpuCores,
|
|
1494
|
+
storage: params.storage,
|
|
1482
1495
|
allowEsxiSlots: allowEsxi,
|
|
1483
1496
|
purpose: `Auto-claimed via find_slot on ${winningCandidate.hostId}`
|
|
1484
1497
|
});
|
|
@@ -1588,6 +1601,10 @@ export class SlotManager {
|
|
|
1588
1601
|
branch: params.branch,
|
|
1589
1602
|
host: targetHost || "auto",
|
|
1590
1603
|
durationMinutes: params.durationMinutes,
|
|
1604
|
+
slotSizeMb: params.requiredMemoryGb !== undefined ? params.requiredMemoryGb * 1024 : undefined,
|
|
1605
|
+
diskGb: params.diskGb ?? params.minFreeDiskGb,
|
|
1606
|
+
cpuCores: params.cpuCores,
|
|
1607
|
+
storage: params.storage,
|
|
1591
1608
|
allowEsxiSlots: allowEsxi,
|
|
1592
1609
|
purpose: `Auto-reclaimed and claimed via find_slot`
|
|
1593
1610
|
});
|