Quickstart¶
A walk-through of what bty can do today, ordered roughly the way an operator would meet it: build a delivery medium, boot a target, flash, optionally provision, and finally drive a fleet over the network via the bty-web server.
Get the USB live image¶
Either download a pre-built one from the GitHub release, or build from a checkout.
Pre-built (fastest):
mkdir -p ~/system_imaging/disk && cd ~/system_imaging/disk
curl -fLO https://github.com/safl/bty/releases/latest/download/bty-usb-x86_64.img.zst
curl -fLO https://github.com/safl/bty/releases/latest/download/bty-usb-x86_64.img.zst.sha256
sha256sum -c bty-usb-x86_64.img.zst.sha256
releases/latest/download/<name> always points at the newest tag;
swap latest for a specific tag (e.g. v0.2.7) if you want to pin.
Build from source (when you need to modify the image):
# prerequisites: qemu-system-x86_64, qemu-img, genisoimage, zstd,
# pipx, KVM acceleration
make media-deps # one-time: pipx install cijoe
make build VARIANT=usb-x86 # 15-25 min with KVM
The build downloads the Debian 13 cloud image, drives cloud-init in
QEMU to bake the rootfs, partitions the disk (3 GB Debian root + 9 GB
exFAT BTY_IMAGES), and emits:
~/system_imaging/disk/bty-usb-x86_64.qcow2- intermediate qcow2 (useful for QEMU smoke tests).~/system_imaging/disk/bty-usb-x86_64.img.zst- distributable artifact (the file youddto a USB stick).~/system_imaging/disk/bty-usb-x86_64.img.zst.sha256- checksum.
Flash a USB stick¶
# Identify the USB device first - this is destructive.
lsblk
# /dev/sdX is the USB stick (NOT your local system disk).
zstd -d --stdout ~/system_imaging/disk/bty-usb-x86_64.img.zst | \
sudo dd of=/dev/sdX bs=4M status=progress conv=fsync
sync
The stick now has the bty Debian rootfs partition plus an empty exFAT
partition labelled BTY_IMAGES.
Drop images onto the stick¶
Mount the BTY_IMAGES partition on any Linux / macOS / Windows box
(exFAT is universally readable) and copy your cooked images into it:
sudo mount /dev/disk/by-label/BTY_IMAGES /mnt
sudo cp /path/to/my-image.qcow2 /mnt/
sudo umount /mnt
See the Disk layout section in Concepts for the convention bty expects.
Boot a target machine¶
Insert the USB stick into the target machine and boot from it. The bty
live env auto-logins as root on tty1. From there you can run the CLI
(bty list disks, bty flash ...) or bty-tui for an interactive
terminal UI.
The rootfs is mounted read-only with a tmpfs overlay (overlayroot),
so anything you change in the live env vanishes on reboot. The
BTY_IMAGES partition is not overlaid - files you copied there
persist.
What you can do today¶
Inspect¶
Inside the live env (or on any Linux box where bty is installed):
# List interesting block devices on the system
bty list disks
# List images available under /var/lib/bty/images (or BTY_IMAGE_ROOT)
bty list images
# Inspect a specific image in detail
bty inspect image /var/lib/bty/images/my-image.qcow2
# Each leaf command also accepts --json
bty list disks --json
bty inspect image --json /var/lib/bty/images/my-image.qcow2
Flash a target disk¶
# 1. Validate that an image can be flashed to a target without writing.
bty flash --image /var/lib/bty/images/my-image.qcow2 \
--target /dev/sdX \
--provision none \
--dry-run
# 2. Once the plan looks right, run for real (requires root):
sudo bty flash --image /var/lib/bty/images/my-image.qcow2 \
--target /dev/sdX \
--provision none \
--yes
--dry-run prints a plan and validates without writing. --yes is
the explicit consent token for the destructive write - bty flash
refuses to do anything without one or the other.
Cloud-init provisioning¶
Seed cloud-init’s NoCloud datasource onto the freshly-flashed disk so the target self-configures on first boot:
sudo bty flash --image /var/lib/bty/images/debian.qcow2 \
--target /dev/sdX \
--provision cloud-init \
--user-data ./userdata.yaml \
--yes
bty mounts the cloud-init-enabled rootfs partition on the target,
drops user-data (and a synthesised meta-data if --meta-data is
not supplied) under /var/lib/cloud/seed/nocloud-net/, and unmounts.
On first boot the OS picks up the seed via cloud-init’s NoCloud
datasource.
CIJOE provisioning (offline)¶
Run a cijoe workflow against the freshly-flashed filesystem before the target reboots:
sudo bty flash --image /var/lib/bty/images/debian.qcow2 \
--target /dev/sdX \
--provision cijoe \
--cijoe-workflow ./tweaks.yaml \
--yes
bty mounts the largest partition on the target, exports
BTY_ROOTFS pointing at the mount, then runs the supplied cijoe
workflow. Workflow tasks reference $BTY_ROOTFS to drop config
files, install seed credentials, etc. Requires cijoe on PATH
(install via pipx install cijoe).
Interactive flashing via the TUI:
sudo bty-tui
The TUI lists available images (left pane) and block devices (right
pane). Cursor between the panes, select with Enter, then press F
to flash. A modal shows the plan and any validation errors; confirm
to run. A status modal streams the result.
Without root the TUI still launches in a read-only mode (you can
inspect lists), but the F action refuses with a status message.
Requires the [tui] install extra (pipx install "bty-lab[tui]").
See Reference > CLI for the full surface.
Network flashing via the bty-web server¶
bty-web is the HTTP server side of bty - browser UI + REST API +
the iPXE chain a target boots into for network-flash. The server
appliance image (make build VARIANT=server-x86) ships preconfigured;
for a quick local test you can run it directly:
# On the server (or any box you're testing on):
export BTY_STATE_DIR=/var/lib/bty
bty-web # listens on 0.0.0.0:8080 by default
Auth is OS-PAM against the bty service user (the account bty-web
runs as). On the appliance image the default is bty / bty; rotate
with sudo passwd bty before exposing. The browser UI at
http://server:8080/ui/login is the primary operator entry point;
GET /pxe/{mac} (the route PXE clients hit) is open and needs no
auth.
If you want to script mutations from a shell, drive /ui/login once
to get the cookie, then attach it on subsequent requests:
COOKIE=$(curl -sS -i -X POST -d "password=bty" \
http://server:8080/ui/login \
| grep -i '^set-cookie:.*bty-token' | sed 's/.*bty-token=\([^;]*\).*/\1/')
curl -H "Cookie: bty-token=$COOKIE" http://server:8080/machines
curl -H "Cookie: bty-token=$COOKIE" -X PUT \
-H "Content-Type: application/json" \
-d '{"image":"debian.qcow2","provisioning_mode":"none","boot_policy":"flash"}' \
http://server:8080/machines/aa:bb:cc:dd:ee:ff
PXE clients hit GET /pxe/{mac} (open, no auth) for the per-MAC
iPXE config and chain into the live env, which downloads the
assigned image and flashes the target’s local disk.
Browser UI¶
http://server:8080/ui/login - the same bty / bty credential
gets you a cookie-backed session. The dashboard shows machine /
image counts; the Machines page is a live table that updates
via Server-Sent Events as PXE clients self-discover. The
Settings page activates the dnsmasq proxy-DHCP block when
you’re ready to start serving PXE.
All client-side assets (Bootstrap CSS, Bootstrap Icons, HTMX, htmx-ext-sse) are vendored in the wheel - the appliance does not contact any external CDN at runtime.
What is coming¶
See PLAN.md for
the live roadmap (per-machine cijoe online provisioning, image
catalog upload via the UI, target-disk hints in the per-MAC plan,
etc.).