provisionAgent fix 12-20-25
This commit is contained in:
parent
28a5b80e01
commit
43f1853e4f
@ -1,46 +1,20 @@
|
||||
// src/api/provisionDev.js
|
||||
// DEV-SIDE request normalization + validation
|
||||
// src/api/handlers/provisionDev.js
|
||||
|
||||
export function normalizeDevRequest(body = {}) {
|
||||
const {
|
||||
customerId,
|
||||
name,
|
||||
cpuCores,
|
||||
memoryMiB,
|
||||
diskGiB,
|
||||
portsNeeded,
|
||||
if (!body.runtime) {
|
||||
throw new Error("runtime is required for dev container");
|
||||
}
|
||||
|
||||
// dev fields
|
||||
runtime,
|
||||
|
||||
// canonical runtime version field for dev (matches your curl)
|
||||
version,
|
||||
|
||||
// legacy/alternate naming (optional)
|
||||
runtimeVersion,
|
||||
|
||||
// optional addons
|
||||
addons,
|
||||
} = body;
|
||||
|
||||
if (!customerId) throw new Error("customerId required");
|
||||
if (!runtime) throw new Error("runtime required");
|
||||
const resolvedVersion = version || runtimeVersion;
|
||||
if (!resolvedVersion) throw new Error("version required");
|
||||
if (!body.version) {
|
||||
throw new Error("version is required for dev container");
|
||||
}
|
||||
|
||||
return {
|
||||
customerId,
|
||||
name,
|
||||
cpuCores,
|
||||
memoryMiB,
|
||||
diskGiB,
|
||||
portsNeeded,
|
||||
|
||||
runtime,
|
||||
version: String(resolvedVersion),
|
||||
|
||||
addons: Array.isArray(addons) ? addons : undefined,
|
||||
customerId: body.customerId,
|
||||
runtime: body.runtime,
|
||||
version: body.version,
|
||||
memoryMiB: body.memoryMiB || 2048,
|
||||
cpuCores: body.cpuCores || 2,
|
||||
portsNeeded: body.portsNeeded || 0,
|
||||
};
|
||||
}
|
||||
|
||||
export default { normalizeDevRequest };
|
||||
|
||||
@ -1,60 +1,22 @@
|
||||
// src/api/provisionGame.js
|
||||
// GAME-SIDE request normalization + validation (no payload changes yet)
|
||||
// src/api/handlers/provisionGame.js
|
||||
|
||||
export function normalizeGameRequest(body = {}) {
|
||||
const {
|
||||
customerId,
|
||||
game,
|
||||
variant,
|
||||
version,
|
||||
world,
|
||||
name,
|
||||
cpuCores,
|
||||
memoryMiB,
|
||||
diskGiB,
|
||||
portsNeeded,
|
||||
artifactPath,
|
||||
javaPath,
|
||||
if (!body.game) {
|
||||
throw new Error("game is required");
|
||||
}
|
||||
|
||||
// passthrough creds (kept here just for shaping; payload stays in provisionAgent.js for now)
|
||||
steamUser,
|
||||
steamPass,
|
||||
steamAuth,
|
||||
adminUser,
|
||||
adminPass,
|
||||
} = body;
|
||||
|
||||
if (!customerId) throw new Error("customerId required");
|
||||
if (!game) throw new Error("game required");
|
||||
if (!variant) throw new Error("variant required");
|
||||
|
||||
const gameLower = String(game).toLowerCase();
|
||||
const isMinecraft = gameLower.includes("minecraft");
|
||||
if (!body.variant) {
|
||||
throw new Error("variant is required");
|
||||
}
|
||||
|
||||
return {
|
||||
customerId,
|
||||
game,
|
||||
variant,
|
||||
version,
|
||||
world,
|
||||
|
||||
name,
|
||||
cpuCores,
|
||||
memoryMiB,
|
||||
diskGiB,
|
||||
portsNeeded,
|
||||
|
||||
artifactPath,
|
||||
javaPath,
|
||||
|
||||
steamUser,
|
||||
steamPass,
|
||||
steamAuth,
|
||||
adminUser,
|
||||
adminPass,
|
||||
|
||||
isMinecraft,
|
||||
customerId: body.customerId,
|
||||
game: body.game,
|
||||
variant: body.variant,
|
||||
version: body.version,
|
||||
world: body.world || "world",
|
||||
memoryMiB: body.memoryMiB || 2048,
|
||||
cpuCores: body.cpuCores || 2,
|
||||
portsNeeded: body.portsNeeded || 0,
|
||||
};
|
||||
}
|
||||
|
||||
export default { normalizeGameRequest };
|
||||
|
||||
@ -1,12 +1,5 @@
|
||||
// src/api/provisionAgent.js
|
||||
// FINAL AGENT-DRIVEN PROVISIONING PIPELINE
|
||||
// Supports: paper, vanilla, purpur, forge, fabric, neoforge + dev containers
|
||||
//
|
||||
// Updated (Dec 2025):
|
||||
// - Keep V3 hostname behavior (FQDN: mc-vanilla-5072.zerolaghub.quest)
|
||||
// - Decouple edge publishing from PortPool allocation
|
||||
// - Minecraft does NOT allocate PortPool ports, but still publishes edge using routing port 25565
|
||||
// - Preserve game/dev validation split (normalizeGameRequest / normalizeDevRequest)
|
||||
// FINAL AGENT-DRIVEN PROVISIONING PIPELINE (STABLE + SCALABLE)
|
||||
|
||||
import "dotenv/config";
|
||||
import fetch from "node-fetch";
|
||||
@ -21,7 +14,6 @@ import {
|
||||
} from "../services/proxmoxClient.js";
|
||||
|
||||
import { getCtIpWithRetry } from "../services/getCtIp.js";
|
||||
import { PortAllocationService } from "../services/portAllocator.js";
|
||||
import {
|
||||
allocateVmid,
|
||||
confirmVmidAllocated,
|
||||
@ -33,7 +25,6 @@ import { enqueuePublishEdge } from "../queues/postProvision.js";
|
||||
import { normalizeGameRequest } from "./handlers/provisionGame.js";
|
||||
import { normalizeDevRequest } from "./handlers/provisionDev.js";
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const AGENT_TEMPLATE_VMID = Number(
|
||||
process.env.AGENT_TEMPLATE_VMID ||
|
||||
@ -45,179 +36,51 @@ const AGENT_TEMPLATE_VMID = Number(
|
||||
const AGENT_PORT = Number(process.env.ZLH_AGENT_PORT || 18888);
|
||||
const AGENT_TOKEN = process.env.ZLH_AGENT_TOKEN || null;
|
||||
|
||||
// V3 behavior: slotHostname is FQDN built here
|
||||
const ZONE = process.env.TECHNITIUM_ZONE || "zerolaghub.quest";
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
/* -------------------------------------------------------------
|
||||
VERSION PARSER (Minecraft only)
|
||||
PAYLOAD BUILDERS (CANONICAL)
|
||||
------------------------------------------------------------- */
|
||||
function parseMcVersion(ver) {
|
||||
if (!ver) return { major: 0, minor: 0, patch: 0 };
|
||||
const p = String(ver).split(".");
|
||||
return {
|
||||
major: Number(p[0]) || 0,
|
||||
minor: Number(p[1]) || 0,
|
||||
patch: Number(p[2]) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------
|
||||
JAVA RUNTIME SELECTOR (Minecraft only)
|
||||
------------------------------------------------------------- */
|
||||
function pickJavaRuntimeForMc(version) {
|
||||
const { major, minor, patch } = parseMcVersion(version);
|
||||
|
||||
if (major > 1) return 21;
|
||||
|
||||
if (major === 1) {
|
||||
if (minor >= 21) return 21;
|
||||
if (minor === 20 && patch >= 5) return 21;
|
||||
if (minor > 20) return 21;
|
||||
return 17;
|
||||
}
|
||||
|
||||
return 17;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------
|
||||
HOSTNAME GENERATION
|
||||
------------------------------------------------------------- */
|
||||
function generateSystemHostname({ ctype, game, variant, vmid }) {
|
||||
if (ctype === "dev") return `dev-${vmid}`;
|
||||
|
||||
const g = (game || "").toLowerCase();
|
||||
const v = (variant || "").toLowerCase();
|
||||
|
||||
let prefix = "game";
|
||||
if (g.includes("minecraft")) prefix = "mc";
|
||||
else if (g.includes("terraria")) prefix = "terraria";
|
||||
else if (g.includes("valheim")) prefix = "valheim";
|
||||
else if (g.includes("rust")) prefix = "rust";
|
||||
|
||||
let varPart = "";
|
||||
if (g.includes("minecraft")) {
|
||||
if (
|
||||
["paper", "forge", "fabric", "vanilla", "purpur", "neoforge"].includes(v)
|
||||
) {
|
||||
varPart = v;
|
||||
}
|
||||
}
|
||||
|
||||
return varPart ? `${prefix}-${varPart}-${vmid}` : `${prefix}-${vmid}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------
|
||||
ADMIN PASSWORD GENERATOR
|
||||
------------------------------------------------------------- */
|
||||
function generateAdminPassword() {
|
||||
return crypto.randomBytes(12).toString("base64url");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------
|
||||
GAME PAYLOAD
|
||||
------------------------------------------------------------- */
|
||||
function buildGameAgentPayload({
|
||||
vmid,
|
||||
game,
|
||||
variant,
|
||||
version,
|
||||
world,
|
||||
ports,
|
||||
artifactPath,
|
||||
javaPath,
|
||||
memoryMiB,
|
||||
steamUser,
|
||||
steamPass,
|
||||
steamAuth,
|
||||
adminUser,
|
||||
adminPass,
|
||||
}) {
|
||||
const g = (game || "minecraft").toLowerCase();
|
||||
const v = (variant || "").toLowerCase();
|
||||
const ver = version || "1.20.1";
|
||||
const w = world || "world";
|
||||
|
||||
if (!v) throw new Error("variant is required");
|
||||
|
||||
let art = artifactPath;
|
||||
let jpath = javaPath;
|
||||
|
||||
if (!art && g === "minecraft") {
|
||||
switch (v) {
|
||||
case "paper":
|
||||
case "vanilla":
|
||||
case "purpur":
|
||||
art = `minecraft/${v}/${ver}/server.jar`;
|
||||
break;
|
||||
case "forge":
|
||||
art = `minecraft/forge/${ver}/forge-installer.jar`;
|
||||
break;
|
||||
case "fabric":
|
||||
art = `minecraft/fabric/${ver}/fabric-server.jar`;
|
||||
break;
|
||||
case "neoforge":
|
||||
art = `minecraft/neoforge/${ver}/neoforge-installer.jar`;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported Minecraft variant: ${v}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!jpath && g === "minecraft") {
|
||||
const javaVersion = pickJavaRuntimeForMc(ver);
|
||||
jpath =
|
||||
javaVersion === 21
|
||||
? "java/21/OpenJDK21.tar.gz"
|
||||
: "java/17/OpenJDK17.tar.gz";
|
||||
}
|
||||
|
||||
let mem = Number(memoryMiB) || 0;
|
||||
if (mem <= 0) mem = ["forge", "neoforge"].includes(v) ? 4096 : 2048;
|
||||
|
||||
return {
|
||||
vmid,
|
||||
game: g,
|
||||
variant: v,
|
||||
version: ver,
|
||||
world: w,
|
||||
ports: Array.isArray(ports) ? ports : [ports].filter(Boolean),
|
||||
artifact_path: art,
|
||||
java_path: jpath,
|
||||
memory_mb: mem,
|
||||
steam_user: steamUser || "anonymous",
|
||||
steam_pass: steamPass || "",
|
||||
steam_auth: steamAuth || "",
|
||||
admin_user: adminUser || "admin",
|
||||
admin_pass: adminPass || generateAdminPassword(),
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------
|
||||
DEV PAYLOAD
|
||||
------------------------------------------------------------- */
|
||||
function buildDevAgentPayload({ vmid, runtime, version, memoryMiB, ports }) {
|
||||
function buildDevAgentPayload({ vmid, runtime, version, memoryMiB }) {
|
||||
if (!runtime) throw new Error("runtime required for dev container");
|
||||
if (!version) throw new Error("version required for dev container");
|
||||
|
||||
return {
|
||||
vmid,
|
||||
ctype: "dev",
|
||||
ctype: "dev", // ← CRITICAL, AGENT CONTRACT
|
||||
runtime,
|
||||
version,
|
||||
memory_mb: Number(memoryMiB) || 2048,
|
||||
ports: Array.isArray(ports) ? ports : [ports].filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
function buildGameAgentPayload(req) {
|
||||
// req already normalized by provisionGame
|
||||
return {
|
||||
vmid: req.vmid,
|
||||
game: req.game,
|
||||
variant: req.variant,
|
||||
version: req.version,
|
||||
world: req.world,
|
||||
ports: req.ports || [],
|
||||
artifact_path: req.artifactPath,
|
||||
java_path: req.javaPath,
|
||||
memory_mb: req.memoryMiB,
|
||||
admin_user: req.adminUser,
|
||||
admin_pass: req.adminPass,
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------
|
||||
SEND CONFIG
|
||||
AGENT COMMUNICATION
|
||||
------------------------------------------------------------- */
|
||||
async function sendAgentConfig({ ip, payload }) {
|
||||
const url = `http://${ip}:${AGENT_PORT}/config`;
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (AGENT_TOKEN) headers["Authorization"] = `Bearer ${AGENT_TOKEN}`;
|
||||
|
||||
const resp = await fetch(url, {
|
||||
async function sendAgentConfig({ ip, payload }) {
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (AGENT_TOKEN) headers.Authorization = `Bearer ${AGENT_TOKEN}`;
|
||||
|
||||
const resp = await fetch(`http://${ip}:${AGENT_PORT}/config`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
@ -229,50 +92,18 @@ async function sendAgentConfig({ ip, payload }) {
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------
|
||||
WAIT FOR AGENT READY
|
||||
------------------------------------------------------------- */
|
||||
async function waitForAgentRunning({ ip, timeoutMs = 10 * 60_000 }) {
|
||||
const url = `http://${ip}:${AGENT_PORT}/status`;
|
||||
const headers = {};
|
||||
if (AGENT_TOKEN) headers["Authorization"] = `Bearer ${AGENT_TOKEN}`;
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastLoggedStep = null;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const resp = await fetch(url, { headers });
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
const state = (data.state || "").toLowerCase();
|
||||
const step = data.installStep || data.currentStep || "unknown";
|
||||
const progress = data.progress || "";
|
||||
|
||||
if (step !== lastLoggedStep) {
|
||||
console.log(`[AGENT ${ip}] state=${state} step=${step} ${progress}`);
|
||||
lastLoggedStep = step;
|
||||
}
|
||||
|
||||
if (state === "running") {
|
||||
console.log(`[AGENT ${ip}] ✓ Provisioning complete`);
|
||||
return data;
|
||||
}
|
||||
|
||||
if (state === "error") {
|
||||
const errorMsg = data.error || "agent error";
|
||||
console.error(`[AGENT ${ip}] ✗ ERROR:`, errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (
|
||||
!err.message.includes("ECONNREFUSED") &&
|
||||
!err.message.includes("fetch failed")
|
||||
) {
|
||||
console.error(`[AGENT ${ip}] Poll error:`, err.message);
|
||||
}
|
||||
const res = await fetch(`http://${ip}:${AGENT_PORT}/status`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.state === "running") return;
|
||||
if (data.state === "error") throw new Error(data.error || "agent error");
|
||||
}
|
||||
} catch {}
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
@ -282,63 +113,33 @@ async function waitForAgentRunning({ ip, timeoutMs = 10 * 60_000 }) {
|
||||
/* -------------------------------------------------------------
|
||||
MAIN ENTRYPOINT
|
||||
------------------------------------------------------------- */
|
||||
|
||||
export async function provisionAgentInstance(body = {}) {
|
||||
const ctype = body.ctype || "game";
|
||||
console.log(`[agentProvision] STEP 0: Starting ${ctype} container provisioning`);
|
||||
if (!["game", "dev"].includes(ctype)) {
|
||||
throw new Error(`invalid ctype: ${ctype}`);
|
||||
}
|
||||
|
||||
console.log(`[agentProvision] starting ${ctype} provisioning`);
|
||||
|
||||
// EARLY SPLIT — DO NOT MOVE
|
||||
const req =
|
||||
ctype === "dev" ? normalizeDevRequest(body) : normalizeGameRequest(body);
|
||||
|
||||
const gameLower = String(req.game || "").toLowerCase();
|
||||
const isMinecraft = ctype === "game" && gameLower.includes("minecraft");
|
||||
ctype === "dev"
|
||||
? normalizeDevRequest(body)
|
||||
: normalizeGameRequest(body);
|
||||
|
||||
let vmid;
|
||||
let ctIp;
|
||||
let allocatedPorts = [];
|
||||
let txnId = null;
|
||||
|
||||
try {
|
||||
console.log("[agentProvision] STEP 1: allocate VMID");
|
||||
vmid = await allocateVmid(ctype);
|
||||
console.log(`[agentProvision] → Allocated vmid=${vmid}`);
|
||||
console.log(`[agentProvision] vmid=${vmid}`);
|
||||
|
||||
// Allocate ports if needed (Minecraft skips PortPool; uses 25565 via Velocity)
|
||||
if (!isMinecraft && req.portsNeeded && req.portsNeeded > 0) {
|
||||
console.log("[agentProvision] STEP 2: port allocation");
|
||||
txnId = crypto.randomUUID();
|
||||
const hostname =
|
||||
ctype === "dev"
|
||||
? `dev-${vmid}`
|
||||
: req.hostname || `game-${vmid}`;
|
||||
|
||||
const portObjs = await PortAllocationService.reserve({
|
||||
game: req.game,
|
||||
variant: req.variant,
|
||||
customerId: req.customerId,
|
||||
vmid,
|
||||
purpose: ctype === "game" ? "game_main" : "dev",
|
||||
txnId,
|
||||
count: req.portsNeeded,
|
||||
});
|
||||
|
||||
allocatedPorts = Array.isArray(portObjs)
|
||||
? portObjs.map((p) => (typeof p === "object" ? p.port : p))
|
||||
: [portObjs];
|
||||
|
||||
console.log(`[agentProvision] → Allocated ports: ${allocatedPorts.join(", ")}`);
|
||||
} else {
|
||||
console.log("[agentProvision] STEP 2: port allocation (skipped)");
|
||||
}
|
||||
|
||||
const hostname = generateSystemHostname({
|
||||
ctype,
|
||||
game: req.game,
|
||||
variant: req.variant,
|
||||
vmid,
|
||||
});
|
||||
|
||||
// V3 correct behavior: build FQDN here
|
||||
const slotHostname = `${hostname}.${ZONE}`;
|
||||
|
||||
console.log(
|
||||
`[agentProvision] STEP 3: clone template ${AGENT_TEMPLATE_VMID} → vmid=${vmid}`
|
||||
);
|
||||
await cloneContainer({
|
||||
templateVmid: AGENT_TEMPLATE_VMID,
|
||||
vmid,
|
||||
@ -346,7 +147,6 @@ export async function provisionAgentInstance(body = {}) {
|
||||
full: 1,
|
||||
});
|
||||
|
||||
console.log("[agentProvision] STEP 4: configure CPU/mem/bridge/tags");
|
||||
await configureContainer({
|
||||
vmid,
|
||||
cpu: req.cpuCores || 2,
|
||||
@ -354,14 +154,9 @@ export async function provisionAgentInstance(body = {}) {
|
||||
bridge: ctype === "dev" ? "vmbr2" : "vmbr3",
|
||||
});
|
||||
|
||||
console.log("[agentProvision] STEP 5: start container");
|
||||
await startWithRetry(vmid);
|
||||
|
||||
console.log("[agentProvision] STEP 6: detect container IP");
|
||||
ctIp = await getCtIpWithRetry(vmid);
|
||||
console.log(`[agentProvision] → ctIp=${ctIp}`);
|
||||
|
||||
console.log("[agentProvision] STEP 7: build agent payload");
|
||||
const payload =
|
||||
ctype === "dev"
|
||||
? buildDevAgentPayload({
|
||||
@ -369,22 +164,15 @@ export async function provisionAgentInstance(body = {}) {
|
||||
runtime: body.runtime,
|
||||
version: body.version,
|
||||
memoryMiB: req.memoryMiB,
|
||||
ports: allocatedPorts,
|
||||
})
|
||||
: buildGameAgentPayload({
|
||||
vmid,
|
||||
...req,
|
||||
// agent can still use ports; for minecraft, provide 25565 semantic port
|
||||
ports: allocatedPorts.length > 0 ? allocatedPorts : isMinecraft ? [25565] : [],
|
||||
});
|
||||
: buildGameAgentPayload({ ...req, vmid });
|
||||
|
||||
console.log(`[agentProvision] payload:`);
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
|
||||
console.log("[agentProvision] STEP 8: POST /config to agent (async provision+start)");
|
||||
await sendAgentConfig({ ip: ctIp, payload });
|
||||
|
||||
console.log("[agentProvision] STEP 9: wait for agent to be running via /status");
|
||||
await waitForAgentRunning({ ip: ctIp });
|
||||
|
||||
console.log("[agentProvision] STEP 10: DB save");
|
||||
await prisma.containerInstance.create({
|
||||
data: {
|
||||
vmid,
|
||||
@ -392,67 +180,33 @@ export async function provisionAgentInstance(body = {}) {
|
||||
ctype,
|
||||
hostname,
|
||||
ip: ctIp,
|
||||
allocatedPorts, // matches schema
|
||||
payload,
|
||||
agentState: "running",
|
||||
agentLastSeen: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// STEP 11: commit ports ONLY if allocated from PortPool
|
||||
if (allocatedPorts.length > 0) {
|
||||
console.log("[agentProvision] STEP 11: commit ports");
|
||||
await PortAllocationService.commit({ vmid, ports: allocatedPorts });
|
||||
} else {
|
||||
console.log("[agentProvision] STEP 11: commit ports (skipped - none allocated)");
|
||||
}
|
||||
if (!payload.ctype) {
|
||||
throw new Error("Payload missing ctype (game|dev)");
|
||||
}
|
||||
|
||||
// STEP 12: publish edge for ALL game servers (Minecraft included)
|
||||
if (ctype === "game") {
|
||||
console.log("[agentProvision] STEP 12: publish edge");
|
||||
|
||||
const edgePorts =
|
||||
allocatedPorts.length > 0 ? allocatedPorts : isMinecraft ? [25565] : [];
|
||||
|
||||
await enqueuePublishEdge({
|
||||
vmid,
|
||||
slotHostname, // FQDN (V3 behavior)
|
||||
instanceHostname: hostname, // short (optional, kept for compatibility)
|
||||
ports: edgePorts,
|
||||
instanceHostname: hostname,
|
||||
ctIp,
|
||||
game: req.game,
|
||||
txnId,
|
||||
});
|
||||
} else {
|
||||
console.log("[agentProvision] STEP 12: publish edge (skipped - dev container)");
|
||||
}
|
||||
|
||||
await confirmVmidAllocated(vmid);
|
||||
|
||||
console.log("[agentProvision] COMPLETE: success");
|
||||
return { vmid, hostname, ip: ctIp, ports: allocatedPorts };
|
||||
return { vmid, hostname, ip: ctIp };
|
||||
} catch (err) {
|
||||
console.error("[agentProvision] ERROR:", err.message);
|
||||
|
||||
// Rollback ports on failure
|
||||
if (vmid && allocatedPorts.length > 0) {
|
||||
try {
|
||||
await PortAllocationService.releaseByVmid(vmid);
|
||||
console.log(`[agentProvision] → Rolled back ports for vmid=${vmid}`);
|
||||
} catch (rollbackErr) {
|
||||
console.error("[agentProvision] → Port rollback failed:", rollbackErr.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (vmid) {
|
||||
try {
|
||||
await deleteContainer(vmid);
|
||||
} catch {}
|
||||
try {
|
||||
await releaseVmid(vmid);
|
||||
} catch {}
|
||||
try { await deleteContainer(vmid); } catch {}
|
||||
try { await releaseVmid(vmid); } catch {}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user