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:
parent
ce1be52047
commit
238bb8a020
6512 changed files with 1019 additions and 65372 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,4 +1,8 @@
|
|||
state/
|
||||
/runtime/
|
||||
/dist/
|
||||
/banger
|
||||
/bangerd
|
||||
*.log
|
||||
*.sock
|
||||
*.pid
|
||||
|
|
|
|||
46
AGENTS.md
46
AGENTS.md
|
|
@ -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.
|
||||
|
|
|
|||
43
Makefile
43
Makefile
|
|
@ -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
304
README.md
|
|
@ -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
72
cmd/runtimebundle/main.go
Normal 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]")
|
||||
}
|
||||
12
customize.sh
12
customize.sh
|
|
@ -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}"
|
||||
|
|
|
|||
BIN
firecracker
BIN
firecracker
Binary file not shown.
|
|
@ -1,7 +0,0 @@
|
|||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACBCcKLE0FGsW007R6s2MvgYaA/E2KhBwEVy3jZ5l60OMQAAAJDph86L6YfO
|
||||
iwAAAAtzc2gtZWQyNTUxOQAAACBCcKLE0FGsW007R6s2MvgYaA/E2KhBwEVy3jZ5l60OMQ
|
||||
AAAECwXc/eHLI6iZ4vF0TF4gHU7/FigvePPD4xcsnzSNbO/kJwosTQUaxbTTtHqzYy+Bho
|
||||
D8TYqEHARXLeNnmXrQ4xAAAAC3RoYWxlc0BtYWluAQI=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 != "" {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
326
internal/runtimebundle/bundle.go
Normal file
326
internal/runtimebundle/bundle.go
Normal 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
|
||||
}
|
||||
150
internal/runtimebundle/bundle_test.go
Normal file
150
internal/runtimebundle/bundle_test.go
Normal 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[:])
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
log "building $OUT_ROOTFS from $BASE_ROOTFS"
|
||||
exec "$DIR/customize.sh" "$BASE_ROOTFS" \
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
|
||||
log "building $OUT_ROOTFS from $BASE_ROOTFS"
|
||||
exec env BANGER_RUNTIME_DIR="$RUNTIME_DIR" "$DIR/customize.sh" "$BASE_ROOTFS" \
|
||||
--out "$OUT_ROOTFS" \
|
||||
--size "$SIZE_SPEC" \
|
||||
--docker
|
||||
|
|
|
|||
415
namegen
415
namegen
|
|
@ -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
20
runtime-bundle.toml
Normal 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
159
verify.sh
|
|
@ -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"
|
||||
exit 1
|
||||
fi
|
||||
VM_JSON="$VM_DIR/vm.json"
|
||||
if [[ ! -f "$VM_JSON" ]]; then
|
||||
log "vm.json not found: $VM_JSON"
|
||||
exit 1
|
||||
fi
|
||||
TAP="$tap"
|
||||
VM_DIR="$vm_dir"
|
||||
GUEST_IP="$guest_ip"
|
||||
|
||||
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"
|
||||
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
|
||||
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"
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue