ops: add CI boot-image releases and Unifi PXE rollout
Some checks failed
ci-gates / checks (push) Has been cancelled
deploy-cloudflare / checks (push) Has been cancelled
deploy-cloudflare / deploy (push) Has been cancelled

This commit is contained in:
every.channel 2026-02-28 22:53:59 -08:00
parent 043b1730dc
commit be26313225
No known key found for this signature in database
9 changed files with 720 additions and 0 deletions

View file

@ -0,0 +1,276 @@
name: deploy-runner-images
on:
push:
tags: [boot-v*]
workflow_dispatch:
inputs:
release_tag:
description: "Release tag override (manual runs only)"
required: false
default: ""
publish_release:
description: "Publish artifacts to Forgejo release (true/false)"
required: false
default: "true"
build_x86_64_netboot:
description: "Build x86_64 netboot tarball (true/false)"
required: false
default: "true"
build_x86_64_iso:
description: "Build x86_64 installer ISO (true/false)"
required: false
default: "true"
concurrency:
group: runner-image-deploy-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-release:
if: ${{ github.server_url != 'https://codeberg.org' }}
runs-on: codeberg-medium-lazy
steps:
- name: Fetch source (no git required)
env:
GITHUB_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
if [[ -z "${GITHUB_TOKEN:-}" ]]; then
echo "error: missing github.token"
exit 2
fi
if ! command -v curl >/dev/null 2>&1; then
echo "error: curl is required"
exit 2
fi
if ! command -v tar >/dev/null 2>&1; then
echo "error: tar is required"
exit 2
fi
if [[ -z "${GITHUB_SHA:-}" ]]; then
echo "error: missing GITHUB_SHA"
exit 2
fi
if [[ -z "${GITHUB_SERVER_URL:-}" ]]; then
echo "error: missing GITHUB_SERVER_URL"
exit 2
fi
if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then
echo "error: missing GITHUB_REPOSITORY"
exit 2
fi
rm -rf .repo
mkdir -p .repo
curl -fsSL -H "Authorization: token ${GITHUB_TOKEN}" \
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/archive/${GITHUB_SHA}.tar.gz?rev=${GITHUB_SHA}" \
-o .repo/src.tgz
tar -xzf .repo/src.tgz -C .repo --strip-components=1
rm -f .repo/src.tgz
- name: Bootstrap Nix
shell: bash
run: |
set -euo pipefail
if ! command -v nix >/dev/null 2>&1; then
curl -fsSL https://nixos.org/nix/install -o /tmp/install-nix.sh
sh /tmp/install-nix.sh --no-daemon --yes
fi
if [[ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]]; then
# shellcheck disable=SC1091
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
fi
if [[ -d "$HOME/.nix-profile/bin" ]]; then
echo "PATH=$HOME/.nix-profile/bin:$PATH" >> "$GITHUB_ENV"
export PATH="$HOME/.nix-profile/bin:$PATH"
fi
nix --version
- name: Resolve build plan
id: plan
env:
INPUT_RELEASE_TAG: ${{ github.event.inputs.release_tag }}
INPUT_PUBLISH_RELEASE: ${{ github.event.inputs.publish_release }}
INPUT_BUILD_X86_64_NETBOOT: ${{ github.event.inputs.build_x86_64_netboot }}
INPUT_BUILD_X86_64_ISO: ${{ github.event.inputs.build_x86_64_iso }}
shell: bash
run: |
set -euo pipefail
bool_norm() {
local raw
raw="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')"
case "${raw}" in
''|true|1|yes|y|on) echo "true" ;;
false|0|no|n|off) echo "false" ;;
*)
echo "error: invalid boolean value '${1}'" >&2
exit 2
;;
esac
}
short_sha="${GITHUB_SHA:0:12}"
if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then
release_tag="${GITHUB_REF#refs/tags/}"
else
release_tag="${INPUT_RELEASE_TAG:-}"
if [[ -z "${release_tag}" ]]; then
release_tag="boot-${short_sha}"
fi
fi
if [[ ! "${release_tag}" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "error: release tag contains unsupported characters: ${release_tag}" >&2
exit 2
fi
publish_release="$(bool_norm "${INPUT_PUBLISH_RELEASE:-true}")"
build_x86_64_netboot="$(bool_norm "${INPUT_BUILD_X86_64_NETBOOT:-true}")"
build_x86_64_iso="$(bool_norm "${INPUT_BUILD_X86_64_ISO:-true}")"
if [[ "${build_x86_64_netboot}" != "true" && "${build_x86_64_iso}" != "true" ]]; then
echo "error: at least one image build must be enabled" >&2
exit 2
fi
artifact_suffix="${short_sha}-${GITHUB_RUN_NUMBER:-0}-${GITHUB_RUN_ATTEMPT:-1}"
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
echo "publish_release=${publish_release}" >> "$GITHUB_OUTPUT"
echo "build_x86_64_netboot=${build_x86_64_netboot}" >> "$GITHUB_OUTPUT"
echo "build_x86_64_iso=${build_x86_64_iso}" >> "$GITHUB_OUTPUT"
echo "artifact_suffix=${artifact_suffix}" >> "$GITHUB_OUTPUT"
- name: Build runner boot images
env:
BUILD_X86_64_NETBOOT: ${{ steps.plan.outputs.build_x86_64_netboot }}
BUILD_X86_64_ISO: ${{ steps.plan.outputs.build_x86_64_iso }}
ARTIFACT_SUFFIX: ${{ steps.plan.outputs.artifact_suffix }}
shell: bash
run: |
set -euo pipefail
cd .repo
if [[ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]]; then
# shellcheck disable=SC1091
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
fi
export PATH="$HOME/.nix-profile/bin:$PATH"
artifacts_dir="$PWD/.artifacts"
rm -rf "${artifacts_dir}"
mkdir -p "${artifacts_dir}"
nix_args=(--accept-flake-config --extra-experimental-features "nix-command flakes")
if [[ "${BUILD_X86_64_NETBOOT}" == "true" ]]; then
nix build "${nix_args[@]}" \
.#nixosConfigurations.ec-runner-x86_64-netboot.config.system.build.netboot \
-o result-netboot-x86_64
tar -C result-netboot-x86_64 \
-czf "${artifacts_dir}/ec-runner-x86_64-netboot-${ARTIFACT_SUFFIX}.tar.gz" \
kernel initrd netboot.ipxe
fi
if [[ "${BUILD_X86_64_ISO}" == "true" ]]; then
nix build "${nix_args[@]}" \
.#nixosConfigurations.ec-runner-x86_64-iso.config.system.build.isoImage \
-o result-iso-x86_64
iso_source=""
if [[ -f result-iso-x86_64 ]]; then
iso_source="result-iso-x86_64"
else
iso_source="$(find -L result-iso-x86_64 -type f -name '*.iso' | head -n 1 || true)"
fi
if [[ -z "${iso_source}" ]]; then
echo "error: could not locate ISO output from result-iso-x86_64" >&2
exit 2
fi
cp -f "${iso_source}" "${artifacts_dir}/ec-runner-x86_64-iso-${ARTIFACT_SUFFIX}.iso"
fi
if ! find "${artifacts_dir}" -maxdepth 1 -type f | grep -q .; then
echo "error: no image artifacts were produced" >&2
exit 2
fi
(
cd "${artifacts_dir}"
sha256sum -- * > SHA256SUMS.txt
ls -lh
)
- name: Publish artifacts to Forgejo release
if: ${{ steps.plan.outputs.publish_release == 'true' }}
env:
GITHUB_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ steps.plan.outputs.release_tag }}
shell: bash
run: |
set -euo pipefail
cd .repo
if [[ -z "${GITHUB_TOKEN:-}" ]]; then
echo "error: missing github.token"
exit 2
fi
if [[ -z "${GITHUB_SERVER_URL:-}" ]]; then
echo "error: missing GITHUB_SERVER_URL"
exit 2
fi
if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then
echo "error: missing GITHUB_REPOSITORY"
exit 2
fi
api_base="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
release_json="$(curl -fsSL \
-H "Authorization: token ${GITHUB_TOKEN}" \
"${api_base}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)"
if [[ -z "${release_json}" ]]; then
payload="$(cat <<JSON
{
"tag_name": "${RELEASE_TAG}",
"name": "Boot images ${RELEASE_TAG}",
"body": "Automated runner boot image build from ${GITHUB_SHA}.",
"draft": false,
"prerelease": false
}
JSON
)"
release_json="$(curl -fsSL -X POST \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "content-type: application/json" \
"${api_base}/releases" \
-d "${payload}")"
fi
release_id=""
if command -v python3 >/dev/null 2>&1; then
release_id="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"${release_json}" 2>/dev/null || true)"
fi
if [[ -z "${release_id}" ]]; then
release_id="$(printf '%s' "${release_json}" \
| sed -nE 's/.*"id"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' \
| head -n 1)"
fi
if [[ -z "${release_id}" ]]; then
echo "error: failed to resolve release id for ${RELEASE_TAG}" >&2
exit 2
fi
for asset_path in .artifacts/*; do
[[ -f "${asset_path}" ]] || continue
asset_name="$(basename "${asset_path}")"
curl -fsSL -X POST \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "content-type: application/octet-stream" \
--data-binary @"${asset_path}" \
"${api_base}/releases/${release_id}/assets?name=${asset_name}" >/dev/null
echo "uploaded: ${asset_name}"
done

View file

@ -53,6 +53,12 @@ Git hosting topology:
cat docs/GIT_HOSTING.md
```
NUC PXE rollout (Unifi + ProxyDHCP):
```sh
cat docs/NUC_UNIFI_NETBOOT.md
```
## WebTransport Watch (MoQ)
Publish (node -> Cloudflare relay):

102
docs/NUC_UNIFI_NETBOOT.md Normal file
View file

@ -0,0 +1,102 @@
# NUC Fleet Netboot (Unifi + ProxyDHCP)
This runbook provisions x86_64 NUCs from runner netboot artifacts without USB image flashing.
It uses:
- Unifi DHCP for IP leases.
- Local `dnsmasq` ProxyDHCP for PXE/iPXE bootfile logic.
- Local HTTP + TFTP service for boot artifacts.
## Why ProxyDHCP
iPXE commonly needs two boot stages:
1. firmware PXE -> `ipxe.efi`
2. iPXE -> `netboot.ipxe`
If DHCP always returns `ipxe.efi`, clients can loop forever. ProxyDHCP handles stage-specific boot responses cleanly while leaving Unifi as the DHCP lease server.
## Prerequisites
- A Linux boot server on the same VLAN/L2 domain as the NUCs.
- Unifi network with normal DHCP enabled.
- Local DNS record on that VLAN: `boot.every.channel -> <boot-server-ip>`.
- `curl`, `tar`, `python3`, `dnsmasq` installed on the boot server.
- Runner netboot artifact already published to Forgejo Releases (or available as a local tarball).
## 1) Stage artifacts
From repository root on the boot server:
```sh
./scripts/netboot-stage.sh
```
Optional inputs:
- `EVERY_CHANNEL_NETBOOT_RELEASE_TAG=boot-v2026.02.28`
- `EVERY_CHANNEL_NETBOOT_TARBALL=/path/to/ec-runner-x86_64-netboot-....tar.gz`
- `EVERY_CHANNEL_FORGE_TOKEN=<token>` for private releases
- `EVERY_CHANNEL_NETBOOT_HOSTNAME=boot.every.channel`
This stages:
- `tmp/netboot/http/{kernel,initrd,netboot.ipxe}`
- `tmp/netboot/tftp/ipxe.efi`
## 2) Serve HTTP + TFTP + ProxyDHCP
Example (replace values for your VLAN):
```sh
sudo \
EVERY_CHANNEL_NETBOOT_LISTEN_IP=10.20.30.2 \
EVERY_CHANNEL_NETBOOT_INTERFACE=eth0 \
EVERY_CHANNEL_NETBOOT_PROXY_SUBNET=10.20.30.0/24 \
EVERY_CHANNEL_NETBOOT_HOSTNAME=boot.every.channel \
./scripts/netboot-serve.sh
```
Notes:
- Keep this process running during provisioning.
- Do not set Unifi DHCP bootfile options while this proxy mode is active.
- Ensure `boot.every.channel` resolves to the boot server IP from NUC clients.
## 3) Unifi / NUC settings
Unifi:
- Keep DHCP enabled for the provisioning VLAN.
- Leave DHCP boot/TFTP overrides unset when using `netboot-serve.sh`.
- Create/verify local DNS host override: `boot.every.channel -> <boot-server-ip>`.
NUC BIOS:
- Enable UEFI network boot (IPv4 PXE).
- Disable Legacy/CSM if possible.
- Put network boot before disk for first install cycle.
## 4) Provision the fleet
1. Boot each NUC on the provisioning VLAN.
2. PXE will chainload into iPXE and then runner `netboot.ipxe`.
3. Complete install/bootstrap flow on each node.
4. After successful install, switch boot order back to local disk.
## Troubleshooting
- Symptom: iPXE loop (`ipxe.efi` repeatedly)
- Cause: static DHCP bootfile without iPXE-aware logic.
- Fix: use ProxyDHCP flow (`netboot-serve.sh`) or set conditional DHCP rules.
- Symptom: NUC gets IP but never downloads boot artifacts
- Verify firewall allows UDP 67/68, UDP 69, and TCP 8080 between NUCs and boot server.
- Symptom: no `dnsmasq` offers seen
- Verify `EVERY_CHANNEL_NETBOOT_INTERFACE` and `EVERY_CHANNEL_NETBOOT_PROXY_SUBNET`.
## Security / networking
- Tailscale is not required for provisioning.
- Keep the provisioning VLAN isolated from regular clients.
- Stop `netboot-serve.sh` when rollout is complete.

View file

@ -50,6 +50,36 @@ Build an aarch64 SD image:
nix build .#nixosConfigurations.ec-runner-aarch64-sdimage.config.system.build.sdImage
```
## CI Deploy (Forgejo Releases)
Boot images can be built and published from CI via:
- `.forgejo/workflows/deploy-runner-images.yml`
Triggers:
- Manual: `workflow_dispatch`
- Tags: `boot-v*` (for example `boot-v2026.02.28`)
Manual inputs (all optional):
- `release_tag` (defaults to `boot-<short-sha>`)
- `publish_release` (`true`/`false`, default `true`)
- `build_x86_64_netboot` (`true`/`false`, default `true`)
- `build_x86_64_iso` (`true`/`false`, default `true`)
Published assets are attached to the resolved Forgejo release tag and include:
- x86_64 netboot bundle (`kernel`, `initrd`, `netboot.ipxe`) as `.tar.gz`
- x86_64 installer `.iso`
- `SHA256SUMS.txt`
Notes:
- CI image publish is disabled on the Codeberg mirror host.
- Current CI scope is x86_64 targets; aarch64 image builds remain local/manual unless an aarch64-capable runner is added.
- For multi-NUC PXE rollout on Unifi networks, use `docs/NUC_UNIFI_NETBOOT.md`.
## Outputs
After building, artifacts will be in `./result` (a symlink into the Nix store).

View file

@ -0,0 +1,35 @@
# ECP-0081: CI Boot Image Deployment to Forgejo Releases
Status: Implemented
## Context
Runner boot artifacts (netboot/ISO/SD) are currently built by hand from local Linux/Nix environments. That slows repeatable infra rollout and weakens traceability from commit to runnable images.
The constitution requires infrastructure definitions to live in-repo and stay independently operable. Boot image publication should follow the same Forgejo-primary CI model used for web deploys, while mirror hosts remain distribution-only.
## Decision
1. Add a dedicated Forgejo Actions workflow to build runner boot images from flake outputs in CI.
2. Trigger this workflow on:
- manual dispatch (`workflow_dispatch`), and
- release-style tags (`boot-v*`).
3. Publish built artifacts to Forgejo Releases on the primary host using repository-scoped API calls and the workflow token.
4. Exclude Codeberg mirror runs using the existing server guard (`github.server_url != 'https://codeberg.org'`).
5. Start with x86_64 image targets in CI (`netboot`, `iso`) to keep runtime/runner requirements explicit and reversible.
## Alternatives considered
- Keep boot image builds fully manual. Rejected because operator discipline alone does not provide consistent provenance.
- Push images to external object storage first. Rejected for now because Forgejo Releases are already part of the controlled primary platform.
- Build all architectures in one CI pass immediately. Rejected because runner architecture availability is not guaranteed and would make initial rollout brittle.
## Rollout / teardown plan
- Rollout:
- merge workflow + docs,
- run a manual dispatch to verify artifact publication,
- optionally create `boot-v*` tags for versioned image drops.
- Teardown:
- disable or delete the boot-image workflow,
- continue using local/manual `nix build` paths from `docs/RUNNER_IMAGES.md`.

View file

@ -0,0 +1,36 @@
# ECP-0082: Unifi PXE Rollout Path for Runner Images
Status: Implemented
## Context
Runner netboot artifacts now publish from CI, but there is no repository-native operating path for fleet provisioning on common prosumer networks (for example Unifi VLANs).
Unifi DHCP can expose next-server/bootfile settings, but iPXE chainloading often requires conditional bootfile behavior to avoid loops (`ipxe.efi` first stage, script second stage). Not all controller setups expose that cleanly.
## Decision
1. Add first-party scripts for local netboot staging and serving:
- stage x86_64 netboot artifacts from Forgejo Releases (or local tarball),
- stage iPXE UEFI binary for TFTP,
- run HTTP + TFTP + ProxyDHCP via `dnsmasq` for deterministic chainloading.
2. Keep Unifi DHCP as the IP authority; use ProxyDHCP only to supply bootfile logic.
3. Document a concrete NUC rollout sequence for same-VLAN provisioning.
4. Keep dependencies minimal (`curl`, `tar`, `python3`, `dnsmasq`) and avoid requiring image flashing workflows.
## Alternatives considered
- Require Unifi DHCP conditional iPXE rules. Rejected because controller capabilities vary and misconfiguration risks boot loops.
- Keep manual USB-only provisioning. Rejected because it increases labor for multi-node rollout.
- Add a heavy provisioning stack (MAAS/Foreman/Kickstart integration). Rejected as too much operational overhead for current scale.
## Rollout / teardown plan
- Rollout:
- merge scripts/docs,
- run `netboot-stage` on the boot server,
- run `netboot-serve` on the NUC VLAN and boot hosts via PXE.
- Teardown:
- stop `netboot-serve`,
- remove staged artifacts under `tmp/netboot`,
- continue with ISO+USB fallback path.

View file

@ -12,6 +12,12 @@ git-hosting:
git-mirrors:
./scripts/git-push-mirrors.sh
netboot-stage:
./scripts/netboot-stage.sh
netboot-serve:
./scripts/netboot-serve.sh
test-core:
cargo test -p ec-core -p ec-crypto -p ec-moq -p ec-iroh -p ec-linux-iptv

99
scripts/netboot-serve.sh Executable file
View file

@ -0,0 +1,99 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${root}"
netboot_root="${EVERY_CHANNEL_NETBOOT_ROOT:-tmp/netboot}"
http_dir="${netboot_root}/http"
tftp_dir="${netboot_root}/tftp"
listen_ip="${EVERY_CHANNEL_NETBOOT_LISTEN_IP:-}"
interface_name="${EVERY_CHANNEL_NETBOOT_INTERFACE:-}"
proxy_subnet="${EVERY_CHANNEL_NETBOOT_PROXY_SUBNET:-}"
netboot_hostname="${EVERY_CHANNEL_NETBOOT_HOSTNAME:-}"
http_port="${EVERY_CHANNEL_NETBOOT_HTTP_PORT:-8080}"
dnsmasq_port="${EVERY_CHANNEL_NETBOOT_DNS_PORT:-0}"
need_cmd() {
local name="$1"
if ! command -v "${name}" >/dev/null 2>&1; then
echo "error: required command not found: ${name}" >&2
exit 2
fi
}
need_cmd dnsmasq
need_cmd python3
if [[ "$(id -u)" -ne 0 ]]; then
echo "error: netboot-serve requires root (TFTP + ProxyDHCP ports)." >&2
echo "hint: run with sudo and pass env vars, for example:" >&2
echo " sudo EVERY_CHANNEL_NETBOOT_LISTEN_IP=10.20.30.2 EVERY_CHANNEL_NETBOOT_INTERFACE=eth0 EVERY_CHANNEL_NETBOOT_PROXY_SUBNET=10.20.30.0/24 EVERY_CHANNEL_NETBOOT_HOSTNAME=boot.every.channel ./scripts/netboot-serve.sh" >&2
exit 2
fi
if [[ -z "${listen_ip}" ]]; then
echo "error: set EVERY_CHANNEL_NETBOOT_LISTEN_IP (boot server IP on NUC VLAN)" >&2
exit 2
fi
if [[ -z "${interface_name}" ]]; then
echo "error: set EVERY_CHANNEL_NETBOOT_INTERFACE (interface on NUC VLAN)" >&2
exit 2
fi
if [[ -z "${proxy_subnet}" ]]; then
echo "error: set EVERY_CHANNEL_NETBOOT_PROXY_SUBNET (for example 10.20.30.0/24)" >&2
exit 2
fi
if [[ -z "${netboot_hostname}" ]]; then
netboot_hostname="${listen_ip}"
fi
for required in "${http_dir}/kernel" "${http_dir}/initrd" "${http_dir}/netboot.ipxe" "${tftp_dir}/ipxe.efi"; do
if [[ ! -f "${required}" ]]; then
echo "error: missing required staged file: ${required}" >&2
echo "hint: run ./scripts/netboot-stage.sh first" >&2
exit 2
fi
done
run_dir="$(mktemp -d)"
cleanup() {
if [[ -n "${http_pid:-}" ]] && kill -0 "${http_pid}" >/dev/null 2>&1; then
kill "${http_pid}" >/dev/null 2>&1 || true
wait "${http_pid}" 2>/dev/null || true
fi
rm -rf "${run_dir}"
}
trap cleanup EXIT INT TERM
cat > "${run_dir}/dnsmasq.conf" <<EOF
port=${dnsmasq_port}
bind-interfaces
interface=${interface_name}
listen-address=${listen_ip}
log-dhcp
enable-tftp
tftp-root=${tftp_dir}
dhcp-range=${proxy_subnet},proxy
dhcp-userclass=set:ipxe,iPXE
dhcp-match=set:efi64,option:client-arch,7
dhcp-match=set:efi64,option:client-arch,9
dhcp-option=66,${netboot_hostname}
dhcp-boot=tag:!ipxe,tag:efi64,ipxe.efi
dhcp-boot=tag:ipxe,tag:efi64,http://${netboot_hostname}:${http_port}/netboot.ipxe
dhcp-boot=tag:!ipxe,ipxe.efi
dhcp-boot=tag:ipxe,http://${netboot_hostname}:${http_port}/netboot.ipxe
EOF
python3 -m http.server "${http_port}" --bind "${listen_ip}" --directory "${http_dir}" >/tmp/every-channel-netboot-http.log 2>&1 &
http_pid="$!"
echo "ok: HTTP serving ${http_dir} on http://${listen_ip}:${http_port}/"
echo "ok: advertised netboot host: ${netboot_hostname}"
echo "ok: TFTP serving ${tftp_dir} on ${listen_ip}:69"
echo "ok: ProxyDHCP active for ${proxy_subnet} on interface ${interface_name}"
echo "ok: Use normal Unifi DHCP for IP assignment; do not configure Unifi DHCP bootfile while proxy mode is active."
echo
echo "Press Ctrl+C to stop."
dnsmasq --no-daemon --conf-file="${run_dir}/dnsmasq.conf"

130
scripts/netboot-stage.sh Executable file
View file

@ -0,0 +1,130 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${root}"
forge_host="${EVERY_CHANNEL_FORGE_HOST:-https://forge.every.channel}"
forge_repo="${EVERY_CHANNEL_FORGE_REPO:-every-channel/every.channel}"
release_tag="${EVERY_CHANNEL_NETBOOT_RELEASE_TAG:-}"
local_tarball="${EVERY_CHANNEL_NETBOOT_TARBALL:-}"
out_root="${EVERY_CHANNEL_NETBOOT_ROOT:-tmp/netboot}"
ipxe_efi_url="${EVERY_CHANNEL_IPXE_EFI_URL:-https://boot.ipxe.org/snponly.efi}"
netboot_hostname="${EVERY_CHANNEL_NETBOOT_HOSTNAME:-boot.every.channel}"
http_port="${EVERY_CHANNEL_NETBOOT_HTTP_PORT:-8080}"
token="${EVERY_CHANNEL_FORGE_TOKEN:-${FORGE_TOKEN:-${CODEBERG_TOKEN:-}}}"
need_cmd() {
local name="$1"
if ! command -v "${name}" >/dev/null 2>&1; then
echo "error: required command not found: ${name}" >&2
exit 2
fi
}
need_cmd curl
need_cmd tar
need_cmd python3
tmp_dir="$(mktemp -d)"
cleanup() {
rm -rf "${tmp_dir}"
}
trap cleanup EXIT
archive_path="${tmp_dir}/netboot.tar.gz"
release_asset_url=""
if [[ -n "${local_tarball}" ]]; then
if [[ ! -f "${local_tarball}" ]]; then
echo "error: netboot tarball not found: ${local_tarball}" >&2
exit 2
fi
cp -f "${local_tarball}" "${archive_path}"
else
api_base="${forge_host%/}/api/v1/repos/${forge_repo}"
release_endpoint="${api_base}/releases/latest"
if [[ -n "${release_tag}" ]]; then
release_endpoint="${api_base}/releases/tags/${release_tag}"
fi
auth_args=()
if [[ -n "${token}" ]]; then
auth_args=(-H "Authorization: token ${token}")
fi
release_json="${tmp_dir}/release.json"
curl -fsSL "${auth_args[@]}" "${release_endpoint}" -o "${release_json}"
release_asset_url="$(
python3 - "${release_json}" <<'PY'
import json
import sys
path = sys.argv[1]
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
assets = data.get("assets", [])
candidates = []
for asset in assets:
name = asset.get("name", "")
if name.startswith("ec-runner-x86_64-netboot-") and name.endswith(".tar.gz"):
candidates.append(asset)
if not candidates:
sys.exit(1)
# Pick newest by release ordering if API already sorted; otherwise prefer largest id.
chosen = sorted(candidates, key=lambda x: x.get("id", 0))[-1]
print(chosen.get("browser_download_url", ""))
PY
)"
if [[ -z "${release_asset_url}" ]]; then
echo "error: unable to find x86_64 netboot asset in release" >&2
exit 2
fi
curl -fsSL "${auth_args[@]}" -o "${archive_path}" "${release_asset_url}"
fi
http_dir="${out_root}/http"
tftp_dir="${out_root}/tftp"
rm -rf "${http_dir}"
mkdir -p "${http_dir}" "${tftp_dir}"
tar -xzf "${archive_path}" -C "${http_dir}"
for required in kernel initrd netboot.ipxe; do
if [[ ! -f "${http_dir}/${required}" ]]; then
echo "error: extracted netboot bundle is missing ${required}" >&2
exit 2
fi
done
curl -fsSL -o "${tftp_dir}/ipxe.efi" "${ipxe_efi_url}"
cp -f "${http_dir}/netboot.ipxe" "${tftp_dir}/netboot.ipxe"
cat > "${tftp_dir}/bootstrap.ipxe" <<'EOF'
#!ipxe
dhcp
chain http://__NETBOOT_HOST__:__HTTP_PORT__/netboot.ipxe
EOF
sed -i.bak \
-e "s#__NETBOOT_HOST__#${netboot_hostname}#g" \
-e "s#__HTTP_PORT__#${http_port}#g" \
"${tftp_dir}/bootstrap.ipxe"
rm -f "${tftp_dir}/bootstrap.ipxe.bak"
echo "ok: staged netboot content"
echo "ok: http root: ${http_dir}"
echo "ok: tftp root: ${tftp_dir}"
echo "ok: netboot hostname: ${netboot_hostname}"
echo "ok: netboot http port: ${http_port}"
if [[ -n "${release_asset_url}" ]]; then
echo "ok: source asset: ${release_asset_url}"
else
echo "ok: source asset: ${local_tarball}"
fi
echo "hint: run sudo ./scripts/netboot-serve.sh to expose HTTP+TFTP+ProxyDHCP"