Skip to main content

Flash images onto target disks, offline or networked with and without PXE

Project description

bty mascot - a blue bat holding a PXE handshake card and a disk labelled .qcow2 / .img / .raw

bty - flash images onto target disks, offline or networked with and without PXE

Pronounced "battie" (rhymes with "batty") - the blue bat up top is the mascot, so when in doubt say it like the critter.

CI Docs Documentation PyPI Python Container Changelog

Flash a single bare-metal box ad-hoc with a USB stick, or reflash a whole fleet remotely from a single controller -- bty works with or without PXE and scales from one machine to a rack without changing how you operate. The image is the source of truth: rebuild the image, reflash the target. No imperative configuration management, no idempotency mind games. Works equally well in homelabs, CI fleets, lab benches, data-centre racks, and anywhere else bytes need to land on a disk.

bty is a flasher, not an image builder:

  • Image creation is somebody else's project. First-boot bring-up (users, network, packages, hostnames) gets baked into the image upstream with cloud-init / kickstart / preseed / your favourite image builder. Use the companion image-builder (safl/nosi -- builds Debian / Ubuntu / Fedora / FreeBSD headless images (plus a Fedora desktop) and publishes them to GHCR as ORAS artifacts that bty flashes via oras://), or your own. bty just writes the bytes.
  • No post-boot configuration management either. Anything that needs to be true on the running target (users, hostnames, config files, packages) belongs in the image builder, not in bty. The server does not hold creds for any target it has provisioned -- that blast radius is intentionally absent.
# Local: USB stick into target, two arrows + Enter, done.
bty

# Remote: bind a MAC to an image, the next PXE boot reflashes itself.
# (See the bty-web HTTP API reference in the docs for the full surface.)

# Per-job CI: every job a clean OS, no drift, no snowflakes.

Three delivery shapes, one runtime

