feat: Asahi Linux provisioning for Apple Silicon #10
300
bastion/scripts/build-asahi-rootfs.sh
Executable file
300
bastion/scripts/build-asahi-rootfs.sh
Executable file
@@ -0,0 +1,300 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build a custom Fedora Asahi Remix rootfs with lab firstboot LVM setup.
|
||||||
|
#
|
||||||
|
# Downloads the upstream Fedora Asahi Remix Server package, injects our
|
||||||
|
# firstboot script + systemd service, and repackages it for the bastion.
|
||||||
|
#
|
||||||
|
# Requirements: root, curl, unzip, mount (loop), zip
|
||||||
|
# Output: bastion/asahi-repo/ directory with package + installer_data.json
|
||||||
|
#
|
||||||
|
# Usage: sudo ./scripts/build-asahi-rootfs.sh [--bastion-ip IP] [--http-port PORT]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
ASAHI_DIR="$PROJECT_DIR/asahi-repo"
|
||||||
|
CACHE_DIR="$PROJECT_DIR/.asahi-cache"
|
||||||
|
WORK_DIR=""
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
BASTION_IP="${BASTION_IP:-192.168.8.23}"
|
||||||
|
HTTP_PORT="${HTTP_PORT:-8080}"
|
||||||
|
ROLE="${ROLE:-infra}"
|
||||||
|
HOSTNAME="${HOSTNAME:-mac-studio}"
|
||||||
|
MAC="${MAC:-00:00:00:00:00:00}"
|
||||||
|
ADMIN_USER="${ADMIN_USER:-michal}"
|
||||||
|
|
||||||
|
# Parse args
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--bastion-ip) BASTION_IP="$2"; shift 2 ;;
|
||||||
|
--http-port) HTTP_PORT="$2"; shift 2 ;;
|
||||||
|
--role) ROLE="$2"; shift 2 ;;
|
||||||
|
--hostname) HOSTNAME="$2"; shift 2 ;;
|
||||||
|
--mac) MAC="$2"; shift 2 ;;
|
||||||
|
--admin-user) ADMIN_USER="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Resolve upstream package URL ─────────────────────────────────
|
||||||
|
echo "==> Fetching Asahi installer data..."
|
||||||
|
INSTALLER_DATA=$(curl -sfL "https://cdn.asahilinux.org/installer/installer_data.json")
|
||||||
|
|
||||||
|
# Find the Server variant package URL
|
||||||
|
SERVER_URL=$(echo "$INSTALLER_DATA" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
for os in data.get('os_list', []):
|
||||||
|
name = os.get('name', '').lower()
|
||||||
|
if 'server' in name and 'uefi' not in name and not os.get('expert'):
|
||||||
|
print(os['package'])
|
||||||
|
break
|
||||||
|
" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$SERVER_URL" ]; then
|
||||||
|
echo "ERROR: Could not find Fedora Asahi Remix Server in installer data."
|
||||||
|
echo "Available variants:"
|
||||||
|
echo "$INSTALLER_DATA" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
for os in data.get('os_list', []):
|
||||||
|
print(f\" - {os.get('name', '?')}\")" 2>/dev/null
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
PACKAGE_NAME=$(basename "$SERVER_URL")
|
||||||
|
echo " Variant: Fedora Asahi Remix Server"
|
||||||
|
echo " Package: $PACKAGE_NAME"
|
||||||
|
|
||||||
|
# Also extract the partition layout and supported_fw from upstream
|
||||||
|
UPSTREAM_CONFIG=$(echo "$INSTALLER_DATA" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
for os in data.get('os_list', []):
|
||||||
|
name = os.get('name', '').lower()
|
||||||
|
if 'server' in name and 'uefi' not in name and not os.get('expert'):
|
||||||
|
json.dump(os, sys.stdout)
|
||||||
|
break
|
||||||
|
")
|
||||||
|
|
||||||
|
# ── Download upstream package ────────────────────────────────────
|
||||||
|
mkdir -p "$CACHE_DIR" "$ASAHI_DIR"
|
||||||
|
|
||||||
|
CACHED_PKG="$CACHE_DIR/$PACKAGE_NAME"
|
||||||
|
if [ -f "$CACHED_PKG" ]; then
|
||||||
|
echo "==> Using cached package: $CACHED_PKG"
|
||||||
|
else
|
||||||
|
echo "==> Downloading $SERVER_URL..."
|
||||||
|
curl -# -L -o "$CACHED_PKG" "$SERVER_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Extract and modify rootfs ────────────────────────────────────
|
||||||
|
WORK_DIR=$(mktemp -d)
|
||||||
|
trap 'echo "==> Cleaning up..."; umount "$WORK_DIR/rootfs" 2>/dev/null || true; rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
echo "==> Extracting package..."
|
||||||
|
unzip -q -o "$CACHED_PKG" -d "$WORK_DIR/pkg"
|
||||||
|
|
||||||
|
# List contents
|
||||||
|
echo " Package contents:"
|
||||||
|
ls -lh "$WORK_DIR/pkg/" | grep -v ^total | while read -r line; do echo " $line"; done
|
||||||
|
|
||||||
|
# Find root.img
|
||||||
|
ROOT_IMG=$(find "$WORK_DIR/pkg" -name "root.img" -type f | head -1)
|
||||||
|
if [ -z "$ROOT_IMG" ]; then
|
||||||
|
echo "ERROR: root.img not found in package."
|
||||||
|
echo "Contents: $(ls "$WORK_DIR/pkg/")"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Mounting root.img..."
|
||||||
|
mkdir -p "$WORK_DIR/rootfs"
|
||||||
|
mount -o loop "$ROOT_IMG" "$WORK_DIR/rootfs"
|
||||||
|
|
||||||
|
# ── Read SSH keys from the system ────────────────────────────────
|
||||||
|
SSH_KEYS=""
|
||||||
|
REAL_USER="${SUDO_USER:-$USER}"
|
||||||
|
REAL_HOME=$(eval echo "~$REAL_USER")
|
||||||
|
for keyfile in "$REAL_HOME/.ssh/id_ed25519.pub" "$REAL_HOME/.ssh/id_ecdsa.pub" "$REAL_HOME/.ssh/id_rsa.pub"; do
|
||||||
|
if [ -f "$keyfile" ]; then
|
||||||
|
SSH_KEYS=$(cat "$keyfile")
|
||||||
|
echo " SSH key: $keyfile"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$SSH_KEYS" ]; then
|
||||||
|
echo "WARNING: No SSH public key found. You'll need to add keys manually."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Generate firstboot script from bastion ───────────────────────
|
||||||
|
echo "==> Generating firstboot script..."
|
||||||
|
|
||||||
|
# Try to get the script from a running bastion, fall back to local generation
|
||||||
|
FIRSTBOOT_SCRIPT=""
|
||||||
|
FIRSTBOOT_URL="http://$BASTION_IP:$HTTP_PORT/asahi/firstboot.sh?hostname=$HOSTNAME&role=$ROLE&mac=$MAC&user=$ADMIN_USER"
|
||||||
|
FIRSTBOOT_SCRIPT=$(curl -sf "$FIRSTBOOT_URL" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [ -z "$FIRSTBOOT_SCRIPT" ]; then
|
||||||
|
echo " Bastion not reachable, generating script locally..."
|
||||||
|
# Generate a basic firstboot script inline
|
||||||
|
FIRSTBOOT_SCRIPT=$(cd "$PROJECT_DIR" && node -e "
|
||||||
|
const { renderFirstbootScript } = require('./src/bastion/dist/templates/asahi-firstboot.sh.js');
|
||||||
|
process.stdout.write(renderFirstbootScript({
|
||||||
|
hostname: '$HOSTNAME',
|
||||||
|
role: '$ROLE',
|
||||||
|
serverIp: '$BASTION_IP',
|
||||||
|
httpPort: $HTTP_PORT,
|
||||||
|
sshKeys: $([ -n "$SSH_KEYS" ] && echo "[\"$SSH_KEYS\"]" || echo "[]"),
|
||||||
|
adminUser: '$ADMIN_USER',
|
||||||
|
mac: '$MAC',
|
||||||
|
}));
|
||||||
|
" 2>/dev/null) || {
|
||||||
|
echo " ERROR: Could not generate firstboot script. Build the project first: npm run build"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Inject files into rootfs ─────────────────────────────────────
|
||||||
|
echo "==> Injecting lab configuration into rootfs..."
|
||||||
|
|
||||||
|
# Firstboot script
|
||||||
|
echo "$FIRSTBOOT_SCRIPT" > "$WORK_DIR/rootfs/usr/local/bin/lab-firstboot.sh"
|
||||||
|
chmod 755 "$WORK_DIR/rootfs/usr/local/bin/lab-firstboot.sh"
|
||||||
|
echo " Installed: /usr/local/bin/lab-firstboot.sh"
|
||||||
|
|
||||||
|
# Systemd service
|
||||||
|
cat > "$WORK_DIR/rootfs/etc/systemd/system/lab-firstboot.service" << 'UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=Lab first-boot LVM setup
|
||||||
|
After=local-fs.target network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
ConditionPathExists=!/etc/lab-lvm-setup-done
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/lab-firstboot.sh
|
||||||
|
RemainAfterExit=yes
|
||||||
|
StandardOutput=journal+console
|
||||||
|
StandardError=journal+console
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
UNIT
|
||||||
|
echo " Installed: /etc/systemd/system/lab-firstboot.service"
|
||||||
|
|
||||||
|
# Enable the service
|
||||||
|
mkdir -p "$WORK_DIR/rootfs/etc/systemd/system/multi-user.target.wants"
|
||||||
|
ln -sf /etc/systemd/system/lab-firstboot.service \
|
||||||
|
"$WORK_DIR/rootfs/etc/systemd/system/multi-user.target.wants/lab-firstboot.service"
|
||||||
|
echo " Enabled: lab-firstboot.service"
|
||||||
|
|
||||||
|
# SSH authorized keys for root (for initial access before firstboot runs user creation)
|
||||||
|
if [ -n "$SSH_KEYS" ]; then
|
||||||
|
mkdir -p "$WORK_DIR/rootfs/root/.ssh"
|
||||||
|
chmod 700 "$WORK_DIR/rootfs/root/.ssh"
|
||||||
|
echo "$SSH_KEYS" > "$WORK_DIR/rootfs/root/.ssh/authorized_keys"
|
||||||
|
chmod 600 "$WORK_DIR/rootfs/root/.ssh/authorized_keys"
|
||||||
|
echo " Installed: /root/.ssh/authorized_keys"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure lvm2 and xfsprogs are installed (should be in server image already)
|
||||||
|
echo " Checking required packages..."
|
||||||
|
if [ -f "$WORK_DIR/rootfs/usr/sbin/pvcreate" ]; then
|
||||||
|
echo " lvm2: present"
|
||||||
|
else
|
||||||
|
echo " WARNING: lvm2 not found in rootfs. LVM setup may fail."
|
||||||
|
fi
|
||||||
|
if [ -f "$WORK_DIR/rootfs/usr/sbin/mkfs.xfs" ]; then
|
||||||
|
echo " xfsprogs: present"
|
||||||
|
else
|
||||||
|
echo " WARNING: xfsprogs not found in rootfs. LVM setup may fail."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Unmount and repackage ────────────────────────────────────────
|
||||||
|
echo "==> Unmounting rootfs..."
|
||||||
|
umount "$WORK_DIR/rootfs"
|
||||||
|
|
||||||
|
echo "==> Repackaging..."
|
||||||
|
OUTPUT_PKG="$ASAHI_DIR/fedora-asahi-lab.zip"
|
||||||
|
rm -f "$OUTPUT_PKG"
|
||||||
|
(cd "$WORK_DIR/pkg" && zip -q "$OUTPUT_PKG" *)
|
||||||
|
echo " Output: $OUTPUT_PKG ($(du -sh "$OUTPUT_PKG" | cut -f1))"
|
||||||
|
|
||||||
|
# ── Generate installer_data.json ─────────────────────────────────
|
||||||
|
echo "==> Generating installer_data.json..."
|
||||||
|
|
||||||
|
# Parse upstream config to get supported_fw, boot_object, next_object, and partition details
|
||||||
|
python3 << PYEOF > "$ASAHI_DIR/installer_data.json"
|
||||||
|
import json, sys
|
||||||
|
|
||||||
|
upstream = json.loads('''$UPSTREAM_CONFIG''')
|
||||||
|
|
||||||
|
# Build our custom installer data based on upstream
|
||||||
|
# Keep EFI and Boot partitions identical, modify Root to not expand,
|
||||||
|
# add Data partition that expands for LVM.
|
||||||
|
partitions = []
|
||||||
|
for p in upstream.get('partitions', []):
|
||||||
|
if p.get('type') == 'EFI':
|
||||||
|
partitions.append(p)
|
||||||
|
elif p.get('name') == 'Boot':
|
||||||
|
partitions.append(p)
|
||||||
|
elif p.get('name') == 'Root':
|
||||||
|
# Fixed size root, no expand
|
||||||
|
root_p = dict(p)
|
||||||
|
root_p['expand'] = False
|
||||||
|
# Keep the original size (it's the minimum needed for the rootfs)
|
||||||
|
partitions.append(root_p)
|
||||||
|
|
||||||
|
# Add Data partition for LVM
|
||||||
|
partitions.append({
|
||||||
|
"name": "Data",
|
||||||
|
"type": "Linux",
|
||||||
|
"size": "1073741824B", # 1GB minimum, will expand
|
||||||
|
"expand": True
|
||||||
|
})
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"os_list": [{
|
||||||
|
"name": "Fedora Asahi Lab (${ROLE})",
|
||||||
|
"default_os_name": "Fedora Linux Lab",
|
||||||
|
"boot_object": upstream.get("boot_object", "m1n1.bin"),
|
||||||
|
"next_object": upstream.get("next_object", "m1n1/boot.bin"),
|
||||||
|
"package": "fedora-asahi-lab.zip",
|
||||||
|
"supported_fw": upstream.get("supported_fw", ["13.5"]),
|
||||||
|
"partitions": partitions,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
json.dump(data, sys.stdout, indent=2)
|
||||||
|
print()
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
echo " Generated: $ASAHI_DIR/installer_data.json"
|
||||||
|
|
||||||
|
# Pretty-print the partition layout
|
||||||
|
echo ""
|
||||||
|
echo " Partition layout:"
|
||||||
|
python3 -c "
|
||||||
|
import json
|
||||||
|
with open('$ASAHI_DIR/installer_data.json') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
for p in data['os_list'][0]['partitions']:
|
||||||
|
size = p.get('size', '?')
|
||||||
|
expand = ' (expand)' if p.get('expand') else ''
|
||||||
|
image = f\" [{p['image']}]\" if 'image' in p else ''
|
||||||
|
print(f\" {p['name']:8s} {p['type']:8s} {size:>16s}{expand}{image}\")
|
||||||
|
"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Build complete!"
|
||||||
|
echo ""
|
||||||
|
echo " Package: $ASAHI_DIR/fedora-asahi-lab.zip"
|
||||||
|
echo " Config: $ASAHI_DIR/installer_data.json"
|
||||||
|
echo ""
|
||||||
|
echo " To serve from bastion, copy to the bastion's HTTP directory"
|
||||||
|
echo " or configure REPO_BASE to point here."
|
||||||
|
echo ""
|
||||||
|
echo " To install on Mac Studio:"
|
||||||
|
echo " curl http://$BASTION_IP:$HTTP_PORT/asahi | sh"
|
||||||
@@ -1,14 +1,47 @@
|
|||||||
// Routes for Asahi Linux provisioning.
|
// Routes for Asahi Linux provisioning.
|
||||||
// GET /asahi — wrapper script (curl https://bastion:8080/asahi | sh)
|
// GET /asahi — wrapper script (curl bastion:8080/asahi | sh)
|
||||||
// GET /asahi/installer_data.json — custom installer config with LVM partition layout
|
// GET /asahi/installer_data.json — custom installer config (built or fallback)
|
||||||
// GET /asahi/firstboot.sh — first-boot LVM setup script (for manual use)
|
// 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 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 type { BastionConfig } from "@lab/shared";
|
||||||
import { renderFirstbootScript, renderFirstbootUnit } from "../templates/asahi-firstboot.sh.js";
|
import { renderFirstbootScript, renderFirstbootUnit } from "../templates/asahi-firstboot.sh.js";
|
||||||
import type { Role } from "@lab/shared";
|
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 {
|
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
|
// Wrapper script — user runs: curl http://bastion:8080/asahi | sh
|
||||||
app.get("/asahi", async (_request, reply) => {
|
app.get("/asahi", async (_request, reply) => {
|
||||||
const script = `#!/bin/bash
|
const script = `#!/bin/bash
|
||||||
@@ -49,14 +82,14 @@ tar xf "installer-\${PKG_VER}.tar.gz"
|
|||||||
|
|
||||||
# Point to our custom installer_data.json + rootfs repo
|
# Point to our custom installer_data.json + rootfs repo
|
||||||
export INSTALLER_DATA="\${BASTION}/asahi/installer_data.json"
|
export INSTALLER_DATA="\${BASTION}/asahi/installer_data.json"
|
||||||
export REPO_BASE="\${BASTION}/asahi/repo"
|
export REPO_BASE="\${BASTION}/asahi/repo/"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " Using custom installer data from bastion."
|
echo " Using custom installer data from bastion."
|
||||||
echo " This will create:"
|
echo " This will create:"
|
||||||
echo " - Standard Asahi boot infrastructure (m1n1 + U-Boot)"
|
echo " - Standard Asahi boot infrastructure (m1n1 + U-Boot)"
|
||||||
echo " - 60GB root partition (Fedora Asahi Remix)"
|
echo " - Fedora Asahi Remix root partition"
|
||||||
echo " - Remaining space as LVM data partition"
|
echo " - LVM data partition (remaining space)"
|
||||||
echo ""
|
echo ""
|
||||||
echo " On first boot, LVM volumes will be created automatically:"
|
echo " On first boot, LVM volumes will be created automatically:"
|
||||||
echo " labvg/var (100GB), labvg/varlog (10GB), labvg/home (10GB),"
|
echo " labvg/var (100GB), labvg/varlog (10GB), labvg/home (10GB),"
|
||||||
@@ -74,48 +107,34 @@ fi
|
|||||||
return reply.type("text/x-shellscript").send(script);
|
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) => {
|
app.get("/asahi/installer_data.json", async (_request, reply) => {
|
||||||
// This follows the Asahi installer_data.json schema.
|
// Prefer the built installer_data.json (from build-asahi-rootfs.sh)
|
||||||
// We define a fixed-size root and an expanding data partition for LVM.
|
if (repoDir) {
|
||||||
const data = {
|
const builtConfig = join(repoDir, "installer_data.json");
|
||||||
os_list: [
|
if (existsSync(builtConfig)) {
|
||||||
{
|
const data = JSON.parse(readFileSync(builtConfig, "utf-8"));
|
||||||
name: "Fedora Asahi Lab",
|
return reply.type("application/json").send(data);
|
||||||
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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
// First-boot script — for manual download or embedding in rootfs
|
||||||
@@ -125,17 +144,14 @@ fi
|
|||||||
const hostname = request.query.hostname ?? "mac-studio";
|
const hostname = request.query.hostname ?? "mac-studio";
|
||||||
const role = (request.query.role ?? "infra") as Role;
|
const role = (request.query.role ?? "infra") as Role;
|
||||||
const mac = request.query.mac ?? "unknown";
|
const mac = request.query.mac ?? "unknown";
|
||||||
const user = request.query.user ?? "michal";
|
const user = request.query.user ?? config.adminUser;
|
||||||
|
|
||||||
// Read SSH keys from bastion config
|
|
||||||
const sshKeys = config.sshKeys ?? [];
|
|
||||||
|
|
||||||
const script = renderFirstbootScript({
|
const script = renderFirstbootScript({
|
||||||
hostname,
|
hostname,
|
||||||
role,
|
role,
|
||||||
serverIp: config.serverIp,
|
serverIp: config.serverIp,
|
||||||
httpPort: config.httpPort,
|
httpPort: config.httpPort,
|
||||||
sshKeys,
|
sshKeys: config.sshKeys ?? [],
|
||||||
adminUser: user,
|
adminUser: user,
|
||||||
mac,
|
mac,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,14 +75,14 @@ describe("asahi routes", () => {
|
|||||||
|
|
||||||
expect(data.os_list).toHaveLength(1);
|
expect(data.os_list).toHaveLength(1);
|
||||||
const os = data.os_list[0];
|
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
|
// Three partitions: EFI + Root + Data
|
||||||
expect(os.partitions).toHaveLength(3);
|
expect(os.partitions).toHaveLength(3);
|
||||||
expect(os.partitions[0].type).toBe("EFI");
|
expect(os.partitions[0].type).toBe("EFI");
|
||||||
expect(os.partitions[1].type).toBe("Linux");
|
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].expand).toBe(false);
|
||||||
|
expect(os.partitions[1].image).toBe("root.img");
|
||||||
expect(os.partitions[2].type).toBe("Linux");
|
expect(os.partitions[2].type).toBe("Linux");
|
||||||
expect(os.partitions[2].expand).toBe(true);
|
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)
|
// Wait for the server to start (look for the banner)
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const maxWait = 10_000;
|
const maxWait = 15_000;
|
||||||
while (Date.now() - startedAt < maxWait) {
|
while (Date.now() - startedAt < maxWait) {
|
||||||
if (stdout.includes("Waiting for PXE boot requests")) break;
|
if (stdout.includes("Waiting for PXE boot requests")) break;
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
|
|||||||
Reference in New Issue
Block a user