Switch to fetched runtime bundles

Stop treating Firecracker, kernels, modules, and guest images as tracked source files. Source checkouts now resolve runtime assets from ./runtime, while installed binaries keep using ../lib/banger.

Add a small runtimebundle helper plus runtime-bundle.toml so make can bootstrap, package, and install a runtime bundle with checksum validation. Update the shell helpers and daemon path hints to fail clearly when the bundle is missing instead of assuming repo-root artifacts.

This removes the tracked runtime blobs from HEAD in favor of an ignored local runtime/ tree. Verified with go test ./..., make build, bash -n on the shell helpers, make -n install, and a temporary package/fetch smoke test. The manifest URL/SHA still need a published bundle before fresh clones can bootstrap, and history rewrite remains a separate rollout step.
This commit is contained in:
Thales Maciel 2026-03-16 15:05:10 -03:00
parent ce1be52047
commit 238bb8a020
No known key found for this signature in database
GPG key ID: 33112E6833C34679
6512 changed files with 1019 additions and 65372 deletions

4
.gitignore vendored
View file

@ -1,4 +1,8 @@
state/
/runtime/
/dist/
/banger
/bangerd
*.log
*.sock
*.pid

View file

@ -1,33 +1,39 @@
# Repository Guidelines
## Project Structure & Module Organization
- `run.sh` is the primary launcher script; it builds the per-VM state and configures Firecracker over the local API socket.
- `firecracker`, `vmlinux`, and `rootfs.ext4` are runtime artifacts required to boot the guest.
- `state/` is created at runtime to store per-VM sockets, logs, and metadata (safe to delete when no VMs are running).
- `firecracker.log` is produced by Firecracker; additional per-VM logs live under `state/vm-*/`.
- `cmd/banger` and `cmd/bangerd` are the primary user-facing entrypoints.
- `internal/` contains the daemon, CLI, RPC, storage, Firecracker, and system integration code.
- `customize.sh`, `make-rootfs.sh`, and `interactive.sh` remain as image-build/customization helpers; normal VM lifecycle and NAT management are handled by the Go control plane.
- Source checkouts use a generated `./runtime/` bundle for Firecracker, kernels, modules, rootfs images, and helper copies. Those runtime artifacts are not meant to be tracked directly in Git.
- The daemon keeps state under XDG directories rather than the old repo-local `state/` layout.
## Build, Test, and Development Commands
- `./run.sh` launches a VM using Firecracker, sets up a bridge/TAP device, and prints the guest IP plus SSH command.
- `ssh -i "./id_ed25519" root@<guest_ip>` connects to the guest once it boots.
- `reboot` (inside the guest) shuts down the VM.
- There is no build step in this repo; binaries and images are checked in.
- `make build` builds `./banger` and `./bangerd`.
- `make runtime-bundle` bootstraps `./runtime/` from `runtime-bundle.toml`.
- `./banger vm create --name testbox` creates and starts a VM.
- `./banger vm ssh testbox` connects to a running guest.
- `./banger vm stop testbox` stops a VM while preserving its disks.
- `./banger tui` launches the terminal UI.
- `make test` runs `go test ./...`.
- `./verify.sh` runs the smoke test for the Go VM workflow.
## Coding Style & Naming Conventions
- Shell scripts use Bash with `set -euo pipefail`; keep new scripts strict and explicit.
- Indentation is two spaces in `run.sh`; match existing formatting and quoting style.
- Filenames are short and descriptive (e.g., `run.sh`, `rootfs.ext4`). Prefer lowercase with dashes or dots.
- No formatter or linter is configured; keep changes small and readable.
- Go code should stay small, direct, and standard-library-first unless there is a clear reason otherwise.
- Shell helpers use Bash with `set -euo pipefail`; keep remaining shell scripts strict and explicit.
- Prefer lowercase filenames with short descriptive names.
- Use `gofmt` for Go formatting; no extra formatter is configured for shell files.
## Testing Guidelines
- No automated test framework is present.
- Manual verification: run `./run.sh`, confirm the guest boots, and verify SSH access.
- If adding tests, document how to run them in this file and keep them self-contained.
- Primary automated coverage is `go test ./...`.
- Manual verification for VM lifecycle changes: `./banger vm create`, confirm SSH access, then stop/delete the VM.
- If you add a new operational workflow, document how to exercise it in `README.md`.
- For NAT changes, verify both guest outbound access and host rule cleanup, for example with `./verify.sh --nat`.
## Commit & Pull Request Guidelines
- Git history uses short, informal commit summaries (e.g., "ignore", "Document expected log noise").
- Prefer imperative, single-line subjects; keep them under ~50 characters when possible.
- PRs should describe the change, list any new runtime requirements, and include logs or screenshots if behavior changes.
- Git history uses short, imperative subjects.
- Prefer a real commit body when the change affects lifecycle behavior, storage semantics, or host integration.
- PRs should call out runtime requirements, migration impact, and any host-side verification performed.
## Security & Configuration Tips
- The script requires `sudo` and `/dev/kvm` access; avoid committing secrets or private keys.
- `id_ed25519` is a local SSH key for the guest; rotate or replace it if sharing the repository.
- The VM workflow requires `sudo` and `/dev/kvm` access; do not commit secrets.
- `id_ed25519` lives inside the runtime bundle; rotate or replace it before publishing a shared bundle.

View file

