test: add baseos qemu enrollment
This commit is contained in:
+3
-2
@@ -11,13 +11,14 @@
|
|||||||
"scenario:corestore-volume-driver": "tsx scenarios/corestore-volume-driver/scenario.ts",
|
"scenario:corestore-volume-driver": "tsx scenarios/corestore-volume-driver/scenario.ts",
|
||||||
"scenario:registry-deploy-on-push": "tsx --tsconfig ../cloudly/tsconfig.json scenarios/registry-deploy-on-push/scenario.ts",
|
"scenario:registry-deploy-on-push": "tsx --tsconfig ../cloudly/tsconfig.json scenarios/registry-deploy-on-push/scenario.ts",
|
||||||
"scenario:baseos-image-pipeline": "tsx --tsconfig ../corebuild/tsconfig.json scenarios/baseos-image-pipeline/scenario.ts",
|
"scenario:baseos-image-pipeline": "tsx --tsconfig ../corebuild/tsconfig.json scenarios/baseos-image-pipeline/scenario.ts",
|
||||||
|
"scenario:baseos-qemu-enrollment": "tsx scenarios/baseos-qemu-enrollment/scenario.ts",
|
||||||
"scenario:onebox-basic-lifecycle": "deno run --allow-all --config ../onebox/deno.json scenarios/onebox-basic-lifecycle/scenario.ts",
|
"scenario:onebox-basic-lifecycle": "deno run --allow-all --config ../onebox/deno.json scenarios/onebox-basic-lifecycle/scenario.ts",
|
||||||
"scenario:onebox-backup-restore": "deno run --allow-all --config ../onebox/deno.json scenarios/onebox-backup-restore/scenario.ts",
|
"scenario:onebox-backup-restore": "deno run --allow-all --config ../onebox/deno.json scenarios/onebox-backup-restore/scenario.ts",
|
||||||
"scenario:onebox-cloudly-appstore-worker": "deno run --allow-all --config ../onebox/deno.json scenarios/onebox-cloudly-appstore-worker/scenario.ts",
|
"scenario:onebox-cloudly-appstore-worker": "deno run --allow-all --config ../onebox/deno.json scenarios/onebox-cloudly-appstore-worker/scenario.ts",
|
||||||
"vagrant:up": "vagrant up",
|
"vagrant:up": "vagrant up",
|
||||||
"vagrant:test": "vagrant ssh -c 'cd /serve.zone/testing && pnpm bootstrap:components && pnpm test'",
|
"vagrant:test": "vagrant ssh -c 'cd /serve.zone/testing && pnpm bootstrap:components && pnpm test'",
|
||||||
"vagrant:test:baseos": "vagrant ssh -c 'cd /serve.zone/testing && pnpm bootstrap:components && pnpm scenario:baseos-image-pipeline'",
|
"vagrant:test:baseos": "vagrant ssh -c 'cd /serve.zone/testing && pnpm bootstrap:components && pnpm scenario:baseos-image-pipeline && pnpm scenario:baseos-qemu-enrollment'",
|
||||||
"vagrant:test:full": "vagrant ssh -c 'cd /serve.zone/testing && pnpm bootstrap:components && pnpm test:full && pnpm scenario:baseos-image-pipeline'",
|
"vagrant:test:full": "vagrant ssh -c 'cd /serve.zone/testing && pnpm bootstrap:components && pnpm test:full && pnpm scenario:baseos-image-pipeline && pnpm scenario:baseos-qemu-enrollment'",
|
||||||
"vagrant:destroy": "vagrant destroy -f"
|
"vagrant:destroy": "vagrant destroy -f"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -177,9 +177,11 @@ const main = async () => {
|
|||||||
const capabilities = await capabilitiesResponse.json() as {
|
const capabilities = await capabilitiesResponse.json() as {
|
||||||
supportedArchitectures: string[];
|
supportedArchitectures: string[];
|
||||||
supportedImageKinds: string[];
|
supportedImageKinds: string[];
|
||||||
|
supportedSourcePresets: string[];
|
||||||
};
|
};
|
||||||
assert(capabilities.supportedArchitectures.includes('rpi'), 'CoreBuild did not advertise rpi support');
|
assert(capabilities.supportedArchitectures.includes('rpi'), 'CoreBuild did not advertise rpi support');
|
||||||
assert(capabilities.supportedImageKinds.includes('balena-raw'), 'CoreBuild did not advertise balena-raw support');
|
assert(capabilities.supportedImageKinds.includes('balena-raw'), 'CoreBuild did not advertise balena-raw support');
|
||||||
|
assert(capabilities.supportedSourcePresets.includes('balena-raspberrypi4-64'), 'CoreBuild did not advertise Raspberry Pi balenaOS source preset support');
|
||||||
|
|
||||||
const s3Descriptor = await smarts3.getS3Descriptor({ bucketName });
|
const s3Descriptor = await smarts3.getS3Descriptor({ bucketName });
|
||||||
const artifactKey = `${smokeId}/baseos-rpi.img.xz`;
|
const artifactKey = `${smokeId}/baseos-rpi.img.xz`;
|
||||||
@@ -237,6 +239,7 @@ const main = async () => {
|
|||||||
const baseosEnv = await readGuestFile(decompressedArtifactPath, '/baseos/baserunner.env');
|
const baseosEnv = await readGuestFile(decompressedArtifactPath, '/baseos/baserunner.env');
|
||||||
assert(baseosEnv.includes('BASEOS_CLOUDLY_URL="http://cloudly.test.local"'), 'BaseOS env Cloudly URL was not written');
|
assert(baseosEnv.includes('BASEOS_CLOUDLY_URL="http://cloudly.test.local"'), 'BaseOS env Cloudly URL was not written');
|
||||||
assert(baseosEnv.includes('BASEOS_JOIN_TOKEN="join-token-for-baseos-image-test"'), 'BaseOS env join token was not written');
|
assert(baseosEnv.includes('BASEOS_JOIN_TOKEN="join-token-for-baseos-image-test"'), 'BaseOS env join token was not written');
|
||||||
|
assert(baseosEnv.includes('BASEOS_PRELOAD_TARGET_STATE_PATH="/data/baseos/preload-target-state.json"'), 'BaseOS preload target-state path was not written');
|
||||||
|
|
||||||
console.log('[baseos-image-pipeline] BaseOS raw-image pipeline scenario completed successfully');
|
console.log('[baseos-image-pipeline] BaseOS raw-image pipeline scenario completed successfully');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -6,6 +6,9 @@ export DEBIAN_FRONTEND=noninteractive
|
|||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y ca-certificates curl git docker.io libguestfs-tools openssl qemu-system-x86 qemu-utils unzip xz-utils
|
apt-get install -y ca-certificates curl git docker.io libguestfs-tools openssl qemu-system-x86 qemu-utils unzip xz-utils
|
||||||
|
|
||||||
|
# libguestfs builds a per-user appliance; the vagrant test user must be able to read the host kernel.
|
||||||
|
chmod a+r /boot/vmlinuz-* || true
|
||||||
|
|
||||||
if [ -d /serve.zone ]; then
|
if [ -d /serve.zone ]; then
|
||||||
chown -R vagrant:vagrant /serve.zone
|
chown -R vagrant:vagrant /serve.zone
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user