Flash images onto target disks, locally or over PXE
Project description
bty - flash a fleet without leaving your chair
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 ("cooked") 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 a cooker:
- 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 cooker. Use the companion image-builder pattern, or your own. bty just writes the bytes.
- Post-boot configuration is
cijoe-task. For machines whose MAC bty-web manages, the server SSHes into the freshly-booted target and runs a small CIJOE task. Steps use cijoe's built-in scripts or inline commands -- nothing else. The intent is light post-flash scripting (set a hostname, trigger a reboot, drop a config file), not configuration management. No third-party cijoe script packages; if you need one, the job belongs in the cooker. Cancelable from the browser UI; events visible in the audit log.
# Local: USB stick into target, two arrows + Enter, done.
bty-tui
# Remote: bind a MAC to an image, the next PXE boot reflashes itself.
curl -X PUT http://bty-server:8080/machines/aa:bb:cc:dd:ee:ff \
-d '{"image_sha256":"<sha>","boot_policy":"flash"}'
# 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 |
Single-machine local imaging |
| USB + server catalog | Same stick, but the image list comes from bty-web --server URL |
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.
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.
- Cooked images, not recipes. You build the image once (in your build system of choice), bty writes the bytes. Provisioning is cloud-init or a CIJOE task on first boot - small, declarative, inspectable. 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
bty list disks, bty inspect image, bty flash --dry-run need only
Python 3.11+ and stdlib. bty flash --yes shells out to dd,
qemu-img, zstd, lsblk, 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
PLAN.md- roadmap and design intent.docs/- full documentation (Sphinx + MyST), also atsafl.dk/bty.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bty_lab-0.7.54.tar.gz.
File metadata
- Download URL: bty_lab-0.7.54.tar.gz
- Upload date:
- Size: 3.1 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eea287df8c71da688e0e57c2553de44fb0ff2b99c6bd6caef66f5e7ea7e522f3
|
|
| MD5 |
f1ac05d4b1a80bffc0dd5abc10e1d6b5
|
|
| BLAKE2b-256 |
b1c2c01c8efb965def897e007fc72d4c34828e0c30eeb84a93fb93ba59cb08ee
|
Provenance
The following attestation bundles were made for bty_lab-0.7.54.tar.gz:
Publisher:
release.yml on safl/bty
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bty_lab-0.7.54.tar.gz -
Subject digest:
eea287df8c71da688e0e57c2553de44fb0ff2b99c6bd6caef66f5e7ea7e522f3 - Sigstore transparency entry: 1497951099
- Sigstore integration time:
-
Permalink:
safl/bty@0532d9d0083cc398e668bc7b57a64ed69bb3f680 -
Branch / Tag:
refs/tags/v0.7.54 - Owner: https://github.com/safl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0532d9d0083cc398e668bc7b57a64ed69bb3f680 -
Trigger Event:
push
-
Statement type:
File details
Details for the file bty_lab-0.7.54-py3-none-any.whl.
File metadata
- Download URL: bty_lab-0.7.54-py3-none-any.whl
- Upload date:
- Size: 748.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
605b4120e6e49b70a49743018e8aea8e16ac8720cbcf03d346739d7fbd83bf7c
|
|
| MD5 |
8aa47cd22b55a880513f029567c82ec2
|
|
| BLAKE2b-256 |
e859c8cbd056d1f207d777d609b7304cb48ef546e7525bb88891e332e5d23665
|
Provenance
The following attestation bundles were made for bty_lab-0.7.54-py3-none-any.whl:
Publisher:
release.yml on safl/bty
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bty_lab-0.7.54-py3-none-any.whl -
Subject digest:
605b4120e6e49b70a49743018e8aea8e16ac8720cbcf03d346739d7fbd83bf7c - Sigstore transparency entry: 1497951229
- Sigstore integration time:
-
Permalink:
safl/bty@0532d9d0083cc398e668bc7b57a64ed69bb3f680 -
Branch / Tag:
refs/tags/v0.7.54 - Owner: https://github.com/safl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0532d9d0083cc398e668bc7b57a64ed69bb3f680 -
Trigger Event:
push
-
Statement type: