test: add baseos qemu enrollment
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user