Skip to main content

Flash images onto target disks, locally or remote 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 a fleet without leaving your chair

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

Reflash a homelab box, a CI runner, or a rack of bare-metal targets in the time it takes to make coffee. bty writes pre-built system images onto disks - locally over USB or remotely over PXE. The image is the source of truth: rebuild the image, reflash the target. No imperative configuration management, no idempotency mind games.

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 sysdev images and publishes them to GHCR as ORAS artefacts 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 tui

# 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 tui, flashes the box it's plugged into. Fresh sticks ship with four starter .bri pointers (Debian / Ubuntu / Fedora sysdev images via oras://ghcr.io/safl/nosi/..., plus bty-server) so the catalog is non-empty out of the box. Single-machine local imaging
USB + portable catalog Same stick, plus bty tui --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 appliance bty-web on a Pi or x86 box runs DHCP/TFTP/HTTP; targets PXE-chain into a netboot live env that flashes them unattended 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 PXE-boot appliance also separates rootfs from image cache: drop a 2nd disk in, run sudo bty-image-store-init /dev/sdX once, and the image library survives appliance reflashes. The new appliance auto- mounts the labelled disk at /var/lib/bty/images; no operator action required.

ORAS-published images and portable catalogs

bty consumes images and catalogs as OCI artefacts published with ORAS (OCI Registry As Storage -- the spec for non-container artefacts 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 flash oras://ghcr.io/safl/nosi/debian-sysdev:latest /dev/sdX --yes resolves the manifest, 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 tui --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".
  • .bri descriptors are the per-stick analogue. A USB stick's BTY_IMAGES partition can carry .bri files (one-image-per-file TOML pointers, including oras:// URLs). The TUI merges them with whatever --catalog source the operator passed.

Why this shape: images and catalog metadata are content-addressed artefacts, 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.
  • 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.

Try it without flashing anything

A multi-arch container is published on every release:

docker run -d --name bty-web -p 8080:8080 -v bty-data:/var/lib/bty \
  ghcr.io/safl/bty-web:latest
# -> http://localhost:8080/ui   (login: bty / bty)

Image catalog only - no DHCP / TFTP / PXE proxy in the container (those need bare-metal LAN access; use the appliance for that). See docs/src/walkthrough-server-docker.md for bind-mount permissions, env vars, and password rotation.

Install

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

pipx install bty-lab            # `bty` CLI, zero third-party deps
pipx install "bty-lab[tui]"     # adds `bty-tui` (Textual)
pipx install "bty-lab[web]"     # adds `bty-web` (FastAPI + Pydantic)
pipx install "bty-lab[all]"     # everything

lsblk -d -e7, bty inspect, bty flash --dry-run need only Python 3.11+ and stdlib. bty flash --yes shells out to dd, qemu-img, zstd, lsblk, curl (used by URL / oras:// fetch), and friends - your distro provides those.

For an appliance you can boot directly (USB stick, server image, PXE-chain live env), grab the bake from GitHub Releases. The appliance builder lives under bty-media/.

Status

Pre-1.0 but actively shipping. Every tag publishes wheels (PyPI), appliance images, 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.20.6.tar.gz (2.3 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.20.6-py3-none-any.whl (962.5 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: bty_lab-0.20.6.tar.gz
  • Upload date:
  • Size: 2.3 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.20.6.tar.gz
Algorithm Hash digest
SHA256 52a0da69fac976e7a5e902efade2f9eb55bf54d383750917ab34b5e77f5046a2
MD5 19eca99466efe5f06baf6a28a7e4b9f2
BLAKE2b-256 2422f53e38955bbd222afcc7ec8db0fad92d4951dcf9d915a09154e5fc5a5fa6

See more details on using hashes here.

Provenance

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

Publisher: release.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.20.6-py3-none-any.whl.

File metadata

  • Download URL: bty_lab-0.20.6-py3-none-any.whl
  • Upload date:
  • Size: 962.5 kB
  • 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.20.6-py3-none-any.whl
Algorithm Hash digest
SHA256 92e1a3ea1e61edb8568b55c87ba84fd8c94f16ee3c2c63aef2293fd3fa1a99aa
MD5 83a57127e1aa72af0ba4cea1838debd0
BLAKE2b-256 9227c434d896b7d52ddb852de35e9be8939466819077c9ed886c8a8643c638b1

See more details on using hashes here.

Provenance

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

Publisher: release.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