Shape What it is When it fits
USB live stick bty boots from a flash drive, runs bty, flashes the box it's plugged into. Fresh sticks ship with a starter catalog.toml (Debian / Ubuntu / Fedora / FreeBSD headless images plus a Fedora desktop, via oras://ghcr.io/safl/nosi/...) so the wizard's image picker is non-empty out of the box. Single-machine local imaging
USB + portable catalog Same stick, plus bty --catalog <SOURCE> pointed at a TOML catalog hosted anywhere (a local file, an HTTP URL, an oras:// reference, or a bty-web instance's /catalog.toml). A handful of boxes, shared image library
PXE-boot server sudo uvx bty-lab deploy /opt/bty brings up bty-web + withcache on a Pi or x86 box -- no clone required. An optional tftp sidecar covers legacy BIOS, and your LAN DHCP points PXE clients at the host. Targets PXE-chain into a netboot live env that runs bty --server X --mac Y on tty1, which fetches a per-MAC plan and either auto-flashes or drops the operator into the wizard. See deploy/README.md. CI fleets, racks, anything you don't want to walk to

All three share the same Python codebase, the same image catalog, the same SHA-keyed machine bindings.

The container deploy keeps rootfs separate from the image cache: /var/lib/bty is a named volume that survives container restarts and re-pulls, and the image cache can be delegated to the withcache sidecar so multiple targets pull each image once. See deploy/README.md for the volume layout.

ORAS-published images and portable catalogs

bty consumes images and catalogs as OCI artifacts published with ORAS (OCI Registry As Storage -- the spec for non-container artifacts in a container registry). The end-to-end story:

  • Images live in a registry. safl/nosi publishes Debian / Ubuntu / Fedora disk images to ghcr.io/safl/nosi/<variant>:latest. bty resolves an oras://ghcr.io/safl/nosi/... source from a catalog entry, picks the disk-image layer, and streams the blob straight to the target via the same curl | dd pipeline as any HTTP URL. Anonymous-pull only -- no PAT, no docker login.
  • Catalogs are portable TOML files. A catalog is a small TOML manifest listing named images with src URLs (any combination of http(s)://, oras://, or file://). bty --catalog <SOURCE> accepts a local path, an HTTP URL, or an oras:// reference. Operators can publish a catalog on GitHub Releases, an S3 bucket, a private registry, or alongside images in GHCR -- whatever they already have. bty-web instances serve the same shape at GET /catalog.toml, so a running server is "just another catalog source".
  • One catalog format end to end. A USB stick's BTY_IMAGES partition can carry a catalog.toml alongside image files; the wizard discovers + merges the local catalog with whatever --catalog source the operator passed. Same schema as the server-published catalog, no separate per-stick format.

Why this shape: images and catalog metadata are content-addressed artifacts, not container images. The OCI ecosystem already solves "distribute signed, versioned, content-addressed blobs"; bty just piggybacks on that without dragging in the docker / podman runtime.

Why bty

  • Reflash on every CI job. Per-job cadence: each job lands on a freshly-imaged target, runs, gets reflashed for the next job. No state leaks. No snowflakes. No "works on my machine" because the machine is bit-identical to the manifest every single boot.

  • Pre-built images, not recipes. You build the image once (in your build system of choice), bty writes the bytes. Any first-boot bring-up (users, networking, hostnames) is baked into the image by the image builder upstream via cloud-init / NoCloud user-data. bty itself doesn't run a provisioning step -- no agent, no daemon, no convergence loops.

    Note that interactive picks (operator chooses an image at tty1) are not reported back to the server: bty-server tracks "what image is this MAC supposed to have" only when a flash policy (boot_policy=bty-flash-always / bty-flash-once) binds it. Interactive runs are operator-driven and stay local. See docs/src/concepts.md for the asymmetry.

  • OS-agnostic by design. Linux, FreeBSD, Windows - if it boots from a disk image, bty can flash it. macOS targets are out (Apple Silicon's boot story isn't friendly to imaging).

  • Trust model is explicit. PXE / live-env routes are open (clients have no token); operator routes (/machines, /catalog/*, /boot/releases) require a session cookie. bty-web is for trusted networks (homelab, CI segment), not the open internet.

Stand up a bty server

The canonical deploy is two containers (bty-web + withcache, plus an optional bty-tftp sidecar). With uv (or pipx) on the host, no clone required:

sudo uvx bty-lab deploy /opt/bty
#   bty-web:   http://<host>:8080/ui     (login: bty-lab / bty-lab)
#   withcache: http://<host>:3000/       (login: bty-lab / bty-lab)

deploy auto-detects install mode from your euid:

  • As root (system install): writes envvars, brings up the stack with the TFTP sidecar, installs Podman Quadlet units to /etc/containers/systemd/, starts services via systemctl. Survives host reboots.
  • As a regular user (user install): compose-only. No TFTP, no autostart. The CLI prints what was skipped and the sudo re-run command to promote to a system install.

HOST_ADDR is detected from the host's outbound-route IP; admin passwords default to bty-lab. Change them in /opt/bty/envvars before exposing past trusted LAN. uvx bty-lab upgrade /opt/bty upgrades in place; uvx bty-lab init /opt/bty emits files only (no side effects). See deploy/README.md for the full surface.

Install

bty is one Python package - bty-lab on PyPI - with two console scripts:

pipx install "bty-lab[tui]"     # `bty` (Rich-based wizard, the
                                #  operator-facing tool)
pipx install "bty-lab[web]"     # adds `bty-web` (FastAPI + Pydantic,
                                #  the HTTP controller)
pipx install "bty-lab[all]"     # everything

bty shells out to dd, qemu-img, zstd, lsblk, curl (used by URL / oras:// fetch), and friends - your distro provides those. The dispatch surface is intentionally narrow: bare bty launches the local-image wizard, bty --catalog URL pre-loads a catalog, bty --server X --mac Y fetches a per-MAC plan from the server and dispatches (auto-flash / interactive / no-op).

For media you can boot directly (USB flasher stick, PXE-chain netboot live env), grab the bake from GitHub Releases. The media builder lives under bty-media/. To run bty-web as a controller, use the container deploy under deploy/.

Status

Pre-1.0 but actively shipping. Every tag publishes wheels (PyPI), boot media (USB flasher + netboot live env), and the bty-web container. The end-to-end PXE flow (server + netboot live env + target flash + completion signal) runs in CI on every push. CLI flags and wire formats may still shift between minor versions until 1.0 - watch the schema_version field on --json output and the Machine wire type. The PLAN.md tracks the roadmap milestone by milestone.

Development

pipx install uv
uv sync --all-extras --group dev
uv run pytest                    # full suite
uv run ruff check                # lint
uv run mypy src                  # types

The docs tooling installs separately:

pipx install ./docs/tooling
cd docs
bty-docs-serve                   # live-rebuild dev server on :8000
bty-docs-build-html              # one-shot HTML build
bty-docs-build-pdf               # one-shot PDF (requires LaTeX)

More

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bty_lab-0.44.1.tar.gz (2.1 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bty_lab-0.44.1-py3-none-any.whl (1.0 MB view details)

Uploaded Python 3

File details

Details for the file bty_lab-0.44.1.tar.gz.

File metadata

  • Download URL: bty_lab-0.44.1.tar.gz
  • Upload date:
  • Size: 2.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for bty_lab-0.44.1.tar.gz
Algorithm Hash digest
SHA256 d2ca92ae3a6aa0fe53913921e2efab1475c7ddc2ea64ea5949cb334bef87c08e
MD5 c54b1800b2157481984808798b91306a
BLAKE2b-256 4b1ae80c7411e611645e3644b1b7b5ad9c4deebbf560164a6aa601ec1f93128a

See more details on using hashes here.

Provenance

The following attestation bundles were made for bty_lab-0.44.1.tar.gz:

Publisher: ci-cd.yml on safl/bty

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file bty_lab-0.44.1-py3-none-any.whl.

File metadata

  • Download URL: bty_lab-0.44.1-py3-none-any.whl
  • Upload date:
  • Size: 1.0 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for bty_lab-0.44.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9b6dc1804f9eeff96fb187c4040e4261b593d1f8bc30530915a9fa4496ee4dad
MD5 52d3d89eb1a70398ceadec972aa85d6b
BLAKE2b-256 f4c0313a1b151ba157736e3ea564544221fa9627047ea4fbea04ca535a2b7779

See more details on using hashes here.

Provenance

The following attestation bundles were made for bty_lab-0.44.1-py3-none-any.whl:

Publisher: ci-cd.yml on safl/bty

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page