@ -8,27 +8,33 @@ BINDIR ?= $(PREFIX)/bin
LIBDIR ?= $(PREFIX)/lib
RUNTIMEDIR ?= $(LIBDIR)/banger
DESTDIR ?=
RUNTIME_MANIFEST ?= runtime-bundle.toml
RUNTIME_SOURCE_DIR ?= runtime
RUNTIME_ARCHIVE ?= dist/banger-runtime.tar.gz
BINARIES := banger bangerd
GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort)
RUNTIME_EXECUTABLES := firecracker customize.sh dns.sh packages.sh nat.sh namegen
RUNTIME_DATA_FILES := packages.apt $(wildcard rootfs.ext4) $(wildcard rootfs-docker.ext4)
RUNTIME_DATA_FILES := packages.apt id_ed25519 rootfs-docker.ext4
RUNTIME_OPTIONAL_DATA_FILES := rootfs.ext4
RUNTIME_BOOT_FILES := wtf/root/boot/vmlinux-6.8.0-94-generic wtf/root/boot/initrd.img-6.8.0-94-generic
RUNTIME_MODULES_DIR := wtf/root/lib/modules/6.8.0-94-generic
.DEFAULT_GOAL := help
.PHONY: help build banger bangerd test fmt tidy clean rootfs install
.PHONY: help build banger bangerd test fmt tidy clean rootfs install runtime-bundle runtime-package check-runtime
help:
@printf '%s\n' \
'Targets:' \
' make build Build ./banger and ./bangerd' \
' make runtime-bundle Download and unpack ./runtime from runtime-bundle.toml' \
' make runtime-package Package $(RUNTIME_SOURCE_DIR) into $(RUNTIME_ARCHIVE) and print its SHA256' \
' make install Build and install binaries plus the runtime bundle into $(DESTDIR)$(BINDIR) and $(DESTDIR)$(RUNTIMEDIR)' \
' make test Run go test ./...' \
' make fmt Format Go sources under cmd/ and internal/' \
' make tidy Run go mod tidy' \
' make clean Remove built Go binaries' \
' make rootfs Rebuild the repo-local default rootfs image'
' make rootfs Rebuild the source-checkout default rootfs image in ./runtime'
build: $(BINARIES)
@ -50,11 +56,19 @@ tidy:
clean:
rm -f ./banger ./bangerd
install: build
@for path in $(RUNTIME_EXECUTABLES) $(RUNTIME_BOOT_FILES) $(RUNTIME_MODULES_DIR) packages.apt id_ed25519; do \
test -e "$$path" || { echo "missing runtime artifact: $$path" >&2; exit 1; }; \
runtime-bundle:
$(GO) run ./cmd/runtimebundle fetch --manifest "$(RUNTIME_MANIFEST)" --out "$(RUNTIME_SOURCE_DIR)"
runtime-package:
$(GO) run ./cmd/runtimebundle package --manifest "$(RUNTIME_MANIFEST)" --runtime-dir "$(RUNTIME_SOURCE_DIR)" --out "$(RUNTIME_ARCHIVE)"
check-runtime:
@test -d "$(RUNTIME_SOURCE_DIR)" || { echo "missing runtime bundle directory: $(RUNTIME_SOURCE_DIR); run 'make runtime-bundle'" >&2; exit 1; }
@for path in $(RUNTIME_EXECUTABLES) $(RUNTIME_DATA_FILES) $(RUNTIME_BOOT_FILES) $(RUNTIME_MODULES_DIR); do \
test -e "$(RUNTIME_SOURCE_DIR)/$$path" || { echo "missing runtime artifact: $(RUNTIME_SOURCE_DIR)/$$path; run 'make runtime-bundle'" >&2; exit 1; }; \
done
@test -e rootfs-docker.ext4 || test -e rootfs.ext4 || { echo "missing runtime artifact: rootfs-docker.ext4 or rootfs.ext4" >&2; exit 1; }
install: build check-runtime
mkdir -p "$(DESTDIR)$(BINDIR)"
mkdir -p "$(DESTDIR)$(RUNTIMEDIR)"
mkdir -p "$(DESTDIR)$(RUNTIMEDIR)/wtf/root/boot"
@ -62,13 +76,18 @@ install: build
$(INSTALL) -m 0755 ./banger "$(DESTDIR)$(BINDIR)/banger"
$(INSTALL) -m 0755 ./bangerd "$(DESTDIR)$(BINDIR)/bangerd"
@for path in $(RUNTIME_EXECUTABLES); do \
$(INSTALL) -m 0755 "$$path" "$(DESTDIR)$(RUNTIMEDIR)/$$path"; \
$(INSTALL) -m 0755 "$(RUNTIME_SOURCE_DIR)/$$path" "$(DESTDIR)$(RUNTIMEDIR)/$$path"; \
done
@for path in $(RUNTIME_DATA_FILES) $(RUNTIME_BOOT_FILES); do \
$(INSTALL) -m 0644 "$$path" "$(DESTDIR)$(RUNTIMEDIR)/$$path"; \
$(INSTALL) -m 0644 "$(RUNTIME_SOURCE_DIR)/$$path" "$(DESTDIR)$(RUNTIMEDIR)/$$path"; \
done
$(INSTALL) -m 0600 id_ed25519 "$(DESTDIR)$(RUNTIMEDIR)/id_ed25519"
cp -a "$(RUNTIME_MODULES_DIR)" "$(DESTDIR)$(RUNTIMEDIR)/wtf/root/lib/modules/"
@for path in $(RUNTIME_OPTIONAL_DATA_FILES); do \
if test -e "$(RUNTIME_SOURCE_DIR)/$$path"; then \
$(INSTALL) -m 0644 "$(RUNTIME_SOURCE_DIR)/$$path" "$(DESTDIR)$(RUNTIMEDIR)/$$path"; \
fi; \
done
chmod 0600 "$(DESTDIR)$(RUNTIMEDIR)/id_ed25519"
cp -a "$(RUNTIME_SOURCE_DIR)/$(RUNTIME_MODULES_DIR)" "$(DESTDIR)$(RUNTIMEDIR)/wtf/root/lib/modules/"
rootfs:
./make-rootfs.sh
BANGER_RUNTIME_DIR="$(abspath $(RUNTIME_SOURCE_DIR))" ./make-rootfs.sh

304
README.md
View file

