integration: VM drill for dedicated data disks
All checks were successful
CI/CD / typecheck (pull_request) Successful in 49s
CI/CD / lint (pull_request) Successful in 1m11s
CI/CD / test (pull_request) Successful in 1m14s
CI/CD / build (pull_request) Successful in 1m24s
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
All checks were successful
CI/CD / typecheck (pull_request) Successful in 49s
CI/CD / lint (pull_request) Successful in 1m11s
CI/CD / test (pull_request) Successful in 1m14s
CI/CD / build (pull_request) Successful in 1m24s
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
One real-VM test mirroring thebeast's topology: OS disk + two data disks addressed by /dev/disk/by-id/ (virtio serials standing in for NVMe serials). Fresh k8s-worker v2 install must put labelled whole-disk xfs on each data disk and drop the matching LVs; a flagless reprovision must rediscover both by label and remount them unformatted -- proven by marker files AND unchanged filesystem UUIDs. Harness: createPxeVm grows dataDisks (extra virtio disks with fixed serials, <=20 chars or the guest by-id link truncates), provisionVm passes the by-id paths to /api/install, virtioById exported. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V6HNx6cyr4J4CiMGCy1wkF
This commit is contained in:
@@ -304,6 +304,15 @@ export interface ProvisionedVm { name: string; mac: string; ip: string }
|
||||
*/
|
||||
let nextSerialPort = 4600;
|
||||
|
||||
/**
|
||||
* The stable path a virtio data disk gets inside the guest. This is what the
|
||||
* install request must carry: the feature refuses anything outside
|
||||
* /dev/disk/by-id/, exactly as it does for real NVMe serials.
|
||||
*/
|
||||
export function virtioById(serial: string): string {
|
||||
return `/dev/disk/by-id/virtio-${serial}`;
|
||||
}
|
||||
|
||||
/** Create a blank VM, let it discover, queue the install, wait for SSH. */
|
||||
export async function provisionVm(h: Harness, spec: {
|
||||
name: string;
|
||||
@@ -312,9 +321,17 @@ export async function provisionVm(h: Harness, spec: {
|
||||
diskGb: number;
|
||||
memory?: number;
|
||||
vcpus?: number;
|
||||
/**
|
||||
* Dedicated data disks: attached to the VM with the given virtio serial AND
|
||||
* passed to /api/install as /dev/disk/by-id/virtio-<serial>. Mirrors
|
||||
* thebeast's layout (OS disk + separate longhorn + rancher disks).
|
||||
*/
|
||||
dataDisks?: { longhorn?: { serial: string; sizeGb: number }; rancher?: { serial: string; sizeGb: number } };
|
||||
}): Promise<ProvisionedVm> {
|
||||
const base = `http://${PXE_GATEWAY}:${h.httpPort}`;
|
||||
log(`[${spec.name}] creating VM (${spec.diskGb}GB disk, role=${spec.role}, schema=v${spec.schemaVersion ?? 1})`);
|
||||
const vmDataDisks = [spec.dataDisks?.longhorn, spec.dataDisks?.rancher]
|
||||
.filter((d): d is { serial: string; sizeGb: number } => d !== undefined);
|
||||
log(`[${spec.name}] creating VM (${spec.diskGb}GB disk + ${vmDataDisks.length} data disk(s), role=${spec.role}, schema=v${spec.schemaVersion ?? 1})`);
|
||||
createPxeVm({
|
||||
name: spec.name,
|
||||
memory: spec.memory ?? 4096,
|
||||
@@ -322,6 +339,7 @@ export async function provisionVm(h: Harness, spec: {
|
||||
diskSize: spec.diskGb,
|
||||
network: PXE_NETWORK_NAME,
|
||||
serialPort: nextSerialPort++,
|
||||
...(vmDataDisks.length > 0 ? { dataDisks: vmDataDisks } : {}),
|
||||
});
|
||||
deleteNftablesRejectRules();
|
||||
|
||||
@@ -339,6 +357,12 @@ export async function provisionVm(h: Harness, spec: {
|
||||
body: JSON.stringify({
|
||||
mac, hostname: spec.name, disk: "", role: spec.role,
|
||||
...(spec.schemaVersion ? { schemaVersion: spec.schemaVersion } : {}),
|
||||
...(spec.dataDisks ? {
|
||||
dataDisks: {
|
||||
...(spec.dataDisks.longhorn ? { longhorn: virtioById(spec.dataDisks.longhorn.serial) } : {}),
|
||||
...(spec.dataDisks.rancher ? { rancher: virtioById(spec.dataDisks.rancher.serial) } : {}),
|
||||
},
|
||||
} : {}),
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`[${spec.name}] install queue rejected: ${res.status} ${await res.text()}`);
|
||||
|
||||
@@ -47,6 +47,15 @@ export interface PxeVmConfig {
|
||||
*/
|
||||
decoyNics?: number;
|
||||
decoyNetwork?: string;
|
||||
/**
|
||||
* Extra blank data disks, attached after the OS disk. Each gets a fixed
|
||||
* virtio serial, so inside the guest it has a stable
|
||||
* /dev/disk/by-id/virtio-<serial> path -- the moral equivalent of the
|
||||
* nvme-..._<serial> links the dedicated-data-disk feature requires on real
|
||||
* hardware. virtio-blk truncates serials at 20 characters, so keep them
|
||||
* short or the by-id link quietly won't match.
|
||||
*/
|
||||
dataDisks?: { serial: string; sizeGb: number }[];
|
||||
}
|
||||
|
||||
/** Create a blank UEFI VM that PXE boots from the network. */
|
||||
@@ -60,6 +69,14 @@ export function createPxeVm(config: PxeVmConfig): void {
|
||||
const diskPath = join(IMAGE_DIR, `${config.name}.qcow2`);
|
||||
run(`qemu-img create -f qcow2 "${diskPath}" ${config.diskSize}G`);
|
||||
|
||||
const dataDiskArgs: string[] = [];
|
||||
for (const dd of config.dataDisks ?? []) {
|
||||
if (dd.serial.length > 20) throw new Error(`virtio serial '${dd.serial}' exceeds 20 chars -- the guest by-id link would be truncated`);
|
||||
const p = join(IMAGE_DIR, `${config.name}-${dd.serial}.qcow2`);
|
||||
run(`qemu-img create -f qcow2 "${p}" ${dd.sizeGb}G`);
|
||||
dataDiskArgs.push(`--disk=path=${p},format=qcow2,bus=virtio,serial=${dd.serial}`);
|
||||
}
|
||||
|
||||
// UEFI firmware paths (Fedora)
|
||||
if (arch === "aarch64") {
|
||||
const aavmf = "/usr/share/edk2/aarch64/QEMU_EFI.fd";
|
||||
@@ -79,6 +96,7 @@ export function createPxeVm(config: PxeVmConfig): void {
|
||||
`--memory=${config.memory}`,
|
||||
`--vcpus=${config.vcpus}`,
|
||||
`--disk=path=${diskPath},format=qcow2,bus=virtio`,
|
||||
...dataDiskArgs,
|
||||
// Decoys first so they enumerate ahead of the PXE NIC. They are up and
|
||||
// carry a lease, but have no route to the bastion.
|
||||
...Array.from({ length: config.decoyNics ?? 0 }, () =>
|
||||
|
||||
Reference in New Issue
Block a user