From a535debf2e97c0d18d761b8263a3e618d2576ecc Mon Sep 17 00:00:00 2001 From: Daniel Maksymilian Syrnicki Date: Mon, 13 Apr 2026 23:55:58 +0200 Subject: [PATCH] feat: walking-skeleton live ISO that boots into the Flask wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iso/build.sh runs mkarchiso inside a privileged archlinux container, overlays our customizations onto Arch's stock releng profile (systemd unit that launches Flask on 0.0.0.0:5000, the webinstaller under /opt/furtka, extra packages for python/flask/avahi), and drops a hybrid BIOS/UEFI ISO in iso/out/. Verified end to end: Proxmox VM (OVMF, Secure Boot off) boots the ISO, DHCP's onto the LAN, and serves screens 1-3 of the existing wizard at http://:5000/install/step1. This is the first point at which Furtka is something you can run instead of something you can read about. Two known drive-list bugs surfaced while testing (/dev/loop0 and /dev/sr0 appear as install targets) — captured in the README roadmap. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 + CHANGELOG.md | 1 + README.md | 8 ++- iso/README.md | 52 ++++++++++++++++ iso/build.sh | 62 +++++++++++++++++++ .../system/furtka-webinstaller.service | 14 +++++ .../furtka-webinstaller.service | 1 + iso/overlay/packages.extra | 4 ++ iso/overlay/profiledef.sh | 9 +++ 9 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 iso/README.md create mode 100755 iso/build.sh create mode 100644 iso/overlay/airootfs/etc/systemd/system/furtka-webinstaller.service create mode 120000 iso/overlay/airootfs/etc/systemd/system/multi-user.target.wants/furtka-webinstaller.service create mode 100644 iso/overlay/packages.extra create mode 100644 iso/overlay/profiledef.sh diff --git a/.gitignore b/.gitignore index f1078e1..a51fd35 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__/ # Real credentials must never be committed — use the .example files archinstall/user_credentials.json +iso/out/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 751de03..4a79e96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This project uses calendar versioning: `YY.N-stage` (e.g. `26.0-alpha` = 2026, r ### Added - **Forgejo Actions runner** live on Proxmox VM (`forge-runner-01`, Ubuntu 24.04) with DinD sidecar — CI green end-to-end. Setup scripts in `ops/forgejo-runner/`. +- **Walking-skeleton live ISO** (`iso/build.sh`). Overlays an Arch `releng` profile with Flask + the webinstaller, bakes a systemd unit that auto-starts the wizard on boot, produces a hybrid BIOS/UEFI ISO via `mkarchiso` in a privileged `archlinux:latest` container. Tested booting under OVMF in Proxmox — wizard screens 1–3 respond at `http://:5000`. ## [26.0-alpha] - 2026-04-13 diff --git a/README.md b/README.md index 07441a4..ecf1fdb 100644 --- a/README.md +++ b/README.md @@ -106,8 +106,12 @@ None of these nail the "your dad can set this up" experience. The installer wiza - [x] Release process + CI — CalVer tags, conventional commits, Forgejo Actions (ruff, pytest, JSON, link checks), `26.0-alpha` tagged - [x] Forgejo runner live on Proxmox VM (`forge-runner-01`, Ubuntu 24.04, Docker + DinD sidecar) — setup captured in [docs/runner-setup.md](docs/runner-setup.md) + [ops/forgejo-runner/](ops/forgejo-runner/) - [ ] **Publish `26.0-alpha` Forgejo Release** — [releases/new](https://forgejo.sourcegate.online/daniel/furtka/releases/new), paste CHANGELOG section, tick Pre-release *(Daniel, next session)* -- [ ] **Base OS bootable image** — Robert gets a minimal Arch image that boots, runs Docker, serves the installer webapp at `https://proksi.local` *(next blocker)* -- [ ] Installer wizard screens S5–S8 (domain, SSL, diagnostic, confirm) +- [x] **Walking-skeleton live ISO** — `iso/build.sh` produces a hybrid BIOS/UEFI Arch-based ISO that boots in a Proxmox VM, DHCP's onto the LAN, and serves the Flask webinstaller on `:5000`. Screens 1–3 work end-to-end. Build infra in [`iso/`](iso/). +- [ ] **Drop /dev/loop0 + /dev/sr0 from drive list** — the live ISO's own squashfs and the CD-ROM both show up as install targets. Simple filter in `webinstaller/drives.py`. +- [ ] **Rebrand GRUB menu** — the ISO still boots as "Arch Linux install medium". Cosmetic, fix when we start caring about end-user-facing polish. +- [ ] **Base OS post-install** — what Furtka actually looks like *after* the wizard writes config + reboots: Caddy + Authentik + app store. Robert's area. +- [ ] Installer wizard screens S4–S8 (user/password, domain, SSL, diagnostic, confirm) + actually invoking `archinstall` on the chosen disk +- [ ] `https://proksi.local` via mDNS + local CA (currently only raw-IP HTTP) - [ ] Caddy + Authentik wired into first-boot bootstrap - [ ] Managed gateway infrastructure — `ns1/ns2.furtka.org` + DNS-01 wildcard automation - [ ] First containerized service (Nextcloud?) with auto-SSO + auto-subdomain diff --git a/iso/README.md b/iso/README.md new file mode 100644 index 0000000..764178c --- /dev/null +++ b/iso/README.md @@ -0,0 +1,52 @@ +# Live ISO build + +Builds a bootable Arch-based live ISO that auto-starts the Flask webinstaller from `../webinstaller/` on boot. User plugs in a USB, boots, and the installer wizard comes up on `http://:5000`. + +Directly runnable; CI integration comes later once the build is stable. + +## Run a build + +Needs a host with Docker. Disk space required: ~15 GB scratch during the build, ~1.5 GB for the final ISO. + +```bash +./iso/build.sh +``` + +Output ISO ends up in `iso/out/furtka--x86_64.iso`. Around 3–10 min on a 4-core VM. First run is slower because it pulls `archlinux:latest` and all packages from upstream. + +The script re-execs itself inside a privileged `archlinux:latest` container. That's so `mkarchiso` has root + loop-mount access without polluting the host — Ubuntu hosts don't ship archiso natively anyway. + +## What gets baked in + +The build starts from Arch's stock `releng` profile (the same one used to build the official Arch ISO), then overlays our customizations from `overlay/`: + +| Overlay file | Effect | +|-------------------------------------------|----------------------------------------------------------------------------------| +| `overlay/packages.extra` | Appended to the package list. Adds `python`, `python-flask`, `avahi`, `nss-mdns` | +| `overlay/profiledef.sh` | Appended to `profiledef.sh`. Renames the ISO to `furtka-*` with a dated version | +| `overlay/airootfs/opt/furtka/` | Directory where `webinstaller/` is copied at build time | +| `overlay/airootfs/etc/systemd/system/` | Contains `furtka-webinstaller.service` + a symlink into `multi-user.target.wants/` so it auto-starts on boot | + +The systemd service runs `flask --app app run --host 0.0.0.0 --port 5000` under `/opt/furtka`. The `0.0.0.0` binding is important — the Flask default is localhost-only, which wouldn't be reachable from another machine on the LAN. + +mDNS (`proksi.local`) via avahi is installed but not yet wired. First milestone is just "boot → browser → wizard at raw IP". Naming comes next. + +## Test flow + +1. Build: `./iso/build.sh` +2. Copy the ISO to your Proxmox host's ISO storage (typically `/var/lib/vz/template/iso/`) +3. Create a VM with: + - 2 vCPU, 4 GB RAM, 20 GB disk (empty) + - CD-ROM attached with the Furtka ISO + - Boot order: CD before disk + - Network: same bridge as your LAN, DHCP +4. Start the VM. Wait ~30 s for boot. +5. Find its IP in Proxmox's VM summary (or your router's DHCP table) +6. Open `http://:5000` — the existing 3-screen wizard should be there + +## Known rough edges + +- **Disk space**: the first time you build on a fresh host, the squashfs/xorriso steps need ~15 GB free. If the host's LVM-root is smaller, `xorriso` silently dies at the very end with "Image size exceeds free space on media". +- **Flask `/` route** returns "Hello World" instead of redirecting to `/install/step1`. Harmless but surprising; will be cleaned up when we wire up screens 4–8. +- **No HTTPS yet**. The Furtka plan is "local CA + green padlock on `https://proksi.local`" — that's a later milestone. For now, plain HTTP. +- **archinstall is not invoked**. The wizard collects input but doesn't write to disk yet. Still a walking skeleton, not an installer. diff --git a/iso/build.sh b/iso/build.sh new file mode 100755 index 0000000..e403ac6 --- /dev/null +++ b/iso/build.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Build a Furtka live ISO. +# +# From the repo root or from iso/ on any host with Docker: +# ./iso/build.sh +# +# The build runs inside a privileged `archlinux:latest` container because +# mkarchiso needs root + loop mounts + an Arch package manager, which +# Ubuntu doesn't provide natively. Output ISO goes to iso/out/. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +OUT_DIR="$SCRIPT_DIR/out" + +if [[ "${FURTKA_ISO_INNER:-0}" != "1" ]]; then + mkdir -p "$OUT_DIR" + echo "==> Launching build container" + exec docker run --rm --privileged \ + -v "$REPO_ROOT:/work" \ + -w /work \ + -e FURTKA_ISO_INNER=1 \ + archlinux:latest \ + bash /work/iso/build.sh +fi + +# ---- inside the container from here on ---- + +echo "==> Syncing pacman, installing archiso" +pacman -Syu --noconfirm --needed archiso + +PROFILE_SRC="/usr/share/archiso/configs/releng" +PROFILE_WORK="/tmp/furtka-profile" +BUILD_WORK="/tmp/furtka-build" +OUT_IN_CONTAINER="/work/iso/out" + +rm -rf "$PROFILE_WORK" "$BUILD_WORK" +cp -a "$PROFILE_SRC" "$PROFILE_WORK" + +echo "==> Overlaying Furtka customizations" + +cat "$SCRIPT_DIR/overlay/packages.extra" >> "$PROFILE_WORK/packages.x86_64" + +cat "$SCRIPT_DIR/overlay/profiledef.sh" >> "$PROFILE_WORK/profiledef.sh" + +cp -a "$SCRIPT_DIR/overlay/airootfs/." "$PROFILE_WORK/airootfs/" + +mkdir -p "$PROFILE_WORK/airootfs/opt/furtka" +cp -a "$REPO_ROOT/webinstaller/." "$PROFILE_WORK/airootfs/opt/furtka/" +rm -rf "$PROFILE_WORK/airootfs/opt/furtka/__pycache__" + +mkdir -p "$PROFILE_WORK/airootfs/etc/systemd/system/avahi-daemon.service.d" +ln -sf /usr/lib/systemd/system/avahi-daemon.service \ + "$PROFILE_WORK/airootfs/etc/systemd/system/multi-user.target.wants/avahi-daemon.service" + +echo "==> Building ISO (mkarchiso)" +mkdir -p "$OUT_IN_CONTAINER" +mkarchiso -v -w "$BUILD_WORK" -o "$OUT_IN_CONTAINER" "$PROFILE_WORK" + +echo +echo "==> Done. ISO(s) in $OUT_IN_CONTAINER (on host: iso/out/):" +ls -lh "$OUT_IN_CONTAINER" diff --git a/iso/overlay/airootfs/etc/systemd/system/furtka-webinstaller.service b/iso/overlay/airootfs/etc/systemd/system/furtka-webinstaller.service new file mode 100644 index 0000000..612aa58 --- /dev/null +++ b/iso/overlay/airootfs/etc/systemd/system/furtka-webinstaller.service @@ -0,0 +1,14 @@ +[Unit] +Description=Furtka Live Installer (Flask) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +WorkingDirectory=/opt/furtka +ExecStart=/usr/bin/python -m flask --app app run --host 0.0.0.0 --port 5000 +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/iso/overlay/airootfs/etc/systemd/system/multi-user.target.wants/furtka-webinstaller.service b/iso/overlay/airootfs/etc/systemd/system/multi-user.target.wants/furtka-webinstaller.service new file mode 120000 index 0000000..8d6e4b4 --- /dev/null +++ b/iso/overlay/airootfs/etc/systemd/system/multi-user.target.wants/furtka-webinstaller.service @@ -0,0 +1 @@ +../furtka-webinstaller.service \ No newline at end of file diff --git a/iso/overlay/packages.extra b/iso/overlay/packages.extra new file mode 100644 index 0000000..ed4b40d --- /dev/null +++ b/iso/overlay/packages.extra @@ -0,0 +1,4 @@ +python +python-flask +avahi +nss-mdns diff --git a/iso/overlay/profiledef.sh b/iso/overlay/profiledef.sh new file mode 100644 index 0000000..9857963 --- /dev/null +++ b/iso/overlay/profiledef.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# Overrides for releng's profiledef.sh — only the fields we want to change. +# build.sh sources releng's original first, then this file, so these win. + +iso_name="furtka" +iso_label="FURTKA_$(date +%Y%m)" +iso_publisher="Furtka " +iso_application="Furtka Live Installer" +iso_version="$(date +%Y.%m.%d)"