@ -1,182 +1,206 @@
# banger
Minimal Firecracker launcher.
Persistent Firecracker development VMs managed through a Go daemon, CLI, and TUI.
## Requirements
- Linux host with KVM (`/dev/kvm` access)
- `sudo`, `ip`, `curl`, `ssh`, `jq`
- `dmsetup`, `losetup`, `blockdev` (device-mapper snapshot for rootfs)
- `e2cp`, `e2rm` (writes hostname and resolv.conf into rootfs snapshot)
- `dmsetup`, `losetup`, `blockdev`
- `e2cp`, `e2rm`, `debugfs`
- `mapdns`
## Files
- `firecracker`: Firecracker binary
- `wtf/root/boot/vmlinux-6.8.0-94-generic`: guest kernel
- `wtf/root/boot/initrd.img-6.8.0-94-generic`: guest initrd
- `wtf/root/lib/modules/6.8.0-94-generic/`: guest kernel modules
- `rootfs.ext4`: guest root filesystem (base image if present)
- `rootfs-docker.ext4`: docker-ready guest rootfs (built via `make-rootfs.sh`)
- `packages.apt`: apt packages baked into rebuilt guest images
- `id_ed25519`: SSH key for `root`
- `mapdns`: local DNS mapping CLI used to publish `<vm-name>.vm` → guest IP records
## Runtime Bundle
Runtime artifacts are no longer tracked directly in Git. Source checkouts use a
generated `./runtime/` bundle, while installed binaries use
`$(prefix)/lib/banger`.
## Run
```
./run.sh
The bundle contains:
- `firecracker`
- `wtf/root/boot/vmlinux-6.8.0-94-generic`
- `wtf/root/boot/initrd.img-6.8.0-94-generic`
- `wtf/root/lib/modules/6.8.0-94-generic/`
- `rootfs-docker.ext4`
- `rootfs.ext4` when present
- `packages.apt`
- `id_ed25519`
- the helper scripts used by image builds and installs
Bootstrap a source checkout explicitly:
```bash
make runtime-bundle
```
## Experimental Go Control Plane
There is now an XDG-based Go daemon + CLI prototype alongside the shell scripts.
It keeps persistent VM/image state in SQLite under your XDG state directory and
talks over a Unix socket under your XDG runtime directory.
`make runtime-bundle` reads [`runtime-bundle.toml`](/home/thales/projects/personal/banger/runtime-bundle.toml),
downloads the published bundle, verifies its SHA256, and unpacks it into
`./runtime/`. `make install` will not fetch artifacts for you. The manifest
must point at a published or locally staged bundle before bootstrap can work.
Build it with:
```
## Build
```bash
make runtime-bundle
make build
```
Or directly with Go:
```
go build -o ./banger ./cmd/banger
go build -o ./bangerd ./cmd/bangerd
Install into `~/.local/bin` by default, with the runtime bundle under
`~/.local/lib/banger`:
```bash
make install
```
Basic usage:
```
./banger daemon status
./banger tui
./banger vm list
./banger vm create --name calm-otter --disk-size 16G
./banger vm set calm-otter --memory 2048 --vcpu 4
./banger image list
After `make install`, the installed `banger` and `bangerd` do not need the repo
checkout to keep working.
## Basic VM Workflow
Create and boot a VM:
```bash
banger vm create --name calm-otter --disk-size 16G
```
Notes:
- `banger` auto-starts the per-user daemon when needed.
- `banger tui` launches a terminal UI for browsing, creating, editing, and operating VMs.
- VM configs are persistent by default.
- RAM, vCPU, and work-disk size edits are stopped-only.
- The Go image build path currently delegates guest customization to `customize.sh`.
## Run Options
```
./run.sh --name calm-otter --vcpu 4 --ram 2048 --overlay-size 12G
```
- `--name`: must be unique and match `[a-z0-9][a-z0-9-]{0,63}`.
- `--vcpu`: defaults to 2, max 16.
- `--ram`: MiB, defaults to 1024, max 32768.
- `--overlay-size`: writable dm-snapshot size for VM changes under `/`, including `/root` and `/var` (default: 8G).
- `--rootfs`: path to the rootfs image (default: `./rootfs-docker.ext4`).
- `--kernel`: path to the kernel image (default: `./wtf/root/boot/vmlinux-6.8.0-94-generic`).
- `--initrd`: path to the initrd image (default: `./wtf/root/boot/initrd.img-6.8.0-94-generic`).
## Storage Layout
- `rootfs.ext4` is used as the read-only origin for a per-VM device-mapper snapshot mounted as `/`.
- Each VM gets one sparse writable overlay file (`cow.ext4`) that stores its changes on top of the shared base image.
- `/root` and `/var` live inside that per-VM overlay, so VMs can install packages without copying separate disks per VM.
- `run.sh` masks stale `home.mount` and `var.mount` units at boot so older images with `/dev/vdb` and `/dev/vdc` entries in `/etc/fstab` still boot.
- `/run` and `/tmp` should be tmpfs via `/etc/fstab`.
## SSH
```
ssh -i "./id_ed25519" root@<guest_ip>
List VMs:
```bash
banger vm list
```
Shortcut:
```
./ssh.sh <vm-name-or-ip>
Inspect a VM:
```bash
banger vm show calm-otter
banger vm stats calm-otter
```
## VM DNS
- Spawned VMs register `<vm-name>.vm` → guest IP through `mapdns set`.
- VM teardown removes the mapping through `mapdns rm`.
- `mapdns` writes to `/home/thales/.local/share/mapdns/records.json`.
## Internet Access
VMs do not get internet access by default. You must enable forwarding and NAT:
```
./nat.sh up <id-or-name-prefix>
```
This enables `net.ipv4.ip_forward=1` and installs per-VM NAT rules for the VM's
guest IP and TAP device. To remove rules:
```
./nat.sh down <id-or-name-prefix>
```
Check status with:
```
./nat.sh status <id-or-name-prefix>
SSH into a running VM:
```bash
banger vm ssh calm-otter
```
## Shutdown
```
reboot
Stop, restart, kill, or delete it:
```bash
banger vm stop calm-otter
banger vm start calm-otter
banger vm restart calm-otter
banger vm kill --signal TERM calm-otter
banger vm delete calm-otter
```
## Customize Rootfs (Docker + Kernel Modules)
Use `customize.sh` to build a writable rootfs with Docker and kernel modules
preloaded so Docker works out of the box. Pass the base rootfs as a positional
argument; the output defaults to `docker-<base filename>` in the same directory
unless you pass `--out`.
Base guest packages come from `./packages.apt`. Edit that file to bake tools
like `vim` and `tmux` into rebuilt images.
```
./customize.sh ./rootfs.ext4 --size 6G --docker
Update stopped VM settings:
```bash
banger vm set calm-otter --memory 2048 --vcpu 4 --disk-size 32G
```
Options:
- `--size`: optional size for the output image.
- `--kernel`: kernel path (default: `./wtf/root/boot/vmlinux-6.8.0-94-generic`).
- `--initrd`: initrd path (default: `./wtf/root/boot/initrd.img-6.8.0-94-generic`).
- `--modules`: kernel modules directory (default: `./wtf/root/lib/modules/6.8.0-94-generic`).
- `--docker`: install Docker packages into the image.
- `--out`: output rootfs path (default: `docker-<base filename>`).
After boot, enable NAT and validate Docker:
```
./nat.sh up <id-or-name-prefix>
ssh -i "./id_ed25519" root@<guest_ip> "systemctl enable --now docker"
ssh -i "./id_ed25519" root@<guest_ip> "docker run --rm hello-world"
Launch the TUI:
```bash
banger tui
```
## Build Rootfs On Demand
`run.sh` defaults to `./rootfs-docker.ext4`. If it is missing, `run.sh` will
invoke `make-rootfs.sh` to build it.
## Daemon
The CLI auto-starts `bangerd` when needed.
```
./make-rootfs.sh
Useful daemon commands:
```bash
banger daemon status
banger daemon socket
banger daemon stop
```
`make-rootfs.sh` chooses the first available base image:
- `./rootfs.ext4`
State lives under XDG directories:
- config: `~/.config/banger`
- state: `~/.local/state/banger`
- cache: `~/.cache/banger`
- runtime socket: `$XDG_RUNTIME_DIR/banger/bangerd.sock`
If `./packages.apt` changes after `rootfs-docker.ext4` is built, `run.sh` will
warn and keep using the existing image. `make-rootfs.sh` will also warn and
exit without rebuilding while the image already exists.
Installed binaries resolve their runtime bundle from `../lib/banger` relative to
the executable. Source-checkout binaries resolve it from `./runtime` next to the
repo-built `./banger`. You can override either with `runtime_dir` in
`~/.config/banger/config.toml` or `BANGER_RUNTIME_DIR`.
To rebuild after package changes:
```
rm -f ./rootfs-docker.ext4 ./rootfs-docker.ext4.packages.sha256
./make-rootfs.sh
Useful config keys:
- `runtime_dir`
- `firecracker_bin`
- `ssh_key_path`
- `namegen_path`
- `customize_script`
- `default_rootfs`
- `default_base_rootfs`
- `default_kernel`
- `default_initrd`
- `default_modules_dir`
- `default_packages_file`
## Images
List images:
```bash
banger image list
```
## Interactive Customization
To create a writable copy and customize it manually over SSH (no automatic
package/config changes), use:
```
./interactive.sh ./rootfs-docker.ext4
Build a managed image:
```bash
banger image build --name docker-dev --docker
```
You can override the output path:
```
./interactive.sh ./rootfs-docker.ext4 --out ./my-rootfs.ext4
Show or delete images:
```bash
banger image show docker-dev
banger image delete docker-dev
```
## VM Info File
Each VM writes:
- `state/vms/<id>/vm.json`: local metadata under `.meta` plus the raw Firecracker config under `.config`.
`banger` auto-registers the bundled `default_rootfs` image when it exists. If
`rootfs.ext4` is not present in the bundle, `image build` falls back to using
`rootfs-docker.ext4` as its default base image.
## Log Notes
- `PCI: Fatal: No config space access function found` and `MissingAddressRange` lines are expected with `pci=off` in `run.sh`.
- `SELinux: Could not open policy file ...` is expected in the minimal rootfs.
## Networking And DNS
Enable NAT when creating or updating a VM:
```bash
banger vm create --name web --nat
banger vm set web --nat
banger vm set web --no-nat
```
For daemon-managed VMs, NAT is applied directly by `bangerd` using host `iptables`
rules derived from the VM's current guest IP and TAP device.
Running VMs are published as `<vm-name>.vm` through `mapdns`.
## Storage Model
- VMs share a read-only base rootfs image.
- Each VM gets its own sparse writable system overlay for `/`.
- Each VM gets its own persistent ext4 work disk mounted at `/root`.
- Stopping a VM preserves its overlay and work disk.
## Rebuilding The Repo Default Rootfs
`packages.apt` controls the base apt packages baked into rebuilt images.
To rebuild the source-checkout default image in `./runtime/rootfs-docker.ext4`:
```bash
make rootfs
```
If the package manifest changed and you want a fresh source-checkout image:
```bash
rm -f ./runtime/rootfs-docker.ext4 ./runtime/rootfs-docker.ext4.packages.sha256
make rootfs
```
`make rootfs` expects a bootstrapped runtime bundle. If `./runtime/rootfs.ext4`
is not available, pass an explicit `--base-rootfs` to `./make-rootfs.sh`.
## Maintaining The Runtime Bundle
Maintain the checked-in manifest in [`runtime-bundle.toml`](/home/thales/projects/personal/banger/runtime-bundle.toml)
with the published bundle URL and SHA256.
Package a local `./runtime/` tree for publication:
```bash
make runtime-package
```
That writes `dist/banger-runtime.tar.gz` and prints its SHA256 so you can update
the manifest before publishing or testing bootstrap changes.
## Remaining Shell Helpers
The runtime VM lifecycle is managed through `banger`. The remaining shell scripts are not the primary user interface:
- `customize.sh`: implementation used by `banger image build`; it now reads
assets from `BANGER_RUNTIME_DIR` and stores transient state under
`BANGER_STATE_DIR`/XDG state
- `make-rootfs.sh`: convenience wrapper for rebuilding `./runtime/rootfs-docker.ext4`
- `interactive.sh`: manual one-off rootfs customization over SSH
- `nat.sh`: legacy host NAT helper used by the shell customization flows
- `packages.sh`, `dns.sh`: shell helper libraries
- `verify.sh`: smoke test for the Go workflow (`./verify.sh --nat` adds NAT coverage)

