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/
|
state/
|
||||||
|
/runtime/
|
||||||
|
/dist/
|
||||||
|
/banger
|
||||||
|
/bangerd
|
||||||
*.log
|
*.log
|
||||||
*.sock
|
*.sock
|
||||||
*.pid
|
*.pid
|
||||||
|
|
|
||||||
46
AGENTS.md
46
AGENTS.md
|
|
@ -1,33 +1,39 @@
|
||||||
# Repository Guidelines
|
# Repository Guidelines
|
||||||
|
|
||||||
## Project Structure & Module Organization
|
## 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.
|
- `cmd/banger` and `cmd/bangerd` are the primary user-facing entrypoints.
|
||||||
- `firecracker`, `vmlinux`, and `rootfs.ext4` are runtime artifacts required to boot the guest.
|
- `internal/` contains the daemon, CLI, RPC, storage, Firecracker, and system integration code.
|
||||||
- `state/` is created at runtime to store per-VM sockets, logs, and metadata (safe to delete when no VMs are running).
|
- `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.
|
||||||
- `firecracker.log` is produced by Firecracker; additional per-VM logs live under `state/vm-*/`.
|
- 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
|
## 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.
|
- `make build` builds `./banger` and `./bangerd`.
|
||||||
- `ssh -i "./id_ed25519" root@<guest_ip>` connects to the guest once it boots.
|
- `make runtime-bundle` bootstraps `./runtime/` from `runtime-bundle.toml`.
|
||||||
- `reboot` (inside the guest) shuts down the VM.
|
- `./banger vm create --name testbox` creates and starts a VM.
|
||||||
- There is no build step in this repo; binaries and images are checked in.
|
- `./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
|
## Coding Style & Naming Conventions
|
||||||
- Shell scripts use Bash with `set -euo pipefail`; keep new scripts strict and explicit.
|
- Go code should stay small, direct, and standard-library-first unless there is a clear reason otherwise.
|
||||||
- Indentation is two spaces in `run.sh`; match existing formatting and quoting style.
|
- Shell helpers use Bash with `set -euo pipefail`; keep remaining shell scripts strict and explicit.
|
||||||
- Filenames are short and descriptive (e.g., `run.sh`, `rootfs.ext4`). Prefer lowercase with dashes or dots.
|
- Prefer lowercase filenames with short descriptive names.
|
||||||
- No formatter or linter is configured; keep changes small and readable.
|
- Use `gofmt` for Go formatting; no extra formatter is configured for shell files.
|
||||||
|
|
||||||
## Testing Guidelines
|
## Testing Guidelines
|
||||||
- No automated test framework is present.
|
- Primary automated coverage is `go test ./...`.
|
||||||
- Manual verification: run `./run.sh`, confirm the guest boots, and verify SSH access.
|
- Manual verification for VM lifecycle changes: `./banger vm create`, confirm SSH access, then stop/delete the VM.
|
||||||
- If adding tests, document how to run them in this file and keep them self-contained.
|
- 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
|
## Commit & Pull Request Guidelines
|
||||||
- Git history uses short, informal commit summaries (e.g., "ignore", "Document expected log noise").
|
- Git history uses short, imperative subjects.
|
||||||
- Prefer imperative, single-line subjects; keep them under ~50 characters when possible.
|
- Prefer a real commit body when the change affects lifecycle behavior, storage semantics, or host integration.
|
||||||
- PRs should describe the change, list any new runtime requirements, and include logs or screenshots if behavior changes.
|
- PRs should call out runtime requirements, migration impact, and any host-side verification performed.
|
||||||
|
|
||||||
## Security & Configuration Tips
|
## Security & Configuration Tips
|
||||||
- The script requires `sudo` and `/dev/kvm` access; avoid committing secrets or private keys.
|
- The VM workflow requires `sudo` and `/dev/kvm` access; do not commit secrets.
|
||||||
- `id_ed25519` is a local SSH key for the guest; rotate or replace it if sharing the repository.
|
- `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
|
LIBDIR ?= $(PREFIX)/lib
|
||||||
RUNTIMEDIR ?= $(LIBDIR)/banger
|
RUNTIMEDIR ?= $(LIBDIR)/banger
|
||||||
DESTDIR ?=
|
DESTDIR ?=
|
||||||
|
RUNTIME_MANIFEST ?= runtime-bundle.toml
|
||||||
|
RUNTIME_SOURCE_DIR ?= runtime
|
||||||
|
RUNTIME_ARCHIVE ?= dist/banger-runtime.tar.gz
|
||||||
BINARIES := banger bangerd
|
BINARIES := banger bangerd
|
||||||
GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort)
|
GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort)
|
||||||
RUNTIME_EXECUTABLES := firecracker customize.sh dns.sh packages.sh nat.sh namegen
|
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_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
|
RUNTIME_MODULES_DIR := wtf/root/lib/modules/6.8.0-94-generic
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.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:
|
help:
|
||||||
@printf '%s\n' \
|
@printf '%s\n' \
|
||||||
'Targets:' \
|
'Targets:' \
|
||||||
' make build Build ./banger and ./bangerd' \
|
' 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 install Build and install binaries plus the runtime bundle into $(DESTDIR)$(BINDIR) and $(DESTDIR)$(RUNTIMEDIR)' \
|
||||||
' make test Run go test ./...' \
|
' make test Run go test ./...' \
|
||||||
' make fmt Format Go sources under cmd/ and internal/' \
|
' make fmt Format Go sources under cmd/ and internal/' \
|
||||||
' make tidy Run go mod tidy' \
|
' make tidy Run go mod tidy' \
|
||||||
' make clean Remove built Go binaries' \
|
' 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)
|
build: $(BINARIES)
|
||||||
|
|
||||||
|
|
@ -50,11 +56,19 @@ tidy:
|
||||||
clean:
|
clean:
|
||||||
rm -f ./banger ./bangerd
|
rm -f ./banger ./bangerd
|
||||||
|
|
||||||
install: build
|
runtime-bundle:
|
||||||
@for path in $(RUNTIME_EXECUTABLES) $(RUNTIME_BOOT_FILES) $(RUNTIME_MODULES_DIR) packages.apt id_ed25519; do \
|
$(GO) run ./cmd/runtimebundle fetch --manifest "$(RUNTIME_MANIFEST)" --out "$(RUNTIME_SOURCE_DIR)"
|
||||||
test -e "$$path" || { echo "missing runtime artifact: $$path" >&2; exit 1; }; \
|
|
||||||
|
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
|
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)$(BINDIR)"
|
||||||
mkdir -p "$(DESTDIR)$(RUNTIMEDIR)"
|
mkdir -p "$(DESTDIR)$(RUNTIMEDIR)"
|
||||||
mkdir -p "$(DESTDIR)$(RUNTIMEDIR)/wtf/root/boot"
|
mkdir -p "$(DESTDIR)$(RUNTIMEDIR)/wtf/root/boot"
|
||||||
|
|
@ -62,13 +76,18 @@ install: build
|
||||||
$(INSTALL) -m 0755 ./banger "$(DESTDIR)$(BINDIR)/banger"
|
$(INSTALL) -m 0755 ./banger "$(DESTDIR)$(BINDIR)/banger"
|
||||||
$(INSTALL) -m 0755 ./bangerd "$(DESTDIR)$(BINDIR)/bangerd"
|
$(INSTALL) -m 0755 ./bangerd "$(DESTDIR)$(BINDIR)/bangerd"
|
||||||
@for path in $(RUNTIME_EXECUTABLES); do \
|
@for path in $(RUNTIME_EXECUTABLES); do \
|
||||||
$(INSTALL) -m 0755 "$$path" "$(DESTDIR)$(RUNTIMEDIR)/$$path"; \
|
$(INSTALL) -m 0755 "$(RUNTIME_SOURCE_DIR)/$$path" "$(DESTDIR)$(RUNTIMEDIR)/$$path"; \
|
||||||
done
|
done
|
||||||
@for path in $(RUNTIME_DATA_FILES) $(RUNTIME_BOOT_FILES); do \
|
@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
|
done
|
||||||
$(INSTALL) -m 0600 id_ed25519 "$(DESTDIR)$(RUNTIMEDIR)/id_ed25519"
|
@for path in $(RUNTIME_OPTIONAL_DATA_FILES); do \
|
||||||
cp -a "$(RUNTIME_MODULES_DIR)" "$(DESTDIR)$(RUNTIMEDIR)/wtf/root/lib/modules/"
|
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:
|
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
|
# banger
|
||||||
|
|
||||||
Minimal Firecracker launcher.
|
Persistent Firecracker development VMs managed through a Go daemon, CLI, and TUI.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
- Linux host with KVM (`/dev/kvm` access)
|
- Linux host with KVM (`/dev/kvm` access)
|
||||||
- `sudo`, `ip`, `curl`, `ssh`, `jq`
|
- `sudo`, `ip`, `curl`, `ssh`, `jq`
|
||||||
- `dmsetup`, `losetup`, `blockdev` (device-mapper snapshot for rootfs)
|
- `dmsetup`, `losetup`, `blockdev`
|
||||||
- `e2cp`, `e2rm` (writes hostname and resolv.conf into rootfs snapshot)
|
- `e2cp`, `e2rm`, `debugfs`
|
||||||
|
- `mapdns`
|
||||||
|
|
||||||
## Files
|
## Runtime Bundle
|
||||||
- `firecracker`: Firecracker binary
|
Runtime artifacts are no longer tracked directly in Git. Source checkouts use a
|
||||||
- `wtf/root/boot/vmlinux-6.8.0-94-generic`: guest kernel
|
generated `./runtime/` bundle, while installed binaries use
|
||||||
- `wtf/root/boot/initrd.img-6.8.0-94-generic`: guest initrd
|
`$(prefix)/lib/banger`.
|
||||||
- `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
|
|
||||||
|
|
||||||
## Run
|
The bundle contains:
|
||||||
```
|
- `firecracker`
|
||||||
./run.sh
|
- `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
|
`make runtime-bundle` reads [`runtime-bundle.toml`](/home/thales/projects/personal/banger/runtime-bundle.toml),
|
||||||
There is now an XDG-based Go daemon + CLI prototype alongside the shell scripts.
|
downloads the published bundle, verifies its SHA256, and unpacks it into
|
||||||
It keeps persistent VM/image state in SQLite under your XDG state directory and
|
`./runtime/`. `make install` will not fetch artifacts for you. The manifest
|
||||||
talks over a Unix socket under your XDG runtime directory.
|
must point at a published or locally staged bundle before bootstrap can work.
|
||||||
|
|
||||||
Build it with:
|
## Build
|
||||||
```
|
```bash
|
||||||
|
make runtime-bundle
|
||||||
make build
|
make build
|
||||||
```
|
```
|
||||||
|
|
||||||
Or directly with Go:
|
Install into `~/.local/bin` by default, with the runtime bundle under
|
||||||
```
|
`~/.local/lib/banger`:
|
||||||
go build -o ./banger ./cmd/banger
|
```bash
|
||||||
go build -o ./bangerd ./cmd/bangerd
|
make install
|
||||||
```
|
```
|
||||||
|
|
||||||
Basic usage:
|
After `make install`, the installed `banger` and `bangerd` do not need the repo
|
||||||
```
|
checkout to keep working.
|
||||||
./banger daemon status
|
|
||||||
./banger tui
|
## Basic VM Workflow
|
||||||
./banger vm list
|
Create and boot a VM:
|
||||||
./banger vm create --name calm-otter --disk-size 16G
|
```bash
|
||||||
./banger vm set calm-otter --memory 2048 --vcpu 4
|
banger vm create --name calm-otter --disk-size 16G
|
||||||
./banger image list
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
List VMs:
|
||||||
- `banger` auto-starts the per-user daemon when needed.
|
```bash
|
||||||
- `banger tui` launches a terminal UI for browsing, creating, editing, and operating VMs.
|
banger vm list
|
||||||
- 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>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Shortcut:
|
Inspect a VM:
|
||||||
```
|
```bash
|
||||||
./ssh.sh <vm-name-or-ip>
|
banger vm show calm-otter
|
||||||
|
banger vm stats calm-otter
|
||||||
```
|
```
|
||||||
|
|
||||||
## VM DNS
|
SSH into a running VM:
|
||||||
- Spawned VMs register `<vm-name>.vm` → guest IP through `mapdns set`.
|
```bash
|
||||||
- VM teardown removes the mapping through `mapdns rm`.
|
banger vm ssh calm-otter
|
||||||
- `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>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Shutdown
|
Stop, restart, kill, or delete it:
|
||||||
```
|
```bash
|
||||||
reboot
|
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)
|
Update stopped VM settings:
|
||||||
Use `customize.sh` to build a writable rootfs with Docker and kernel modules
|
```bash
|
||||||
preloaded so Docker works out of the box. Pass the base rootfs as a positional
|
banger vm set calm-otter --memory 2048 --vcpu 4 --disk-size 32G
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Options:
|
Launch the TUI:
|
||||||
- `--size`: optional size for the output image.
|
```bash
|
||||||
- `--kernel`: kernel path (default: `./wtf/root/boot/vmlinux-6.8.0-94-generic`).
|
banger tui
|
||||||
- `--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"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build Rootfs On Demand
|
## Daemon
|
||||||
`run.sh` defaults to `./rootfs-docker.ext4`. If it is missing, `run.sh` will
|
The CLI auto-starts `bangerd` when needed.
|
||||||
invoke `make-rootfs.sh` to build it.
|
|
||||||
|
|
||||||
```
|
Useful daemon commands:
|
||||||
./make-rootfs.sh
|
```bash
|
||||||
|
banger daemon status
|
||||||
|
banger daemon socket
|
||||||
|
banger daemon stop
|
||||||
```
|
```
|
||||||
|
|
||||||
`make-rootfs.sh` chooses the first available base image:
|
State lives under XDG directories:
|
||||||
- `./rootfs.ext4`
|
- 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
|
Installed binaries resolve their runtime bundle from `../lib/banger` relative to
|
||||||
warn and keep using the existing image. `make-rootfs.sh` will also warn and
|
the executable. Source-checkout binaries resolve it from `./runtime` next to the
|
||||||
exit without rebuilding while the image already exists.
|
repo-built `./banger`. You can override either with `runtime_dir` in
|
||||||
|
`~/.config/banger/config.toml` or `BANGER_RUNTIME_DIR`.
|
||||||
|
|
||||||
To rebuild after package changes:
|
Useful config keys:
|
||||||
```
|
- `runtime_dir`
|
||||||
rm -f ./rootfs-docker.ext4 ./rootfs-docker.ext4.packages.sha256
|
- `firecracker_bin`
|
||||||
./make-rootfs.sh
|
- `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
|
Build a managed image:
|
||||||
To create a writable copy and customize it manually over SSH (no automatic
|
```bash
|
||||||
package/config changes), use:
|
banger image build --name docker-dev --docker
|
||||||
|
|
||||||
```
|
|
||||||
./interactive.sh ./rootfs-docker.ext4
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can override the output path:
|
Show or delete images:
|
||||||
```
|
```bash
|
||||||
./interactive.sh ./rootfs-docker.ext4 --out ./my-rootfs.ext4
|
banger image show docker-dev
|
||||||
|
banger image delete docker-dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## VM Info File
|
`banger` auto-registers the bundled `default_rootfs` image when it exists. If
|
||||||
Each VM writes:
|
`rootfs.ext4` is not present in the bundle, `image build` falls back to using
|
||||||
- `state/vms/<id>/vm.json`: local metadata under `.meta` plus the raw Firecracker config under `.config`.
|
`rootfs-docker.ext4` as its default base image.
|
||||||
|
|
||||||
## Log Notes
|
## Networking And DNS
|
||||||
- `PCI: Fatal: No config space access function found` and `MissingAddressRange` lines are expected with `pci=off` in `run.sh`.
|
Enable NAT when creating or updating a VM:
|
||||||
- `SELinux: Could not open policy file ...` is expected in the minimal rootfs.
|
```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
|
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/dns.sh"
|
||||||
source "$RUNTIME_DIR/packages.sh"
|
source "$RUNTIME_DIR/packages.sh"
|
||||||
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/image-build}"
|
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)"
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
source "$DIR/dns.sh"
|
DEFAULT_RUNTIME_DIR="$DIR"
|
||||||
STATE="$DIR/state"
|
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"
|
VM_ROOT="$STATE/vms"
|
||||||
mkdir -p "$VM_ROOT"
|
mkdir -p "$VM_ROOT"
|
||||||
|
|
||||||
FC_BIN="$DIR/firecracker"
|
FC_BIN="$RUNTIME_DIR/firecracker"
|
||||||
KERNEL="$DIR/wtf/root/boot/vmlinux-6.8.0-94-generic"
|
KERNEL="$RUNTIME_DIR/wtf/root/boot/vmlinux-6.8.0-94-generic"
|
||||||
INITRD="$DIR/wtf/root/boot/initrd.img-6.8.0-94-generic"
|
INITRD="$RUNTIME_DIR/wtf/root/boot/initrd.img-6.8.0-94-generic"
|
||||||
SSH_KEY="$DIR/id_ed25519"
|
SSH_KEY="$RUNTIME_DIR/id_ed25519"
|
||||||
|
|
||||||
BR_DEV="br-fc"
|
BR_DEV="br-fc"
|
||||||
BR_IP="172.16.0.1"
|
BR_IP="172.16.0.1"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"banger/internal/api"
|
"banger/internal/api"
|
||||||
"banger/internal/model"
|
"banger/internal/model"
|
||||||
|
"banger/internal/paths"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (model.Image, error) {
|
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
|
baseRootfs = d.config.DefaultBaseRootfs
|
||||||
}
|
}
|
||||||
if baseRootfs == "" {
|
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()
|
id, err := model.NewID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -41,10 +42,10 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (m
|
||||||
rootfsPath := filepath.Join(artifactDir, "rootfs.ext4")
|
rootfsPath := filepath.Join(artifactDir, "rootfs.ext4")
|
||||||
script := d.config.CustomizeScript
|
script := d.config.CustomizeScript
|
||||||
if script == "" {
|
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 {
|
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}
|
args := []string{script, baseRootfs, "--out", rootfsPath}
|
||||||
if params.Size != "" {
|
if params.Size != "" {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"banger/internal/api"
|
"banger/internal/api"
|
||||||
"banger/internal/firecracker"
|
"banger/internal/firecracker"
|
||||||
"banger/internal/model"
|
"banger/internal/model"
|
||||||
|
"banger/internal/paths"
|
||||||
"banger/internal/system"
|
"banger/internal/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -275,6 +276,49 @@ func (d *Daemon) StopVM(ctx context.Context, idOrName string) (model.VMRecord, e
|
||||||
return vm, nil
|
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) {
|
func (d *Daemon) RestartVM(ctx context.Context, idOrName string) (model.VMRecord, error) {
|
||||||
vm, err := d.StopVM(ctx, idOrName)
|
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) {
|
func (d *Daemon) firecrackerBinary() (string, error) {
|
||||||
if d.config.FirecrackerBin == "" {
|
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
|
path := d.config.FirecrackerBin
|
||||||
if !exists(path) {
|
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
|
return path, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,9 @@ func ResolveRuntimeDir(configuredRuntimeDir, deprecatedRepoRoot string) string {
|
||||||
return installRuntimeDir
|
return installRuntimeDir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if HasRuntimeBundle(exeDir) {
|
sourceRuntimeDir := filepath.Join(exeDir, "runtime")
|
||||||
return exeDir
|
if HasRuntimeBundle(sourceRuntimeDir) {
|
||||||
|
return sourceRuntimeDir
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -138,6 +139,10 @@ func BangerdPath() (string, error) {
|
||||||
return "", errors.New("bangerd binary not found next to banger; build ./cmd/bangerd")
|
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 {
|
func getenvDefault(key, fallback string) string {
|
||||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||||
return 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()
|
root := t.TempDir()
|
||||||
createRuntimeBundle(t, root)
|
runtimeDir := filepath.Join(root, "runtime")
|
||||||
|
createRuntimeBundle(t, runtimeDir)
|
||||||
|
|
||||||
origExecutablePath := executablePath
|
origExecutablePath := executablePath
|
||||||
executablePath = func() (string, error) {
|
executablePath = func() (string, error) {
|
||||||
|
|
@ -44,8 +45,8 @@ func TestResolveRuntimeDirUsesExecutableDirectoryBundle(t *testing.T) {
|
||||||
executablePath = origExecutablePath
|
executablePath = origExecutablePath
|
||||||
})
|
})
|
||||||
|
|
||||||
if got := ResolveRuntimeDir("", ""); got != root {
|
if got := ResolveRuntimeDir("", ""); got != runtimeDir {
|
||||||
t.Fatalf("ResolveRuntimeDir() = %q, want %q", got, root)
|
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)"
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
source "$DIR/packages.sh"
|
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"
|
SIZE_SPEC="6G"
|
||||||
BASE_ROOTFS=""
|
BASE_ROOTFS=""
|
||||||
|
|
||||||
|
|
@ -55,20 +61,22 @@ if [[ -f "$OUT_ROOTFS" ]]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$BASE_ROOTFS" ]]; then
|
if [[ -z "$BASE_ROOTFS" ]]; then
|
||||||
if [[ -f "$DIR/rootfs.ext4" ]]; then
|
if [[ -f "$RUNTIME_DIR/rootfs.ext4" ]]; then
|
||||||
BASE_ROOTFS="$DIR/rootfs.ext4"
|
BASE_ROOTFS="$RUNTIME_DIR/rootfs.ext4"
|
||||||
elif [[ -f "$DIR/ubuntu-noble-rootfs/rootfs.ext4" ]]; then
|
elif [[ -f "$DIR/ubuntu-noble-rootfs/rootfs.ext4" ]]; then
|
||||||
BASE_ROOTFS="$DIR/ubuntu-noble-rootfs/rootfs.ext4"
|
BASE_ROOTFS="$DIR/ubuntu-noble-rootfs/rootfs.ext4"
|
||||||
elif [[ -f "$DIR/ubuntu-lts/rootfs.ext4" ]]; then
|
elif [[ -f "$DIR/ubuntu-lts/rootfs.ext4" ]]; then
|
||||||
BASE_ROOTFS="$DIR/ubuntu-lts/rootfs.ext4"
|
BASE_ROOTFS="$DIR/ubuntu-lts/rootfs.ext4"
|
||||||
else
|
else
|
||||||
log "no base rootfs found"
|
log "no base rootfs found; run 'make runtime-bundle' or pass --base-rootfs"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "building $OUT_ROOTFS from $BASE_ROOTFS"
|
mkdir -p "$RUNTIME_DIR"
|
||||||
exec "$DIR/customize.sh" "$BASE_ROOTFS" \
|
|
||||||
--out "$OUT_ROOTFS" \
|
log "building $OUT_ROOTFS from $BASE_ROOTFS"
|
||||||
--size "$SIZE_SPEC" \
|
exec env BANGER_RUNTIME_DIR="$RUNTIME_DIR" "$DIR/customize.sh" "$BASE_ROOTFS" \
|
||||||
--docker
|
--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",
|
||||||
|
]
|
||||||
161
verify.sh
161
verify.sh
|
|
@ -5,70 +5,149 @@ log() {
|
||||||
printf '[verify] %s\n' "$*"
|
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() {
|
cleanup() {
|
||||||
if [[ -z "${VM_JSON:-}" || ! -f "$VM_JSON" ]]; then
|
if [[ -n "${VM_NAME:-}" ]]; then
|
||||||
return
|
./banger vm delete "$VM_NAME" >/dev/null 2>&1 || true
|
||||||
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"
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
log "starting VM"
|
log "starting VM"
|
||||||
if ! ./run.sh; then
|
CREATE_ARGS=(./banger vm create --name "$VM_NAME")
|
||||||
log "run.sh failed"
|
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
|
exit 1
|
||||||
fi
|
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}')"
|
TAP="$tap"
|
||||||
if [[ -z "$VM_DIR" ]]; then
|
VM_DIR="$vm_dir"
|
||||||
log "no VM state directory found"
|
GUEST_IP="$guest_ip"
|
||||||
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")"
|
if (( NAT_ENABLED )); then
|
||||||
created_at="$(jq -r '.meta.created_at // empty' "$VM_JSON")"
|
UPLINK="$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')"
|
||||||
guest_ip="$(jq -r '.meta.guest_ip // empty' "$VM_JSON")"
|
if [[ -z "$UPLINK" ]]; then
|
||||||
tap="$(jq -r '.meta.tap // empty' "$VM_JSON")"
|
log "failed to detect uplink interface"
|
||||||
pid="$(jq -r '.meta.pid // empty' "$VM_JSON")"
|
exit 1
|
||||||
vm_dir="$VM_DIR"
|
fi
|
||||||
|
log "asserting NAT rules are installed"
|
||||||
if [[ -z "$name" || -z "$created_at" || -z "$guest_ip" ]]; then
|
sudo iptables -t nat -C POSTROUTING -s "${GUEST_IP}/32" -o "$UPLINK" -j MASQUERADE
|
||||||
log "missing name or created_at in vm.json"
|
sudo iptables -C FORWARD -i "$TAP" -o "$UPLINK" -j ACCEPT
|
||||||
exit 1
|
sudo iptables -C FORWARD -i "$UPLINK" -o "$TAP" -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "asserting VM is reachable via SSH"
|
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
|
"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"
|
log "cleaning up VM"
|
||||||
cleanup
|
cleanup
|
||||||
|
|
||||||
log "asserting cleanup success"
|
log "asserting cleanup success"
|
||||||
if ip link show "$tap" >/dev/null 2>&1; then
|
if ./banger vm show "$VM_NAME" >/dev/null 2>&1; then
|
||||||
log "tap still exists: $tap"
|
log "vm still exists after delete: $VM_NAME"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [[ -d "$vm_dir" ]]; then
|
if ip link show "$TAP" >/dev/null 2>&1; then
|
||||||
log "vm dir still exists: $vm_dir"
|
log "tap still exists: $TAP"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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"
|
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