Metadata-Version: 2.4
Name: bty-lab
Version: 0.7.56
Summary: Flash images onto target disks, locally or over PXE
Project-URL: Homepage, https://safl.dk/bty/
Project-URL: Documentation, https://safl.dk/bty/
Project-URL: Repository, https://github.com/safl/bty
Author-email: "Simon A. F. Lund" <os@safl.dk>
License-Expression: GPL-3.0-only
License-File: LICENSE
Keywords: appliance,flashing,imaging,ipxe,provisioning
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.11
Provides-Extra: all
Requires-Dist: fastapi>=0.115; extra == 'all'
Requires-Dist: itsdangerous>=2.1; extra == 'all'
Requires-Dist: jinja2>=3.1; extra == 'all'
Requires-Dist: pamela>=1.1; extra == 'all'
Requires-Dist: python-multipart>=0.0.9; extra == 'all'
Requires-Dist: textual>=0.80; extra == 'all'
Requires-Dist: uvicorn[standard]>=0.30; extra == 'all'
Provides-Extra: tui
Requires-Dist: textual>=0.80; extra == 'tui'
Provides-Extra: web
Requires-Dist: fastapi>=0.115; extra == 'web'
Requires-Dist: itsdangerous>=2.1; extra == 'web'
Requires-Dist: jinja2>=3.1; extra == 'web'
Requires-Dist: pamela>=1.1; extra == 'web'
Requires-Dist: python-multipart>=0.0.9; extra == 'web'
Requires-Dist: uvicorn[standard]>=0.30; extra == 'web'
Description-Content-Type: text/markdown

<p align="center">
  <img src="docs/src/_static/bty-mascot.png" alt="bty mascot - a blue bat holding a PXE handshake card and a disk labelled .qcow2 / .img / .raw" width="240">
</p>

# bty - flash a fleet without leaving your chair

[![CI](https://github.com/safl/bty/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/safl/bty/actions/workflows/ci.yml)
[![Docs](https://github.com/safl/bty/actions/workflows/docs.yml/badge.svg?branch=main)](https://github.com/safl/bty/actions/workflows/docs.yml)
[![Documentation](https://img.shields.io/badge/docs-safl.dk%2Fbty-blue)](https://safl.dk/bty)
[![PyPI](https://img.shields.io/pypi/v/bty-lab.svg)](https://pypi.org/project/bty-lab/)
[![Python](https://img.shields.io/pypi/pyversions/bty-lab.svg)](https://pypi.org/project/bty-lab/)
[![Container](https://img.shields.io/badge/container-ghcr.io%2Fsafl%2Fbty--web-blue)](https://github.com/safl/bty/pkgs/container/bty-web)

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](https://github.com/safl/jellyfin-kiosk-appliance-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.

```bash
# 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:

```bash
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`](docs/src/walkthrough-server-docker.md)
for bind-mount permissions, env vars, and password rotation.

## Install

bty is one Python package - [`bty-lab`](https://pypi.org/project/bty-lab/) on
PyPI - with three console scripts:

```bash
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](https://github.com/safl/bty/releases). The
appliance builder lives under [`bty-media/`](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`](PLAN.md) tracks the roadmap milestone by milestone.

## Development

```bash
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:

```bash
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`](PLAN.md) - roadmap and design intent.
- [`docs/`](docs/) - full documentation (Sphinx + MyST), also at
  [`safl.dk/bty`](https://safl.dk/bty).