72
cmd/runtimebundle/main.go Normal file
View file

@ -0,0 +1,72 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"banger/internal/runtimebundle"
)
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "fetch":
if err := fetch(os.Args[2:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
case "package":
if err := pkg(os.Args[2:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
default:
usage()
os.Exit(2)
}
}
func fetch(args []string) error {
fs := flag.NewFlagSet("fetch", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
manifestPath := fs.String("manifest", "runtime-bundle.toml", "path to the runtime bundle manifest")
outDir := fs.String("out", "runtime", "destination runtime directory")
if err := fs.Parse(args); err != nil {
return err
}
manifest, err := runtimebundle.LoadManifest(*manifestPath)
if err != nil {
return err
}
return runtimebundle.Bootstrap(context.Background(), manifest, *manifestPath, *outDir)
}
func pkg(args []string) error {
fs := flag.NewFlagSet("package", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
manifestPath := fs.String("manifest", "runtime-bundle.toml", "path to the runtime bundle manifest")
runtimeDir := fs.String("runtime-dir", "runtime", "runtime directory to package")
outArchive := fs.String("out", "dist/banger-runtime.tar.gz", "output archive path")
if err := fs.Parse(args); err != nil {
return err
}
manifest, err := runtimebundle.LoadManifest(*manifestPath)
if err != nil {
return err
}
sum, err := runtimebundle.Package(*runtimeDir, *outArchive, manifest)
if err != nil {
return err
}
fmt.Println(sum)
return nil
}
func usage() {
fmt.Fprintln(os.Stderr, "usage: runtimebundle <fetch|package> [flags]")
}

View file

@ -29,7 +29,17 @@ parse_size() {
return 1
}
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_RUNTIME_DIR="$SCRIPT_DIR"
if [[ -d "$SCRIPT_DIR/runtime" ]]; then
DEFAULT_RUNTIME_DIR="$SCRIPT_DIR/runtime"
fi
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
if [[ ! -d "$RUNTIME_DIR" ]]; then
log "runtime bundle not found: $RUNTIME_DIR"
log "run 'make runtime-bundle' or set BANGER_RUNTIME_DIR"
exit 1
fi
source "$RUNTIME_DIR/dns.sh"
source "$RUNTIME_DIR/packages.sh"
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/image-build}"

Binary file not shown.

View file

@ -1,7 +0,0 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBCcKLE0FGsW007R6s2MvgYaA/E2KhBwEVy3jZ5l60OMQAAAJDph86L6YfO
iwAAAAtzc2gtZWQyNTUxOQAAACBCcKLE0FGsW007R6s2MvgYaA/E2KhBwEVy3jZ5l60OMQ
AAAECwXc/eHLI6iZ4vF0TF4gHU7/FigvePPD4xcsnzSNbO/kJwosTQUaxbTTtHqzYy+Bho
D8TYqEHARXLeNnmXrQ4xAAAAC3RoYWxlc0BtYWluAQI=
-----END OPENSSH PRIVATE KEY-----

View file

@ -31,15 +31,25 @@ parse_size() {
}
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$DIR/dns.sh"
STATE="$DIR/state"
DEFAULT_RUNTIME_DIR="$DIR"
if [[ -d "$DIR/runtime" ]]; then
DEFAULT_RUNTIME_DIR="$DIR/runtime"
fi
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
if [[ ! -d "$RUNTIME_DIR" ]]; then
log "runtime bundle not found: $RUNTIME_DIR"
log "run 'make runtime-bundle' or set BANGER_RUNTIME_DIR"
exit 1
fi
source "$RUNTIME_DIR/dns.sh"
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/interactive}"
VM_ROOT="$STATE/vms"
mkdir -p "$VM_ROOT"
FC_BIN="$DIR/firecracker"
KERNEL="$DIR/wtf/root/boot/vmlinux-6.8.0-94-generic"
INITRD="$DIR/wtf/root/boot/initrd.img-6.8.0-94-generic"
SSH_KEY="$DIR/id_ed25519"
FC_BIN="$RUNTIME_DIR/firecracker"
KERNEL="$RUNTIME_DIR/wtf/root/boot/vmlinux-6.8.0-94-generic"
INITRD="$RUNTIME_DIR/wtf/root/boot/initrd.img-6.8.0-94-generic"
SSH_KEY="$RUNTIME_DIR/id_ed25519"
BR_DEV="br-fc"
BR_IP="172.16.0.1"

View file

@ -9,6 +9,7 @@ import (
"banger/internal/api"
"banger/internal/model"
"banger/internal/paths"
)
func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (model.Image, error) {
@ -27,7 +28,7 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (m
baseRootfs = d.config.DefaultBaseRootfs
}
if baseRootfs == "" {
return model.Image{}, fmt.Errorf("base rootfs is required")
return model.Image{}, fmt.Errorf("base rootfs is required; %s", paths.RuntimeBundleHint())
}
id, err := model.NewID()
if err != nil {
@ -41,10 +42,10 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (m
rootfsPath := filepath.Join(artifactDir, "rootfs.ext4")
script := d.config.CustomizeScript
if script == "" {
return model.Image{}, fmt.Errorf("customize script not configured; set runtime_dir or customize_script in config.toml")
return model.Image{}, fmt.Errorf("customize script not configured; %s", paths.RuntimeBundleHint())
}
if _, err := os.Stat(script); err != nil {
return model.Image{}, fmt.Errorf("customize.sh not found at %s", script)
return model.Image{}, fmt.Errorf("customize.sh not found at %s; %s", script, paths.RuntimeBundleHint())
}
args := []string{script, baseRootfs, "--out", rootfsPath}
if params.Size != "" {

View file

@ -13,6 +13,7 @@ import (
"banger/internal/api"
"banger/internal/firecracker"
"banger/internal/model"
"banger/internal/paths"
"banger/internal/system"
)
@ -275,6 +276,49 @@ func (d *Daemon) StopVM(ctx context.Context, idOrName string) (model.VMRecord, e
return vm, nil
}
func (d *Daemon) KillVM(ctx context.Context, params api.VMKillParams) (model.VMRecord, error) {
d.mu.Lock()
defer d.mu.Unlock()
vm, err := d.FindVM(ctx, params.IDOrName)
if err != nil {
return model.VMRecord{}, err
}
if vm.State != model.VMStateRunning || !system.ProcessRunning(vm.Runtime.PID, vm.Runtime.APISockPath) {
if err := d.cleanupRuntime(ctx, vm, true); err != nil {
return model.VMRecord{}, err
}
vm.State = model.VMStateStopped
vm.Runtime.State = model.VMStateStopped
clearRuntimeHandles(&vm)
if err := d.store.UpsertVM(ctx, vm); err != nil {
return model.VMRecord{}, err
}
return vm, nil
}
signal := strings.TrimSpace(params.Signal)
if signal == "" {
signal = "TERM"
}
if _, err := d.runner.RunSudo(ctx, "kill", "-"+signal, strconv.Itoa(vm.Runtime.PID)); err != nil {
return model.VMRecord{}, err
}
if err := d.waitForExit(ctx, vm.Runtime.PID, vm.Runtime.APISockPath, 30*time.Second); err != nil {
return model.VMRecord{}, err
}
if err := d.cleanupRuntime(ctx, vm, true); err != nil {
return model.VMRecord{}, err
}
vm.State = model.VMStateStopped
vm.Runtime.State = model.VMStateStopped
clearRuntimeHandles(&vm)
system.TouchNow(&vm)
if err := d.store.UpsertVM(ctx, vm); err != nil {
return model.VMRecord{}, err
}
return vm, nil
}
func (d *Daemon) RestartVM(ctx context.Context, idOrName string) (model.VMRecord, error) {
vm, err := d.StopVM(ctx, idOrName)
@ -547,11 +591,11 @@ func (d *Daemon) createTap(ctx context.Context, tap string) error {
func (d *Daemon) firecrackerBinary() (string, error) {
if d.config.FirecrackerBin == "" {
return "", errors.New("firecracker binary not configured; set runtime_dir or firecracker_bin in config.toml")
return "", fmt.Errorf("firecracker binary not configured; %s", paths.RuntimeBundleHint())
}
path := d.config.FirecrackerBin
if !exists(path) {
return "", fmt.Errorf("firecracker binary not found at %s", path)
return "", fmt.Errorf("firecracker binary not found at %s; %s", path, paths.RuntimeBundleHint())
}
return path, nil
}

View file

@ -89,8 +89,9 @@ func ResolveRuntimeDir(configuredRuntimeDir, deprecatedRepoRoot string) string {
return installRuntimeDir
}
}
if HasRuntimeBundle(exeDir) {
return exeDir
sourceRuntimeDir := filepath.Join(exeDir, "runtime")
if HasRuntimeBundle(sourceRuntimeDir) {
return sourceRuntimeDir
}
return ""
}
@ -138,6 +139,10 @@ func BangerdPath() (string, error) {
return "", errors.New("bangerd binary not found next to banger; build ./cmd/bangerd")
}
func RuntimeBundleHint() string {
return "run `make runtime-bundle` or set runtime_dir in ~/.config/banger/config.toml"
}
func getenvDefault(key, fallback string) string {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value

View file

@ -32,9 +32,10 @@ func TestResolveRuntimeDirUsesInstalledLayout(t *testing.T) {
}
}
func TestResolveRuntimeDirUsesExecutableDirectoryBundle(t *testing.T) {
func TestResolveRuntimeDirUsesSourceCheckoutRuntimeSubdir(t *testing.T) {
root := t.TempDir()
createRuntimeBundle(t, root)
runtimeDir := filepath.Join(root, "runtime")
createRuntimeBundle(t, runtimeDir)
origExecutablePath := executablePath
executablePath = func() (string, error) {
@ -44,8 +45,8 @@ func TestResolveRuntimeDirUsesExecutableDirectoryBundle(t *testing.T) {
executablePath = origExecutablePath
})
if got := ResolveRuntimeDir("", ""); got != root {
t.Fatalf("ResolveRuntimeDir() = %q, want %q", got, root)
if got := ResolveRuntimeDir("", ""); got != runtimeDir {
t.Fatalf("ResolveRuntimeDir() = %q, want %q", got, runtimeDir)
}
}

View file

@ -0,0 +1,326 @@
package runtimebundle
import (
"archive/tar"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
toml "github.com/pelletier/go-toml"
)
type Manifest struct {
Version string `toml:"version"`
URL string `toml:"url"`
SHA256 string `toml:"sha256"`
BundleRoot string `toml:"bundle_root"`
RequiredPaths []string `toml:"required_paths"`
}
func LoadManifest(path string) (Manifest, error) {
data, err := os.ReadFile(path)
if err != nil {
return Manifest{}, err
}
var manifest Manifest
if err := toml.Unmarshal(data, &manifest); err != nil {
return Manifest{}, err
}
manifest.BundleRoot = strings.TrimSpace(manifest.BundleRoot)
manifest.URL = strings.TrimSpace(manifest.URL)
manifest.SHA256 = strings.ToLower(strings.TrimSpace(manifest.SHA256))
for i, required := range manifest.RequiredPaths {
manifest.RequiredPaths[i] = filepath.Clean(strings.TrimSpace(required))
}
sort.Strings(manifest.RequiredPaths)
if len(manifest.RequiredPaths) == 0 {
return Manifest{}, fmt.Errorf("runtime bundle manifest %s has no required_paths", path)
}
return manifest, nil
}
func Bootstrap(ctx context.Context, manifest Manifest, manifestPath, outDir string) error {
if manifest.URL == "" {
return fmt.Errorf("runtime bundle manifest %s has no url; publish a runtime bundle and update the manifest", manifestPath)
}
if manifest.SHA256 == "" {
return fmt.Errorf("runtime bundle manifest %s has no sha256", manifestPath)
}
manifestDir := filepath.Dir(manifestPath)
parentDir := filepath.Dir(outDir)
if err := os.MkdirAll(parentDir, 0o755); err != nil {
return err
}
workDir, err := os.MkdirTemp(parentDir, ".runtime-bundle-*")
if err != nil {
return err
}
defer os.RemoveAll(workDir)
archivePath := filepath.Join(workDir, "bundle.tar.gz")
if err := downloadArchive(ctx, resolveSource(manifestDir, manifest.URL), archivePath); err != nil {
return err
}
sum, err := fileSHA256(archivePath)
if err != nil {
return err
}
if sum != manifest.SHA256 {
return fmt.Errorf("runtime bundle checksum mismatch: got %s want %s", sum, manifest.SHA256)
}
extractDir := filepath.Join(workDir, "extract")
if err := extractTarGz(archivePath, extractDir); err != nil {
return err
}
bundleDir := extractDir
if manifest.BundleRoot != "" {
bundleDir = filepath.Join(extractDir, manifest.BundleRoot)
}
if err := ValidateBundle(bundleDir, manifest.RequiredPaths); err != nil {
return err
}
stageDir := filepath.Join(workDir, "stage")
if err := os.Rename(bundleDir, stageDir); err != nil {
return err
}
if err := os.RemoveAll(outDir); err != nil {
return err
}
if err := os.Rename(stageDir, outDir); err != nil {
return err
}
return nil
}
func ValidateBundle(bundleDir string, requiredPaths []string) error {
for _, rel := range requiredPaths {
if rel == "." || strings.HasPrefix(rel, "..") {
return fmt.Errorf("invalid required bundle path: %s", rel)
}
if _, err := os.Stat(filepath.Join(bundleDir, rel)); err != nil {
return fmt.Errorf("runtime bundle missing %s", rel)
}
}
return nil
}
func Package(runtimeDir, outArchive string, manifest Manifest) (string, error) {
if err := ValidateBundle(runtimeDir, manifest.RequiredPaths); err != nil {
return "", err
}
if err := os.MkdirAll(filepath.Dir(outArchive), 0o755); err != nil {
return "", err
}
file, err := os.Create(outArchive)
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
multi := io.MultiWriter(file, hash)
gz := gzip.NewWriter(multi)
defer gz.Close()
tw := tar.NewWriter(gz)
defer tw.Close()
for _, rel := range manifest.RequiredPaths {
if err := addPathToArchive(tw, runtimeDir, manifest.BundleRoot, rel); err != nil {
return "", err
}
}
if err := tw.Close(); err != nil {
return "", err
}
if err := gz.Close(); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func addPathToArchive(tw *tar.Writer, runtimeDir, bundleRoot, rel string) error {
srcPath := filepath.Join(runtimeDir, rel)
info, err := os.Lstat(srcPath)
if err != nil {
return err
}
archiveName := rel
if bundleRoot != "" {
archiveName = filepath.Join(bundleRoot, rel)
}
if info.IsDir() {
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
header.Name = filepath.ToSlash(archiveName) + "/"
if err := tw.WriteHeader(header); err != nil {
return err
}
entries, err := os.ReadDir(srcPath)
if err != nil {
return err
}
for _, entry := range entries {
childRel := filepath.Join(rel, entry.Name())
if err := addPathToArchive(tw, runtimeDir, bundleRoot, childRel); err != nil {
return err
}
}
return nil
}
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
header.Name = filepath.ToSlash(archiveName)
if err := tw.WriteHeader(header); err != nil {
return err
}
file, err := os.Open(srcPath)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(tw, file)
return err
}
func resolveSource(manifestDir, source string) string {
parsed, err := url.Parse(source)
if err == nil && parsed.Scheme != "" {
return source
}
if filepath.IsAbs(source) {
return source
}
return filepath.Join(manifestDir, source)
}
func downloadArchive(ctx context.Context, source, dst string) error {
switch {
case strings.HasPrefix(source, "http://"), strings.HasPrefix(source, "https://"):
req, err := http.NewRequestWithContext(ctx, http.MethodGet, source, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download runtime bundle: %s", resp.Status)
}
return writeFileFromReader(dst, resp.Body)
case strings.HasPrefix(source, "file://"):
parsed, err := url.Parse(source)
if err != nil {
return err
}
return copyFile(parsed.Path, dst)
default:
return copyFile(source, dst)
}
}
func writeFileFromReader(dst string, reader io.Reader) error {
file, err := os.Create(dst)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, reader)
return err
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
return writeFileFromReader(dst, in)
}
func extractTarGz(archivePath, outDir string) error {
if err := os.MkdirAll(outDir, 0o755); err != nil {
return err
}
file, err := os.Open(archivePath)
if err != nil {
return err
}
defer file.Close()
gz, err := gzip.NewReader(file)
if err != nil {
return err
}
defer gz.Close()
tr := tar.NewReader(gz)
for {
header, err := tr.Next()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
name := filepath.Clean(header.Name)
if name == "." || strings.HasPrefix(name, "..") || filepath.IsAbs(name) {
return fmt.Errorf("invalid archive entry: %s", header.Name)
}
target := filepath.Join(outDir, name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
return err
}
case tar.TypeReg, tar.TypeRegA:
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
file, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return err
}
if _, err := io.Copy(file, tr); err != nil {
file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
default:
return fmt.Errorf("unsupported archive entry type: %s", header.Name)
}
}
}
func fileSHA256(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}

View file

@ -0,0 +1,150 @@
package runtimebundle
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
)
func TestBootstrapExtractsBundleAndValidatesChecksum(t *testing.T) {
manifestDir := t.TempDir()
bundleData := buildArchive(t, map[string]string{
"runtime/firecracker": "fc",
"runtime/customize.sh": "#!/bin/bash\n",
"runtime/packages.apt": "vim\n",
"runtime/rootfs-docker.ext4": "rootfs",
"runtime/wtf/root/boot/vmlinux-6.8.0-94-generic": "kernel",
"runtime/wtf/root/boot/initrd.img-6.8.0-94-generic": "initrd",
"runtime/wtf/root/lib/modules/6.8.0-94-generic/modules.dep": "dep",
})
archivePath := filepath.Join(manifestDir, "bundle.tar.gz")
if err := os.WriteFile(archivePath, bundleData, 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
manifest := Manifest{
URL: "./bundle.tar.gz",
SHA256: sha256Hex(bundleData),
BundleRoot: "runtime",
RequiredPaths: []string{"firecracker", "customize.sh", "packages.apt", "rootfs-docker.ext4", "wtf/root/boot/vmlinux-6.8.0-94-generic", "wtf/root/lib/modules/6.8.0-94-generic"},
}
outDir := filepath.Join(t.TempDir(), "runtime")
if err := Bootstrap(context.Background(), manifest, filepath.Join(manifestDir, "runtime-bundle.toml"), outDir); err != nil {
t.Fatalf("Bootstrap: %v", err)
}
for _, rel := range manifest.RequiredPaths {
if _, err := os.Stat(filepath.Join(outDir, rel)); err != nil {
t.Fatalf("runtime missing %s: %v", rel, err)
}
}
}
func TestBootstrapRejectsChecksumMismatch(t *testing.T) {
manifestDir := t.TempDir()
archivePath := filepath.Join(manifestDir, "bundle.tar.gz")
if err := os.WriteFile(archivePath, []byte("not-a-tarball"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
manifest := Manifest{
URL: "./bundle.tar.gz",
SHA256: strings.Repeat("0", 64),
BundleRoot: "runtime",
RequiredPaths: []string{"firecracker"},
}
err := Bootstrap(context.Background(), manifest, filepath.Join(manifestDir, "runtime-bundle.toml"), filepath.Join(t.TempDir(), "runtime"))
if err == nil || !strings.Contains(err.Error(), "checksum mismatch") {
t.Fatalf("Bootstrap() error = %v, want checksum mismatch", err)
}
}
func TestPackageWritesArchive(t *testing.T) {
runtimeDir := t.TempDir()
for _, rel := range []string{
"firecracker",
"customize.sh",
"packages.apt",
"rootfs-docker.ext4",
"wtf/root/boot/vmlinux-6.8.0-94-generic",
"wtf/root/boot/initrd.img-6.8.0-94-generic",
"wtf/root/lib/modules/6.8.0-94-generic",
} {
path := filepath.Join(runtimeDir, rel)
if rel == "wtf/root/lib/modules/6.8.0-94-generic" {
if err := os.MkdirAll(path, 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(filepath.Join(path, "modules.dep"), []byte(rel), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
continue
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("MkdirAll: %v", err)
}
if err := os.WriteFile(path, []byte(rel), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
}
manifest := Manifest{
BundleRoot: "runtime",
RequiredPaths: []string{
"firecracker",
"customize.sh",
"packages.apt",
"rootfs-docker.ext4",
"wtf/root/boot/vmlinux-6.8.0-94-generic",
"wtf/root/boot/initrd.img-6.8.0-94-generic",
"wtf/root/lib/modules/6.8.0-94-generic",
},
}
outArchive := filepath.Join(t.TempDir(), "bundle.tar.gz")
sum, err := Package(runtimeDir, outArchive, manifest)
if err != nil {
t.Fatalf("Package: %v", err)
}
if sum == "" {
t.Fatalf("Package() returned empty checksum")
}
if _, err := os.Stat(outArchive); err != nil {
t.Fatalf("archive missing: %v", err)
}
}
func buildArchive(t *testing.T, files map[string]string) []byte {
t.Helper()
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
tw := tar.NewWriter(gz)
for name, contents := range files {
header := &tar.Header{
Name: name,
Mode: 0o644,
Size: int64(len(contents)),
}
if err := tw.WriteHeader(header); err != nil {
t.Fatalf("WriteHeader(%s): %v", name, err)
}
if _, err := tw.Write([]byte(contents)); err != nil {
t.Fatalf("Write(%s): %v", name, err)
}
}
if err := tw.Close(); err != nil {
t.Fatalf("Close tar: %v", err)
}
if err := gz.Close(); err != nil {
t.Fatalf("Close gzip: %v", err)
}
return buf.Bytes()
}
func sha256Hex(data []byte) string {
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}

View file

@ -19,7 +19,13 @@ EOF
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$DIR/packages.sh"
OUT_ROOTFS="$DIR/rootfs-docker.ext4"
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DIR/runtime}"
if [[ ! -d "$RUNTIME_DIR" ]]; then
log "runtime bundle not found: $RUNTIME_DIR"
log "run 'make runtime-bundle' or set BANGER_RUNTIME_DIR"
exit 1
fi
OUT_ROOTFS="$RUNTIME_DIR/rootfs-docker.ext4"
SIZE_SPEC="6G"
BASE_ROOTFS=""
@ -55,20 +61,22 @@ if [[ -f "$OUT_ROOTFS" ]]; then
fi
if [[ -z "$BASE_ROOTFS" ]]; then
if [[ -f "$DIR/rootfs.ext4" ]]; then
BASE_ROOTFS="$DIR/rootfs.ext4"
if [[ -f "$RUNTIME_DIR/rootfs.ext4" ]]; then
BASE_ROOTFS="$RUNTIME_DIR/rootfs.ext4"
elif [[ -f "$DIR/ubuntu-noble-rootfs/rootfs.ext4" ]]; then
BASE_ROOTFS="$DIR/ubuntu-noble-rootfs/rootfs.ext4"
elif [[ -f "$DIR/ubuntu-lts/rootfs.ext4" ]]; then
BASE_ROOTFS="$DIR/ubuntu-lts/rootfs.ext4"
else
log "no base rootfs found"
log "no base rootfs found; run 'make runtime-bundle' or pass --base-rootfs"
exit 1
fi
fi
mkdir -p "$RUNTIME_DIR"
log "building $OUT_ROOTFS from $BASE_ROOTFS"
exec "$DIR/customize.sh" "$BASE_ROOTFS" \
exec env BANGER_RUNTIME_DIR="$RUNTIME_DIR" "$DIR/customize.sh" "$BASE_ROOTFS" \
--out "$OUT_ROOTFS" \
--size "$SIZE_SPEC" \
--docker

415
namegen
View file

@ -1,415 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ADJECTIVES=(
ace
apt
fit
fun
odd
top
able
beau
bold
calm
chic
cool
deep
deft
easy
epic
fair
fine
free
full
game
glad
glow
good
holy
keen
kind
lean
mild
neat
nice
open
pure
real
snug
spry
tidy
true
warm
wavy
wise
adept
agile
alert
alive
ample
angel
awake
aware
brave
brisk
chill
clean
clear
close
comic
eager
elite
first
fleet
fresh
grace
grand
great
happy
hardy
ideal
jolly
light
lithe
loyal
lucid
lucky
lunar
magic
merry
nifty
noble
peppy
perky
proud
quick
quiet
ready
regal
savvy
sharp
smart
solid
sound
sunny
super
sweet
swift
vivid
witty
zesty
)
SUBSTANTIVES=(
ox
aim
air
arm
bud
day
hay
jam
jay
joy
key
map
may
nod
ore
pen
sky
sun
way
zen
ant
ape
auk
bat
bee
cat
cod
cow
dog
elk
fox
hen
owl
pig
ram
rat
yak
boar
buck
bull
calf
carp
crab
crow
deer
dove
fish
foal
frog
goat
gull
hare
hawk
ibex
kiwi
kudu
lamb
lion
lynx
mink
mole
mule
newt
orca
oryx
puma
seal
slug
stag
swan
tern
toad
tuna
wasp
wolf
zebu
bison
camel
crane
eagle
finch
goose
heron
hippo
horse
hyena
koala
llama
macaw
moose
otter
quail
raven
robin
shark
sheep
shrew
skunk
sloth
snail
squid
tapir
tiger
trout
whale
zebra
ally
arch
area
aura
axis
bank
barn
beam
bell
belt
bend
bird
boat
bond
book
boot
bowl
brim
calm
camp
card
care
cell
city
clan
club
code
core
crux
dawn
deal
film
firm
flag
flow
foam
gate
gift
glow
hall
hand
harp
hill
home
hope
host
idea
isle
item
keel
knot
land
leaf
link
lion
loom
love
luck
mark
moon
moss
nook
note
pact
page
path
peak
poem
port
ring
road
rock
roof
rule
sail
seal
seed
song
star
tide
tree
tune
walk
ward
wave
well
wind
wing
wish
wood
work
zone
amity
asset
bloom
brook
bunch
charm
chart
cheer
chord
cliff
cloud
coast
comet
craft
crane
crest
crowd
crown
cycle
faith
field
flame
fleet
focus
forge
frame
fruit
glade
grace
grain
grove
guide
guild
haven
heart
honey
honor
humor
image
index
jewel
judge
kudos
lumen
lunar
magic
march
marsh
mercy
model
moral
music
niche
oasis
ocean
opera
orbit
order
peace
pearl
petal
phase
piano
pilot
place
plaza
prism
proof
pulse
quest
quiet
quill
radar
rally
range
realm
reign
river
route
scene
scope
score
shade
shape
shore
skill
spark
spice
spire
spoke
stone
story
table
token
trend
tribe
trust
unity
valor
value
verse
vista
voice
world
)
rand() {
echo $(( RANDOM % $1 ))
}
adjective="${ADJECTIVES[$(rand ${#ADJECTIVES[@]})]}"
substantive="${SUBSTANTIVES[$(rand ${#SUBSTANTIVES[@]})]}"
printf '%s-%s' "$adjective" "$substantive"

20
runtime-bundle.toml Normal file
View file

@ -0,0 +1,20 @@
# Update `url` and `sha256` to the published runtime bundle before using
# `make runtime-bundle` in a fresh checkout.
version = "v0"
url = ""
sha256 = ""
bundle_root = "runtime"
required_paths = [
"firecracker",
"customize.sh",
"dns.sh",
"packages.sh",
"nat.sh",
"namegen",
"packages.apt",
"id_ed25519",
"rootfs-docker.ext4",
"wtf/root/boot/vmlinux-6.8.0-94-generic",
"wtf/root/boot/initrd.img-6.8.0-94-generic",
"wtf/root/lib/modules/6.8.0-94-generic",
]

159
verify.sh
View file

@ -5,70 +5,149 @@ log() {
printf '[verify] %s\n' "$*"
}
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_RUNTIME_DIR="$DIR"
if [[ -d "$DIR/runtime" ]]; then
DEFAULT_RUNTIME_DIR="$DIR/runtime"
fi
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
SSH_KEY="$RUNTIME_DIR/id_ed25519"
if [[ ! -d "$RUNTIME_DIR" ]]; then
log "runtime bundle not found: $RUNTIME_DIR"
log "run 'make runtime-bundle' or set BANGER_RUNTIME_DIR"
exit 1
fi
if [[ ! -f "$SSH_KEY" ]]; then
log "ssh key not found: $SSH_KEY"
exit 1
fi
wait_for_ssh() {
local guest_ip="$1"
local deadline=$((SECONDS + 60))
while ((SECONDS < deadline)); do
if ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=2 "root@${guest_ip}" "true" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
return 1
}
usage() {
cat <<'EOF'
Usage: ./verify.sh [--nat]
Run a basic smoke test for the Go VM workflow.
Use --nat to additionally verify outbound NAT and host rule cleanup.
EOF
}
NAT_ENABLED=0
if [[ "${1:-}" == "--nat" ]]; then
NAT_ENABLED=1
shift
fi
if (($# != 0)); then
usage
exit 1
fi
VM_NAME="verify-$(date +%s)"
VM_JSON=""
TAP=""
VM_DIR=""
GUEST_IP=""
UPLINK=""
cleanup() {
if [[ -z "${VM_JSON:-}" || ! -f "$VM_JSON" ]]; then
return
fi
pid="$(jq -r '.meta.pid // empty' "$VM_JSON")"
tap="$(jq -r '.meta.tap // empty' "$VM_JSON")"
vm_dir="$(dirname "$VM_JSON")"
if [[ -n "$pid" ]]; then
sudo kill "$pid" 2>/dev/null || true
fi
if [[ -n "$tap" ]]; then
sudo ip link del "$tap" 2>/dev/null || true
fi
if [[ -n "$vm_dir" ]]; then
rm -rf "$vm_dir"
if [[ -n "${VM_NAME:-}" ]]; then
./banger vm delete "$VM_NAME" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
log "starting VM"
if ! ./run.sh; then
log "run.sh failed"
CREATE_ARGS=(./banger vm create --name "$VM_NAME")
if (( NAT_ENABLED )); then
CREATE_ARGS+=(--nat)
fi
"${CREATE_ARGS[@]}" >/dev/null
VM_JSON="$(./banger vm show "$VM_NAME")"
name="$(printf '%s\n' "$VM_JSON" | jq -r '.name // empty')"
guest_ip="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.guest_ip // empty')"
tap="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.tap_device // empty')"
vm_dir="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.vm_dir // empty')"
if [[ -z "$name" || -z "$guest_ip" || -z "$tap" || -z "$vm_dir" ]]; then
log "missing VM metadata from banger vm show"
exit 1
fi
VM_DIR="$(find state/vms -maxdepth 1 -mindepth 1 -type d -printf '%T@ %p\n' 2>/dev/null | sort -nr | head -n 1 | awk '{print $2}')"
if [[ -z "$VM_DIR" ]]; then
log "no VM state directory found"
TAP="$tap"
VM_DIR="$vm_dir"
GUEST_IP="$guest_ip"
if (( NAT_ENABLED )); then
UPLINK="$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')"
if [[ -z "$UPLINK" ]]; then
log "failed to detect uplink interface"
exit 1
fi
VM_JSON="$VM_DIR/vm.json"
if [[ ! -f "$VM_JSON" ]]; then
log "vm.json not found: $VM_JSON"
exit 1
fi
name="$(jq -r '.meta.name // empty' "$VM_JSON")"
created_at="$(jq -r '.meta.created_at // empty' "$VM_JSON")"
guest_ip="$(jq -r '.meta.guest_ip // empty' "$VM_JSON")"
tap="$(jq -r '.meta.tap // empty' "$VM_JSON")"
pid="$(jq -r '.meta.pid // empty' "$VM_JSON")"
vm_dir="$VM_DIR"
if [[ -z "$name" || -z "$created_at" || -z "$guest_ip" ]]; then
log "missing name or created_at in vm.json"
exit 1
log "asserting NAT rules are installed"
sudo iptables -t nat -C POSTROUTING -s "${GUEST_IP}/32" -o "$UPLINK" -j MASQUERADE
sudo iptables -C FORWARD -i "$TAP" -o "$UPLINK" -j ACCEPT
sudo iptables -C FORWARD -i "$UPLINK" -o "$TAP" -m state --state RELATED,ESTABLISHED -j ACCEPT
fi
log "asserting VM is reachable via SSH"
ssh -i "./id_ed25519" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
if ! wait_for_ssh "$guest_ip"; then
log "ssh did not become ready for ${guest_ip}"
exit 1
fi
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"root@${guest_ip}" "uname -a" >/dev/null
if (( NAT_ENABLED )); then
log "asserting VM has outbound network access"
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"root@${guest_ip}" "curl -fsS https://example.com >/dev/null" >/dev/null
fi
log "cleaning up VM"
cleanup
log "asserting cleanup success"
if ip link show "$tap" >/dev/null 2>&1; then
log "tap still exists: $tap"
if ./banger vm show "$VM_NAME" >/dev/null 2>&1; then
log "vm still exists after delete: $VM_NAME"
exit 1
fi
if [[ -d "$vm_dir" ]]; then
log "vm dir still exists: $vm_dir"
if ip link show "$TAP" >/dev/null 2>&1; then
log "tap still exists: $TAP"
exit 1
fi
if [[ -d "$VM_DIR" ]]; then
log "vm dir still exists: $VM_DIR"
exit 1
fi
if (( NAT_ENABLED )); then
if sudo iptables -t nat -C POSTROUTING -s "${GUEST_IP}/32" -o "$UPLINK" -j MASQUERADE 2>/dev/null; then
log "nat rule still exists for ${GUEST_IP}"
exit 1
fi
if sudo iptables -C FORWARD -i "$TAP" -o "$UPLINK" -j ACCEPT 2>/dev/null; then
log "forward-out rule still exists for ${TAP}"
exit 1
fi
if sudo iptables -C FORWARD -i "$UPLINK" -o "$TAP" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
log "forward-in rule still exists for ${TAP}"
exit 1
fi
fi
log "ok"

Some files were not shown because too many files have changed in this diff Show more