Files
testing/scenarios/baseos-qemu-enrollment/scenario.ts
T

148 lines
5.0 KiB
TypeScript
Raw Normal View History

2026-05-07 20:33:14 +00:00
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
import { spawn } from 'node:child_process';
import { access } from 'node:fs/promises';
const imagePath = process.env.BASEOS_QEMU_IMAGE;
const cloudlyPort = Number(process.env.BASEOS_QEMU_CLOUDLY_PORT || '18080');
const bootTimeoutMs = Number(process.env.BASEOS_QEMU_BOOT_TIMEOUT_MS || String(1000 * 60 * 5));
const commandExists = async (commandArg: string) => {
const child = spawn('bash', ['-lc', `command -v ${commandArg}`], { stdio: 'ignore' });
return await new Promise<boolean>((resolveArg) => {
child.on('close', (codeArg) => resolveArg(codeArg === 0));
child.on('error', () => resolveArg(false));
});
};
const readJsonBody = async (reqArg: IncomingMessage) => {
const chunks: Buffer[] = [];
for await (const chunk of reqArg) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const bodyText = Buffer.concat(chunks).toString('utf8').trim();
return bodyText ? JSON.parse(bodyText) as Record<string, unknown> : {};
};
const sendJson = (resArg: ServerResponse, statusCodeArg: number, bodyArg: Record<string, unknown>) => {
resArg.statusCode = statusCodeArg;
resArg.setHeader('content-type', 'application/json');
resArg.end(JSON.stringify(bodyArg));
};
const startMockCloudly = async () => {
let registrationBody: Record<string, unknown> | undefined;
const server = createServer(async (req, res) => {
try {
const requestUrl = new URL(req.url || '/', `http://127.0.0.1:${cloudlyPort}`);
if (req.method === 'POST' && requestUrl.pathname === '/baseos/v1/nodes/register') {
registrationBody = await readJsonBody(req);
sendJson(res, 200, {
accepted: true,
nodeId: 'qemu-baseos-node',
nodeToken: 'qemu-node-token',
desiredState: {},
});
return;
}
if (req.method === 'POST' && requestUrl.pathname === '/baseos/v1/nodes/heartbeat') {
sendJson(res, 200, {
accepted: true,
desiredState: {},
});
return;
}
sendJson(res, 404, { errorText: 'not found' });
} catch (error) {
sendJson(res, 500, { errorText: (error as Error).message });
}
});
await new Promise<void>((resolveArg) => server.listen({ port: cloudlyPort, host: '0.0.0.0' }, resolveArg));
return {
server,
getRegistrationBody: () => registrationBody,
};
};
const stopServer = async (serverArg: Server) => {
await new Promise<void>((resolveArg, rejectArg) => {
serverArg.close((errorArg) => errorArg ? rejectArg(errorArg) : resolveArg());
});
};
const waitForRegistration = async (getRegistrationBodyArg: () => Record<string, unknown> | undefined) => {
const startTime = Date.now();
while (Date.now() - startTime < bootTimeoutMs) {
const registrationBody = getRegistrationBodyArg();
if (registrationBody) {
return registrationBody;
}
await new Promise((resolveArg) => setTimeout(resolveArg, 1000));
}
throw new Error(`BaseOS QEMU image did not register within ${bootTimeoutMs}ms`);
};
const main = async () => {
if (!imagePath) {
console.log('[baseos-qemu-enrollment] Skipping: BASEOS_QEMU_IMAGE is not set');
return;
}
await access(imagePath);
if (!(await commandExists('qemu-system-x86_64'))) {
throw new Error('Missing required command for BaseOS QEMU scenario: qemu-system-x86_64');
}
const mockCloudly = await startMockCloudly();
const qemu = spawn('qemu-system-x86_64', [
'-machine',
'accel=kvm:tcg',
'-m',
process.env.BASEOS_QEMU_MEMORY || '2048',
'-smp',
process.env.BASEOS_QEMU_CPUS || '2',
'-nographic',
'-no-reboot',
'-drive',
`file=${imagePath},format=raw,if=virtio`,
'-netdev',
'user,id=net0',
'-device',
'virtio-net-pci,netdev=net0',
'-serial',
'mon:stdio',
], {
stdio: ['ignore', 'pipe', 'pipe'],
});
const logs: string[] = [];
const collectLog = (chunkArg: Buffer) => {
logs.push(chunkArg.toString('utf8'));
if (logs.length > 100) {
logs.splice(0, logs.length - 100);
}
};
qemu.stdout.on('data', collectLog);
qemu.stderr.on('data', collectLog);
try {
const registrationBody = await Promise.race([
waitForRegistration(mockCloudly.getRegistrationBody),
new Promise<never>((_resolveArg, rejectArg) => {
qemu.on('close', (codeArg) => {
rejectArg(new Error(`QEMU exited before BaseOS registration with code ${codeArg}\n${logs.join('')}`));
});
}),
]);
const status = registrationBody.status as { runtime?: string; nodeId?: string } | undefined;
if (status?.runtime !== 'baseos' || !status.nodeId) {
throw new Error(`Invalid BaseOS registration payload: ${JSON.stringify(registrationBody)}`);
}
console.log('[baseos-qemu-enrollment] BaseOS QEMU enrollment scenario completed successfully');
} finally {
qemu.kill('SIGTERM');
setTimeout(() => qemu.kill('SIGKILL'), 5000).unref();
await stopServer(mockCloudly.server).catch(() => undefined);
}
};
await main();