feat: Asahi rootfs build pipeline + serve from bastion
Some checks failed
CI/CD / lint (pull_request) Failing after 10s
CI/CD / test (pull_request) Failing after 10s
CI/CD / typecheck (pull_request) Failing after 22s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
Some checks failed
CI/CD / lint (pull_request) Failing after 10s
CI/CD / test (pull_request) Failing after 10s
CI/CD / typecheck (pull_request) Failing after 22s
CI/CD / build (pull_request) Has been skipped
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
- Add scripts/build-asahi-rootfs.sh: downloads upstream Fedora Asahi Remix Server, injects lab firstboot script + systemd service + SSH keys, repackages with installer_data.json that adds LVM Data partition - Bastion serves built artifacts at /asahi/repo/* via fastify-static - installer_data.json prefers built config, falls back to minimal - Fix __dirname crash in ESM module (use import.meta.url) - Fix smoke test timeout (was crashing due to __dirname) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,47 @@
|
||||
// Routes for Asahi Linux provisioning.
|
||||
// GET /asahi — wrapper script (curl https://bastion:8080/asahi | sh)
|
||||
// GET /asahi/installer_data.json — custom installer config with LVM partition layout
|
||||
// GET /asahi/firstboot.sh — first-boot LVM setup script (for manual use)
|
||||
// GET /asahi — wrapper script (curl bastion:8080/asahi | sh)
|
||||
// GET /asahi/installer_data.json — custom installer config (built or fallback)
|
||||
// GET /asahi/repo/* — serves built rootfs package (fedora-asahi-lab.zip)
|
||||
// GET /asahi/firstboot.sh — first-boot LVM setup script (for manual use)
|
||||
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import fastifyStatic from "@fastify/static";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { BastionConfig } from "@lab/shared";
|
||||
import { renderFirstbootScript, renderFirstbootUnit } from "../templates/asahi-firstboot.sh.js";
|
||||
import type { Role } from "@lab/shared";
|
||||
|
||||
/** Find the asahi-repo directory (built by scripts/build-asahi-rootfs.sh). */
|
||||
function findAsahiRepo(config: BastionConfig): string | null {
|
||||
// Check relative to bastionDir (container deploy)
|
||||
const inBastionDir = join(config.bastionDir, "asahi-repo");
|
||||
if (existsSync(inBastionDir)) return inBastionDir;
|
||||
|
||||
// Check relative to project root (dev mode)
|
||||
try {
|
||||
const thisDir = dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = join(thisDir, "..", "..", "..", "..");
|
||||
const inProjectRoot = join(projectRoot, "asahi-repo");
|
||||
if (existsSync(inProjectRoot)) return inProjectRoot;
|
||||
} catch { /* import.meta.url not available in tests */ }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function registerAsahiRoutes(app: FastifyInstance, config: BastionConfig): void {
|
||||
const repoDir = findAsahiRepo(config);
|
||||
|
||||
// Serve built rootfs package files (fedora-asahi-lab.zip, etc.)
|
||||
if (repoDir) {
|
||||
app.register(fastifyStatic, {
|
||||
root: repoDir,
|
||||
prefix: "/asahi/repo/",
|
||||
decorateReply: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Wrapper script — user runs: curl http://bastion:8080/asahi | sh
|
||||
app.get("/asahi", async (_request, reply) => {
|
||||
const script = `#!/bin/bash
|
||||
@@ -49,14 +82,14 @@ tar xf "installer-\${PKG_VER}.tar.gz"
|
||||
|
||||
# Point to our custom installer_data.json + rootfs repo
|
||||
export INSTALLER_DATA="\${BASTION}/asahi/installer_data.json"
|
||||
export REPO_BASE="\${BASTION}/asahi/repo"
|
||||
export REPO_BASE="\${BASTION}/asahi/repo/"
|
||||
|
||||
echo ""
|
||||
echo " Using custom installer data from bastion."
|
||||
echo " This will create:"
|
||||
echo " - Standard Asahi boot infrastructure (m1n1 + U-Boot)"
|
||||
echo " - 60GB root partition (Fedora Asahi Remix)"
|
||||
echo " - Remaining space as LVM data partition"
|
||||
echo " - Fedora Asahi Remix root partition"
|
||||
echo " - LVM data partition (remaining space)"
|
||||
echo ""
|
||||
echo " On first boot, LVM volumes will be created automatically:"
|
||||
echo " labvg/var (100GB), labvg/varlog (10GB), labvg/home (10GB),"
|
||||
@@ -74,48 +107,34 @@ fi
|
||||
return reply.type("text/x-shellscript").send(script);
|
||||
});
|
||||
|
||||
// Custom installer_data.json — two Linux partitions (root + LVM data)
|
||||
// Custom installer_data.json — serves built config or fallback
|
||||
app.get("/asahi/installer_data.json", async (_request, reply) => {
|
||||
// This follows the Asahi installer_data.json schema.
|
||||
// We define a fixed-size root and an expanding data partition for LVM.
|
||||
const data = {
|
||||
os_list: [
|
||||
{
|
||||
name: "Fedora Asahi Lab",
|
||||
default_os_name: "Fedora Linux with Lab LVM",
|
||||
boot_object: "m1n1.bin",
|
||||
next_object: "u-boot-nodtb.bin",
|
||||
package: "fedora-asahi-lab.zip",
|
||||
supported_fw: ["13.5"],
|
||||
partitions: [
|
||||
{
|
||||
name: "EFI",
|
||||
type: "EFI",
|
||||
size: "500MB",
|
||||
format: "fat",
|
||||
copy_firmware: true,
|
||||
copy_installer_data: true,
|
||||
source: "esp",
|
||||
},
|
||||
{
|
||||
name: "Root",
|
||||
type: "Linux",
|
||||
size: "60GB",
|
||||
image: "root.img",
|
||||
expand: false,
|
||||
},
|
||||
{
|
||||
name: "Data",
|
||||
type: "Linux",
|
||||
size: "1GB",
|
||||
expand: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
// Prefer the built installer_data.json (from build-asahi-rootfs.sh)
|
||||
if (repoDir) {
|
||||
const builtConfig = join(repoDir, "installer_data.json");
|
||||
if (existsSync(builtConfig)) {
|
||||
const data = JSON.parse(readFileSync(builtConfig, "utf-8"));
|
||||
return reply.type("application/json").send(data);
|
||||
}
|
||||
}
|
||||
|
||||
return reply.type("application/json").send(data);
|
||||
// Fallback: minimal config (won't have boot.img, for testing only)
|
||||
return reply.type("application/json").send({
|
||||
os_list: [{
|
||||
name: "Fedora Asahi Lab",
|
||||
default_os_name: "Fedora Linux with Lab LVM",
|
||||
boot_object: "m1n1.bin",
|
||||
next_object: "m1n1/boot.bin",
|
||||
package: "fedora-asahi-lab.zip",
|
||||
supported_fw: ["13.5"],
|
||||
partitions: [
|
||||
{ name: "EFI", type: "EFI", size: "524288000B", format: "fat",
|
||||
copy_firmware: true, copy_installer_data: true, source: "esp" },
|
||||
{ name: "Root", type: "Linux", size: "5368709120B", image: "root.img", expand: false },
|
||||
{ name: "Data", type: "Linux", size: "1073741824B", expand: true },
|
||||
],
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
// First-boot script — for manual download or embedding in rootfs
|
||||
@@ -125,17 +144,14 @@ fi
|
||||
const hostname = request.query.hostname ?? "mac-studio";
|
||||
const role = (request.query.role ?? "infra") as Role;
|
||||
const mac = request.query.mac ?? "unknown";
|
||||
const user = request.query.user ?? "michal";
|
||||
|
||||
// Read SSH keys from bastion config
|
||||
const sshKeys = config.sshKeys ?? [];
|
||||
const user = request.query.user ?? config.adminUser;
|
||||
|
||||
const script = renderFirstbootScript({
|
||||
hostname,
|
||||
role,
|
||||
serverIp: config.serverIp,
|
||||
httpPort: config.httpPort,
|
||||
sshKeys,
|
||||
sshKeys: config.sshKeys ?? [],
|
||||
adminUser: user,
|
||||
mac,
|
||||
});
|
||||
|
||||
@@ -75,14 +75,14 @@ describe("asahi routes", () => {
|
||||
|
||||
expect(data.os_list).toHaveLength(1);
|
||||
const os = data.os_list[0];
|
||||
expect(os.name).toBe("Fedora Asahi Lab");
|
||||
expect(os.name).toContain("Fedora Asahi Lab");
|
||||
|
||||
// Three partitions: EFI + Root + Data
|
||||
expect(os.partitions).toHaveLength(3);
|
||||
expect(os.partitions[0].type).toBe("EFI");
|
||||
expect(os.partitions[1].type).toBe("Linux");
|
||||
expect(os.partitions[1].size).toBe("60GB");
|
||||
expect(os.partitions[1].expand).toBe(false);
|
||||
expect(os.partitions[1].image).toBe("root.img");
|
||||
expect(os.partitions[2].type).toBe("Linux");
|
||||
expect(os.partitions[2].expand).toBe(true);
|
||||
});
|
||||
|
||||
@@ -137,7 +137,7 @@ describe("bastion smoke tests", () => {
|
||||
|
||||
// Wait for the server to start (look for the banner)
|
||||
const startedAt = Date.now();
|
||||
const maxWait = 10_000;
|
||||
const maxWait = 15_000;
|
||||
while (Date.now() - startedAt < maxWait) {
|
||||
if (stdout.includes("Waiting for PXE boot requests")) break;
|
||||
await sleep(200);
|
||||
|
||||
Reference in New Issue
Block a user