Reorganize the source checkout layout
Separate tracked source from generated artifacts so the repo root stops accumulating helper scripts, manifests, and local runtime outputs. Move manual shell entrypoints under scripts/, manifests under config/, and the Firecracker API reference under docs/reference/. Make build and runtimebundle now target build/bin, build/runtime, and build/dist as the canonical source-checkout paths. Update runtime discovery, helper scripts, tests, and docs to follow the new layout while keeping legacy source-checkout runtime fallbacks for existing local bundles during migration. Validated with bash -n on the moved scripts, make build, and GOCACHE=/tmp/banger-gocache go test ./....
This commit is contained in:
parent
2362d0ae39
commit
01c7cb5e65
23 changed files with 296 additions and 186 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
|||
state/
|
||||
/build/
|
||||
/runtime/
|
||||
/dist/
|
||||
/banger
|
||||
|
|
|
|||
38
AGENTS.md
38
AGENTS.md
|
|
@ -4,28 +4,28 @@
|
|||
- `cmd/banger` and `cmd/bangerd` are the primary user-facing entrypoints.
|
||||
- `internal/` contains the daemon, CLI, RPC, storage, Firecracker, and system integration code.
|
||||
- The VM lifecycle is now organized around daemon capabilities plus a structured guest-config builder. New host-integrated VM features should plug into that Go path instead of adding more one-off branches through `internal/daemon/vm.go`.
|
||||
- `customize.sh`, `make-rootfs.sh`, and `interactive.sh` remain as manual rootfs/customization helpers; normal VM lifecycle, NAT, `.vm` DNS, and daemon-driven image builds are handled by the Go control plane.
|
||||
- Source checkouts use a generated `./runtime/` bundle for Firecracker, kernels, modules, rootfs images, and helper copies. Bundle defaults come from `./runtime/bundle.json` when present. Those runtime artifacts are not meant to be tracked directly in Git.
|
||||
- `scripts/customize.sh`, `scripts/make-rootfs.sh`, and `scripts/interactive.sh` remain as manual rootfs/customization helpers; normal VM lifecycle, NAT, `.vm` DNS, and daemon-driven image builds are handled by the Go control plane.
|
||||
- Source checkouts use a generated `./build/runtime/` bundle for Firecracker, kernels, modules, rootfs images, and helper copies. Bundle defaults come from `./build/runtime/bundle.json` when present. 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
|
||||
- `make build` builds `./banger`, `./bangerd`, and the bundled `./runtime/banger-vsock-agent` guest helper.
|
||||
- `make build` builds `./build/bin/banger`, `./build/bin/bangerd`, and the bundled `./build/runtime/banger-vsock-agent` guest helper.
|
||||
- `make bench-create` benchmarks `vm create` and first-SSH readiness on the current host.
|
||||
- `make runtime-bundle` bootstraps `./runtime/` from the archive referenced by `RUNTIME_MANIFEST`; the checked-in `runtime-bundle.toml` is only a template.
|
||||
- `make void-kernel` downloads and stages a Void `linux6.12` kernel under `./runtime/void-kernel`, including extracted `vmlinux`, raw `vmlinuz`, a matching generated `initramfs`, config, and matching modules.
|
||||
- `make rootfs-void` builds an experimental local-only `x86_64-glibc` Void rootfs plus work-seed under `./runtime/`; it prefers staged `./runtime/void-kernel` modules when present, but does not replace the default Debian path or teach `banger image build` about Void.
|
||||
- `make runtime-bundle` bootstraps `./build/runtime/` from the archive referenced by `RUNTIME_MANIFEST`; the checked-in `config/runtime-bundle.toml` is only a template.
|
||||
- `make void-kernel` downloads and stages a Void `linux6.12` kernel under `./build/runtime/void-kernel`, including extracted `vmlinux`, raw `vmlinuz`, a matching generated `initramfs`, config, and matching modules.
|
||||
- `make rootfs-void` builds an experimental local-only `x86_64-glibc` Void rootfs plus work-seed under `./build/runtime/`; it prefers staged `./build/runtime/void-kernel` modules when present, but does not replace the default Debian path or teach `banger image build` about Void.
|
||||
- `make verify-void` registers `void-exp` and runs the normal smoke test against that image.
|
||||
- `banger` validates required host tools per command and reports actionable missing-tool errors; do not assume one workstation's package set.
|
||||
- `./banger vm create --name testbox` creates and starts a VM.
|
||||
- `./banger vm create` now blocks until the guest reaches the daemon's default readiness checks and shows live progress stages on TTY stderr while it waits.
|
||||
- `./banger vm ssh testbox` connects to a running guest using the runtime bundle SSH key and reminds the user if the VM is still running when the session exits.
|
||||
- `./banger vm stop testbox` stops a VM while preserving its disks.
|
||||
- `./banger vm stop vm-a vm-b vm-c` and `./banger vm set --nat web-1 web-2` are supported; multi-VM lifecycle and `set` actions fan out concurrently through the CLI.
|
||||
- `./banger doctor` reports runtime bundle, host tool, feature, and image-build readiness from the same Go checks used by the daemon.
|
||||
- `./banger image register --name local --rootfs /abs/path/rootfs.ext4` creates or updates an unmanaged image record without changing the default image config; use it for experimental guest iteration paths such as Void.
|
||||
- `./build/bin/banger vm create --name testbox` creates and starts a VM.
|
||||
- `./build/bin/banger vm create` now blocks until the guest reaches the daemon's default readiness checks and shows live progress stages on TTY stderr while it waits.
|
||||
- `./build/bin/banger vm ssh testbox` connects to a running guest using the runtime bundle SSH key and reminds the user if the VM is still running when the session exits.
|
||||
- `./build/bin/banger vm stop testbox` stops a VM while preserving its disks.
|
||||
- `./build/bin/banger vm stop vm-a vm-b vm-c` and `./build/bin/banger vm set --nat web-1 web-2` are supported; multi-VM lifecycle and `set` actions fan out concurrently through the CLI.
|
||||
- `./build/bin/banger doctor` reports runtime bundle, host tool, feature, and image-build readiness from the same Go checks used by the daemon.
|
||||
- `./build/bin/banger image register --name local --rootfs /abs/path/rootfs.ext4` creates or updates an unmanaged image record without changing the default image config; use it for experimental guest iteration paths such as Void.
|
||||
- `bangerd` now also serves a localhost web UI on `http://127.0.0.1:7777` by default unless `web_listen_addr = ""` disables it; the UI uses server-rendered templates, polls async VM/image operations, and keeps image path selection on the host via a server-side file picker.
|
||||
- `make test` runs `go test ./...`.
|
||||
- `./verify.sh` runs the smoke test for the Go VM workflow.
|
||||
- `./scripts/verify.sh` runs the smoke test for the Go VM workflow.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Go code should stay small, direct, and standard-library-first unless there is a clear reason otherwise.
|
||||
|
|
@ -35,15 +35,15 @@
|
|||
|
||||
## Testing Guidelines
|
||||
- Primary automated coverage is `go test ./...`.
|
||||
- Manual verification for VM lifecycle changes: `./banger vm create`, confirm SSH access, then stop/delete the VM.
|
||||
- For host-integration changes, run `./banger doctor` as a quick readiness check before the live VM smoke.
|
||||
- Manual verification for VM lifecycle changes: `./build/bin/banger vm create`, confirm SSH access, then stop/delete the VM.
|
||||
- For host-integration changes, run `./build/bin/banger doctor` as a quick readiness check before the live VM smoke.
|
||||
- The web UI follows the same sudo model as the CLI path: bangerd stays unprivileged and privileged writes only work when `sudo -v` is already warm or sudo is passwordless.
|
||||
- Rebuilt images now include `mise`, `opencode`, a host-reachable default `opencode` server service on guest TCP port `4096`, `tmux-resurrect`/`tmux-continuum` defaults for `root`, and the `banger-vsock-agent` service used by the SSH reminder and guest health-check path; if you change guest provisioning, document whether users need to rebuild `./runtime/rootfs-docker.ext4` or another base image to pick it up.
|
||||
- The experimental Void rootfs path now includes the repo's basic dev baseline plus Docker and Compose, alongside boot, SSH, a guest network bootstrap sourced from the kernel `ip=` cmdline, the vsock HTTP health agent, pinned `mise` plus `opencode` for `root`, the default host-reachable `opencode` server service on guest TCP port `4096`, a `bash` root shell while leaving `/bin/sh` alone, and the `/root` work-seed. When `./runtime/void-kernel/` exists, the Void image registration path expects a complete staged Void kernel, initramfs, and modules tree and points `void-exp` at it. Keep further baked-in tooling deliberate and user-driven.
|
||||
- Rebuilt images now include `mise`, `opencode`, a host-reachable default `opencode` server service on guest TCP port `4096`, `tmux-resurrect`/`tmux-continuum` defaults for `root`, and the `banger-vsock-agent` service used by the SSH reminder and guest health-check path; if you change guest provisioning, document whether users need to rebuild `./build/runtime/rootfs-docker.ext4` or another base image to pick it up.
|
||||
- The experimental Void rootfs path now includes the repo's basic dev baseline plus Docker and Compose, alongside boot, SSH, a guest network bootstrap sourced from the kernel `ip=` cmdline, the vsock HTTP health agent, pinned `mise` plus `opencode` for `root`, the default host-reachable `opencode` server service on guest TCP port `4096`, a `bash` root shell while leaving `/bin/sh` alone, and the `/root` work-seed. When `./build/runtime/void-kernel/` exists, the Void image registration path expects a complete staged Void kernel, initramfs, and modules tree and points `void-exp` at it. Keep further baked-in tooling deliberate and user-driven.
|
||||
- Rebuilt images also emit a `work-seed.ext4` sidecar used to speed up future VM creates. Older managed images may take one slower create to refresh seeded SSH access before they rejoin the fast path. If you touch `/root` provisioning, verify both the rootfs and the work-seed output.
|
||||
- The daemon may keep idle TAP devices in a pool for faster creates. Smoke tests should treat `tap-pool-*` devices as reusable capacity, not cleanup leaks.
|
||||
- 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`.
|
||||
- For NAT changes, verify both guest outbound access and host rule cleanup, for example with `./scripts/verify.sh --nat`.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Git history uses short, imperative subjects.
|
||||
|
|
|
|||
55
Makefile
55
Makefile
|
|
@ -8,10 +8,14 @@ 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
|
||||
BUILD_DIR ?= build
|
||||
BUILD_BIN_DIR ?= $(BUILD_DIR)/bin
|
||||
RUNTIME_MANIFEST ?= config/runtime-bundle.toml
|
||||
RUNTIME_SOURCE_DIR ?= $(BUILD_DIR)/runtime
|
||||
RUNTIME_ARCHIVE ?= $(BUILD_DIR)/dist/banger-runtime.tar.gz
|
||||
BANGER_BIN ?= $(BUILD_BIN_DIR)/banger
|
||||
BANGERD_BIN ?= $(BUILD_BIN_DIR)/bangerd
|
||||
BINARIES := $(BANGER_BIN) $(BANGERD_BIN)
|
||||
RUNTIME_HELPERS := $(RUNTIME_SOURCE_DIR)/banger-vsock-agent
|
||||
GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort)
|
||||
RUNTIME_EXECUTABLES := firecracker customize.sh packages.sh namegen banger-vsock-agent
|
||||
|
|
@ -29,8 +33,8 @@ VOID_VM_NAME ?= void-dev
|
|||
help:
|
||||
@printf '%s\n' \
|
||||
'Targets:' \
|
||||
' make build Build ./banger and ./bangerd' \
|
||||
' make runtime-bundle Fetch and unpack ./runtime from the archive referenced by $(RUNTIME_MANIFEST)' \
|
||||
' make build Build ./build/bin/banger and ./build/bin/bangerd' \
|
||||
' make runtime-bundle Fetch and unpack ./build/runtime from the archive referenced by $(RUNTIME_MANIFEST)' \
|
||||
' make runtime-package Package $(RUNTIME_SOURCE_DIR) into $(RUNTIME_ARCHIVE) and print its SHA256' \
|
||||
' make bench-create Benchmark vm create and SSH readiness with scripts/bench-create.sh' \
|
||||
' make install Build and install binaries plus the runtime bundle into $(DESTDIR)$(BINDIR) and $(DESTDIR)$(RUNTIMEDIR)' \
|
||||
|
|
@ -38,20 +42,22 @@ help:
|
|||
' 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 source-checkout default Debian rootfs image in ./runtime' \
|
||||
' make void-kernel Download and stage a Void kernel, initramfs, and modules under ./runtime/void-kernel' \
|
||||
' make rootfs-void Build an experimental Void Linux rootfs and work-seed in ./runtime' \
|
||||
' make rootfs Rebuild the source-checkout default Debian rootfs image in ./build/runtime' \
|
||||
' make void-kernel Download and stage a Void kernel, initramfs, and modules under ./build/runtime/void-kernel' \
|
||||
' make rootfs-void Build an experimental Void Linux rootfs and work-seed in ./build/runtime' \
|
||||
' make void-register Register or update the experimental Void image as $(VOID_IMAGE_NAME)' \
|
||||
' make void-vm Register the experimental Void image and create a VM named $(VOID_VM_NAME)' \
|
||||
' make verify-void Register the experimental Void image and run verify.sh against it'
|
||||
' make verify-void Register the experimental Void image and run scripts/verify.sh against it'
|
||||
|
||||
build: $(BINARIES) $(RUNTIME_HELPERS)
|
||||
|
||||
banger: $(GO_SOURCES) go.mod go.sum
|
||||
$(GO) build -o ./banger ./cmd/banger
|
||||
$(BANGER_BIN): $(GO_SOURCES) go.mod go.sum
|
||||
mkdir -p "$(BUILD_BIN_DIR)"
|
||||
$(GO) build -o "$(BANGER_BIN)" ./cmd/banger
|
||||
|
||||
bangerd: $(GO_SOURCES) go.mod go.sum
|
||||
$(GO) build -o ./bangerd ./cmd/bangerd
|
||||
$(BANGERD_BIN): $(GO_SOURCES) go.mod go.sum
|
||||
mkdir -p "$(BUILD_BIN_DIR)"
|
||||
$(GO) build -o "$(BANGERD_BIN)" ./cmd/bangerd
|
||||
|
||||
$(RUNTIME_SOURCE_DIR)/banger-vsock-agent: $(GO_SOURCES) go.mod go.sum
|
||||
mkdir -p "$(RUNTIME_SOURCE_DIR)"
|
||||
|
|
@ -67,7 +73,8 @@ tidy:
|
|||
$(GO) mod tidy
|
||||
|
||||
clean:
|
||||
rm -f ./banger ./bangerd
|
||||
rm -rf "$(BUILD_BIN_DIR)"
|
||||
rm -f "$(RUNTIME_SOURCE_DIR)/banger-vsock-agent"
|
||||
|
||||
runtime-bundle:
|
||||
$(GO) run ./cmd/runtimebundle fetch --manifest "$(RUNTIME_MANIFEST)" --out "$(RUNTIME_SOURCE_DIR)"
|
||||
|
|
@ -76,7 +83,7 @@ runtime-package:
|
|||
$(GO) run ./cmd/runtimebundle package --manifest "$(RUNTIME_MANIFEST)" --runtime-dir "$(RUNTIME_SOURCE_DIR)" --out "$(RUNTIME_ARCHIVE)"
|
||||
|
||||
bench-create: build
|
||||
bash ./scripts/bench-create.sh $(ARGS)
|
||||
BANGER_BIN="$(abspath $(BANGER_BIN))" bash ./scripts/bench-create.sh $(ARGS)
|
||||
|
||||
check-runtime:
|
||||
@test -d "$(RUNTIME_SOURCE_DIR)" || { echo "missing runtime bundle directory: $(RUNTIME_SOURCE_DIR); run 'make runtime-bundle'" >&2; exit 1; }
|
||||
|
|
@ -89,8 +96,8 @@ install: build check-runtime
|
|||
mkdir -p "$(DESTDIR)$(RUNTIMEDIR)"
|
||||
mkdir -p "$(DESTDIR)$(RUNTIMEDIR)/wtf/root/boot"
|
||||
mkdir -p "$(DESTDIR)$(RUNTIMEDIR)/wtf/root/lib/modules"
|
||||
$(INSTALL) -m 0755 ./banger "$(DESTDIR)$(BINDIR)/banger"
|
||||
$(INSTALL) -m 0755 ./bangerd "$(DESTDIR)$(BINDIR)/bangerd"
|
||||
$(INSTALL) -m 0755 "$(BANGER_BIN)" "$(DESTDIR)$(BINDIR)/banger"
|
||||
$(INSTALL) -m 0755 "$(BANGERD_BIN)" "$(DESTDIR)$(BINDIR)/bangerd"
|
||||
@for path in $(RUNTIME_EXECUTABLES); do \
|
||||
$(INSTALL) -m 0755 "$(RUNTIME_SOURCE_DIR)/$$path" "$(DESTDIR)$(RUNTIMEDIR)/$$path"; \
|
||||
done
|
||||
|
|
@ -106,19 +113,19 @@ install: build check-runtime
|
|||
cp -a "$(RUNTIME_SOURCE_DIR)/$(RUNTIME_MODULES_DIR)" "$(DESTDIR)$(RUNTIMEDIR)/wtf/root/lib/modules/"
|
||||
|
||||
rootfs:
|
||||
BANGER_RUNTIME_DIR="$(abspath $(RUNTIME_SOURCE_DIR))" ./make-rootfs.sh
|
||||
BANGER_RUNTIME_DIR="$(abspath $(RUNTIME_SOURCE_DIR))" ./scripts/make-rootfs.sh
|
||||
|
||||
void-kernel:
|
||||
BANGER_RUNTIME_DIR="$(abspath $(RUNTIME_SOURCE_DIR))" ./make-void-kernel.sh
|
||||
BANGER_RUNTIME_DIR="$(abspath $(RUNTIME_SOURCE_DIR))" ./scripts/make-void-kernel.sh
|
||||
|
||||
rootfs-void:
|
||||
BANGER_RUNTIME_DIR="$(abspath $(RUNTIME_SOURCE_DIR))" ./make-rootfs-void.sh
|
||||
BANGER_RUNTIME_DIR="$(abspath $(RUNTIME_SOURCE_DIR))" ./scripts/make-rootfs-void.sh
|
||||
|
||||
void-register: build
|
||||
BANGER_RUNTIME_DIR="$(abspath $(RUNTIME_SOURCE_DIR))" VOID_IMAGE_NAME="$(VOID_IMAGE_NAME)" BANGER_BIN="$(abspath ./banger)" ./register-void-image.sh
|
||||
BANGER_RUNTIME_DIR="$(abspath $(RUNTIME_SOURCE_DIR))" VOID_IMAGE_NAME="$(VOID_IMAGE_NAME)" BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/register-void-image.sh
|
||||
|
||||
void-vm: void-register
|
||||
./banger vm create --image "$(VOID_IMAGE_NAME)" --name "$(VOID_VM_NAME)"
|
||||
"$(abspath $(BANGER_BIN))" vm create --image "$(VOID_IMAGE_NAME)" --name "$(VOID_VM_NAME)"
|
||||
|
||||
verify-void: void-register
|
||||
./verify.sh --image "$(VOID_IMAGE_NAME)"
|
||||
BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/verify.sh --image "$(VOID_IMAGE_NAME)"
|
||||
|
|
|
|||
102
README.md
102
README.md
|
|
@ -17,7 +17,7 @@ assuming one workstation layout.
|
|||
|
||||
## Runtime Bundle
|
||||
Runtime artifacts are no longer tracked directly in Git. Source checkouts use a
|
||||
generated `./runtime/` bundle, while installed binaries use
|
||||
generated `./build/runtime/` bundle, while installed binaries use
|
||||
`$(prefix)/lib/banger`.
|
||||
|
||||
The bundle contains:
|
||||
|
|
@ -34,30 +34,30 @@ The bundle contains:
|
|||
- the helper scripts used by manual customization and installs
|
||||
|
||||
Bootstrap a source checkout from a local or published runtime archive. The
|
||||
checked-in [`runtime-bundle.toml`](/home/thales/projects/personal/banger/runtime-bundle.toml)
|
||||
checked-in [`config/runtime-bundle.toml`](/home/thales/projects/personal/banger/config/runtime-bundle.toml)
|
||||
is a template and intentionally ships with empty `url` and `sha256`.
|
||||
|
||||
If you need to create a local archive first, do that from a checkout or machine
|
||||
that already has a populated `./runtime/` tree:
|
||||
that already has a populated `./build/runtime/` tree:
|
||||
```bash
|
||||
make runtime-package
|
||||
cp dist/banger-runtime.tar.gz /path/to/fresh-checkout/dist/
|
||||
cp build/dist/banger-runtime.tar.gz /path/to/fresh-checkout/build/dist/
|
||||
```
|
||||
|
||||
In the fresh checkout:
|
||||
```bash
|
||||
cp runtime-bundle.toml runtime-bundle.local.toml
|
||||
cp config/runtime-bundle.toml config/runtime-bundle.local.toml
|
||||
```
|
||||
|
||||
Edit `runtime-bundle.local.toml` to point at the staged archive and checksum:
|
||||
Edit `config/runtime-bundle.local.toml` to point at the staged archive and checksum:
|
||||
```toml
|
||||
url = "./dist/banger-runtime.tar.gz"
|
||||
url = "./build/dist/banger-runtime.tar.gz"
|
||||
sha256 = "<sha256 printed by make runtime-package>"
|
||||
```
|
||||
|
||||
Then bootstrap `./runtime/` with the local manifest copy:
|
||||
Then bootstrap `./build/runtime/` with the local manifest copy:
|
||||
```bash
|
||||
make runtime-bundle RUNTIME_MANIFEST=runtime-bundle.local.toml
|
||||
make runtime-bundle RUNTIME_MANIFEST=config/runtime-bundle.local.toml
|
||||
```
|
||||
|
||||
`url` may be a relative path, absolute path, `file:///...` URL, or HTTP(S)
|
||||
|
|
@ -68,8 +68,19 @@ URL. `make install` will not fetch artifacts for you.
|
|||
make build
|
||||
```
|
||||
|
||||
Run `make build` after `./runtime/` has been bootstrapped. It also rebuilds the
|
||||
bundled `banger-vsock-agent` guest helper in `./runtime/`.
|
||||
Run `make build` after `./build/runtime/` has been bootstrapped. It writes
|
||||
`./build/bin/banger`, `./build/bin/bangerd`, and refreshes the bundled
|
||||
`banger-vsock-agent` guest helper in `./build/runtime/`.
|
||||
|
||||
Older ignored root artifacts such as `./runtime/`, `./banger`, and `./bangerd`
|
||||
are no longer the canonical source-checkout layout. Leave them alone if you
|
||||
still need them, or remove them manually after migrating to `build/`.
|
||||
|
||||
If you have confirmed your current images and runtime settings no longer point
|
||||
at the old checkout-local paths, a one-time cleanup looks like:
|
||||
```bash
|
||||
rm -rf ./runtime ./banger ./bangerd
|
||||
```
|
||||
|
||||
Install into `~/.local/bin` by default, with the runtime bundle under
|
||||
`~/.local/lib/banger`:
|
||||
|
|
@ -178,8 +189,9 @@ State lives under XDG directories:
|
|||
- runtime socket: `$XDG_RUNTIME_DIR/banger/bangerd.sock`
|
||||
|
||||
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
|
||||
the executable. Source-checkout binaries resolve it from `./build/runtime` next
|
||||
to `./build/bin/banger`, and still fall back to a legacy `./runtime` checkout
|
||||
bundle if that exists. You can override either with `runtime_dir` in
|
||||
`~/.config/banger/config.toml` or `BANGER_RUNTIME_DIR`.
|
||||
|
||||
Useful config keys:
|
||||
|
|
@ -323,32 +335,32 @@ shell helpers treated as manual workflows rather than architecture drivers.
|
|||
- 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,
|
||||
`config/packages.apt` controls the base apt packages baked into rebuilt images,
|
||||
including guest tools such as `ss` used by `banger vm ports`.
|
||||
|
||||
To rebuild the source-checkout default image in `./runtime/rootfs-docker.ext4`:
|
||||
To rebuild the source-checkout default image in `./build/runtime/rootfs-docker.ext4`:
|
||||
```bash
|
||||
make rootfs
|
||||
```
|
||||
|
||||
That rebuild also regenerates `./runtime/rootfs-docker.work-seed.ext4`, which
|
||||
That rebuild also regenerates `./build/runtime/rootfs-docker.work-seed.ext4`, which
|
||||
the daemon uses to speed up future `vm create` calls, and bakes in the default
|
||||
host-reachable `opencode` server service.
|
||||
|
||||
If your runtime bundle does not include `./runtime/rootfs.ext4`, pass an
|
||||
If your runtime bundle does not include `./build/runtime/rootfs.ext4`, pass an
|
||||
explicit base image instead:
|
||||
```bash
|
||||
./make-rootfs.sh --base-rootfs /path/to/base-rootfs.ext4
|
||||
./scripts/make-rootfs.sh --base-rootfs /path/to/base-rootfs.ext4
|
||||
```
|
||||
|
||||
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
|
||||
rm -f ./build/runtime/rootfs-docker.ext4 ./build/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`.
|
||||
`make rootfs` expects a bootstrapped runtime bundle. If `./build/runtime/rootfs.ext4`
|
||||
is not available, pass an explicit `--base-rootfs` to `./scripts/make-rootfs.sh`.
|
||||
Existing VMs keep using their current image and disks; rebuilds only affect VMs
|
||||
created from the rebuilt image afterward. Restarting an existing VM is not
|
||||
enough to pick up guest provisioning changes such as the default `opencode`
|
||||
|
|
@ -363,13 +375,13 @@ make rootfs-void
|
|||
```
|
||||
|
||||
That writes:
|
||||
- `./runtime/void-kernel/` when `make void-kernel` is used
|
||||
- `./runtime/rootfs-void.ext4`
|
||||
- `./runtime/rootfs-void.work-seed.ext4`
|
||||
- `./build/runtime/void-kernel/` when `make void-kernel` is used
|
||||
- `./build/runtime/rootfs-void.ext4`
|
||||
- `./build/runtime/rootfs-void.work-seed.ext4`
|
||||
|
||||
This path is intentionally local-only and does not change the default Debian
|
||||
image flow. `make void-kernel` stages an actual Void `linux6.12` kernel package
|
||||
under `./runtime/void-kernel/`, including the raw `vmlinuz`, extracted
|
||||
under `./build/runtime/void-kernel/`, including the raw `vmlinuz`, extracted
|
||||
Firecracker `vmlinux`, a matching `initramfs`, the matching config, and the
|
||||
matching modules tree. The initramfs is generated locally with `dracut`
|
||||
against the downloaded Void sysroot so the kernel, initrd, and modules stay
|
||||
|
|
@ -395,11 +407,11 @@ The builder fetches official static XBPS tools and packages from the Void
|
|||
mirror during the build. The kernel fetcher and rootfs builder currently
|
||||
support only `x86_64`.
|
||||
|
||||
The package set comes from [`packages.void`](/home/thales/projects/personal/banger/packages.void).
|
||||
The package set comes from [`config/packages.void`](/home/thales/projects/personal/banger/config/packages.void).
|
||||
You can override the mirror, size, output path, or kernel package directly:
|
||||
```bash
|
||||
./make-void-kernel.sh --kernel-package linux6.12
|
||||
./make-rootfs-void.sh --mirror https://repo-default.voidlinux.org --size 2G
|
||||
./scripts/make-void-kernel.sh --kernel-package linux6.12
|
||||
./scripts/make-rootfs-void.sh --mirror https://repo-default.voidlinux.org --size 2G
|
||||
```
|
||||
|
||||
The fastest local iteration loop does not require changing your default image
|
||||
|
|
@ -408,8 +420,8 @@ config at all:
|
|||
make void-kernel
|
||||
make rootfs-void
|
||||
make void-register
|
||||
./banger vm create --image void-exp --name void-dev
|
||||
./banger vm ssh void-dev
|
||||
./build/bin/banger vm create --image void-exp --name void-dev
|
||||
./build/bin/banger vm ssh void-dev
|
||||
```
|
||||
|
||||
Rebuild the staged Void kernel or Void rootfs, then recreate existing
|
||||
|
|
@ -425,7 +437,7 @@ make verify-void
|
|||
`make void-register` uses the unmanaged image registration path to create or
|
||||
update a `void-exp` image record in place, so repeated rebuilds do not require
|
||||
editing `~/.config/banger/config.toml`. It expects a complete staged Void
|
||||
kernel set under `./runtime/void-kernel/` and points the experimental image at
|
||||
kernel set under `./build/runtime/void-kernel/` and points the experimental image at
|
||||
the staged Void `vmlinux`, `initramfs`, and matching modules tree.
|
||||
|
||||
There is also a one-step helper target:
|
||||
|
|
@ -453,12 +465,12 @@ and package manifest:
|
|||
```bash
|
||||
banger image register \
|
||||
--name void-exp \
|
||||
--rootfs ./runtime/rootfs-void.ext4 \
|
||||
--work-seed ./runtime/rootfs-void.work-seed.ext4 \
|
||||
--kernel ./runtime/void-kernel/boot/vmlinux-6.12.77_1 \
|
||||
--initrd ./runtime/void-kernel/boot/initramfs-6.12.77_1.img \
|
||||
--modules ./runtime/void-kernel/lib/modules/6.12.77_1 \
|
||||
--packages ./packages.void
|
||||
--rootfs ./build/runtime/rootfs-void.ext4 \
|
||||
--work-seed ./build/runtime/rootfs-void.work-seed.ext4 \
|
||||
--kernel ./build/runtime/void-kernel/boot/vmlinux-6.12.77_1 \
|
||||
--initrd ./build/runtime/void-kernel/boot/initramfs-6.12.77_1.img \
|
||||
--modules ./build/runtime/void-kernel/lib/modules/6.12.77_1 \
|
||||
--packages ./config/packages.void
|
||||
```
|
||||
|
||||
If an unmanaged image with the same name already exists, `image register`
|
||||
|
|
@ -466,17 +478,17 @@ updates it in place so future `vm create --image <name>` calls pick up the new
|
|||
artifacts immediately.
|
||||
|
||||
## Maintaining The Runtime Bundle
|
||||
The checked-in [`runtime-bundle.toml`](/home/thales/projects/personal/banger/runtime-bundle.toml)
|
||||
The checked-in [`config/runtime-bundle.toml`](/home/thales/projects/personal/banger/config/runtime-bundle.toml)
|
||||
is a template. Keep `bundle_metadata` accurate there, but use a separate local
|
||||
manifest copy when you need concrete `url` and `sha256` values for bootstrap
|
||||
testing or publication.
|
||||
|
||||
Package a local `./runtime/` tree into an archive:
|
||||
Package a local `./build/runtime/` tree into an archive:
|
||||
```bash
|
||||
make runtime-package
|
||||
```
|
||||
|
||||
That writes `dist/banger-runtime.tar.gz` and prints its SHA256 so you can update
|
||||
That writes `build/dist/banger-runtime.tar.gz` and prints its SHA256 so you can update
|
||||
a local manifest copy before testing bootstrap changes or publishing the
|
||||
archive elsewhere.
|
||||
|
||||
|
|
@ -499,10 +511,10 @@ The benchmark prints JSON with:
|
|||
|
||||
## Remaining Shell Helpers
|
||||
The runtime VM lifecycle is managed through `banger`. The remaining shell scripts are not the primary user interface:
|
||||
- `customize.sh`: manual reference flow for rootfs customization; `banger image build` is now Go-native, but the script still reads
|
||||
- `scripts/customize.sh`: manual reference flow for rootfs customization; `banger image build` is now Go-native, but the script still 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
|
||||
- `packages.sh`: shell helper library
|
||||
- `verify.sh`: smoke test for the Go workflow (`./verify.sh --nat` adds NAT coverage)
|
||||
- `scripts/make-rootfs.sh`: convenience wrapper for rebuilding `./build/runtime/rootfs-docker.ext4`
|
||||
- `scripts/interactive.sh`: manual one-off rootfs customization over SSH
|
||||
- `scripts/lib/packages.sh`: shell helper library
|
||||
- `scripts/verify.sh`: smoke test for the Go workflow (`./scripts/verify.sh --nat` adds NAT coverage)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ import (
|
|||
"banger/internal/runtimebundle"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultManifestPath = "config/runtime-bundle.toml"
|
||||
defaultRuntimeDir = "build/runtime"
|
||||
defaultArchivePath = "build/dist/banger-runtime.tar.gz"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
|
|
@ -34,8 +40,8 @@ func main() {
|
|||
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")
|
||||
manifestPath := fs.String("manifest", defaultManifestPath, "path to the runtime bundle manifest")
|
||||
outDir := fs.String("out", defaultRuntimeDir, "destination runtime directory")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -49,9 +55,9 @@ func fetch(args []string) error {
|
|||
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")
|
||||
manifestPath := fs.String("manifest", defaultManifestPath, "path to the runtime bundle manifest")
|
||||
runtimeDir := fs.String("runtime-dir", defaultRuntimeDir, "runtime directory to package")
|
||||
outArchive := fs.String("out", defaultArchivePath, "output archive path")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Template manifest for local or published runtime bundle archives.
|
||||
# Keep this checked-in file empty by default; use a local manifest copy with
|
||||
# concrete `url` and `sha256` values when bootstrapping `./runtime/`.
|
||||
# concrete `url` and `sha256` values when bootstrapping `./build/runtime/`.
|
||||
version = "v0"
|
||||
url = ""
|
||||
sha256 = ""
|
||||
|
|
@ -5,10 +5,10 @@
|
|||
# to the Void image yet; banger image build still assumes the Debian flow.
|
||||
# If you run `make void-kernel`, also merge the commented kernel/initrd/modules lines.
|
||||
|
||||
runtime_dir = "/abs/path/to/banger/runtime"
|
||||
runtime_dir = "/abs/path/to/banger/build/runtime"
|
||||
default_image_name = "void-exp"
|
||||
default_rootfs = "/abs/path/to/banger/runtime/rootfs-void.ext4"
|
||||
default_work_seed = "/abs/path/to/banger/runtime/rootfs-void.work-seed.ext4"
|
||||
# default_kernel = "/abs/path/to/banger/runtime/void-kernel/boot/vmlinux-6.12.77_1"
|
||||
# default_initrd = "/abs/path/to/banger/runtime/void-kernel/boot/initramfs-6.12.77_1.img"
|
||||
# default_modules_dir = "/abs/path/to/banger/runtime/void-kernel/lib/modules/6.12.77_1"
|
||||
default_rootfs = "/abs/path/to/banger/build/runtime/rootfs-void.ext4"
|
||||
default_work_seed = "/abs/path/to/banger/build/runtime/rootfs-void.work-seed.ext4"
|
||||
# default_kernel = "/abs/path/to/banger/build/runtime/void-kernel/boot/vmlinux-6.12.77_1"
|
||||
# default_initrd = "/abs/path/to/banger/build/runtime/void-kernel/boot/initramfs-6.12.77_1.img"
|
||||
# default_modules_dir = "/abs/path/to/banger/build/runtime/void-kernel/lib/modules/6.12.77_1"
|
||||
|
|
|
|||
|
|
@ -427,7 +427,7 @@ func TestAbsolutizeImageRegisterPaths(t *testing.T) {
|
|||
KernelPath: filepath.Join(".", "runtime", "vmlinux"),
|
||||
InitrdPath: filepath.Join(".", "runtime", "initrd.img"),
|
||||
ModulesDir: filepath.Join(".", "runtime", "modules"),
|
||||
PackagesPath: filepath.Join(".", "packages.void"),
|
||||
PackagesPath: filepath.Join(".", "config", "packages.void"),
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
|
|
|
|||
|
|
@ -111,11 +111,11 @@ func TestEnsureDefaultImageReconcilesStaleUnmanagedDefaultInPlace(t *testing.T)
|
|||
ID: "default-id",
|
||||
Name: "default",
|
||||
Managed: false,
|
||||
RootfsPath: "/home/thales/projects/personal/banger/rootfs-docker.ext4",
|
||||
KernelPath: "/home/thales/projects/personal/banger/wtf/root/boot/vmlinux-6.8.0-94-generic",
|
||||
InitrdPath: "/home/thales/projects/personal/banger/wtf/root/boot/initrd.img-6.8.0-94-generic",
|
||||
ModulesDir: "/home/thales/projects/personal/banger/wtf/root/lib/modules/6.8.0-94-generic",
|
||||
PackagesPath: "/home/thales/projects/personal/banger/packages.apt",
|
||||
RootfsPath: "/home/thales/projects/personal/banger/build/runtime/rootfs-docker.ext4",
|
||||
KernelPath: "/home/thales/projects/personal/banger/build/runtime/wtf/root/boot/vmlinux-6.8.0-94-generic",
|
||||
InitrdPath: "/home/thales/projects/personal/banger/build/runtime/wtf/root/boot/initrd.img-6.8.0-94-generic",
|
||||
ModulesDir: "/home/thales/projects/personal/banger/build/runtime/wtf/root/lib/modules/6.8.0-94-generic",
|
||||
PackagesPath: "/home/thales/projects/personal/banger/build/runtime/packages.apt",
|
||||
Docker: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
|
|
|
|||
|
|
@ -86,14 +86,24 @@ func ResolveRuntimeDir(configuredRuntimeDir, deprecatedRepoRoot string) string {
|
|||
}
|
||||
exeDir := filepath.Dir(exe)
|
||||
if filepath.Base(exeDir) == "bin" {
|
||||
if filepath.Base(filepath.Dir(exeDir)) == "build" {
|
||||
buildRuntimeDir := filepath.Clean(filepath.Join(exeDir, "..", "runtime"))
|
||||
if HasRuntimeBundle(buildRuntimeDir) {
|
||||
return buildRuntimeDir
|
||||
}
|
||||
}
|
||||
installRuntimeDir := filepath.Clean(filepath.Join(exeDir, "..", "lib", "banger"))
|
||||
if HasRuntimeBundle(installRuntimeDir) {
|
||||
return installRuntimeDir
|
||||
}
|
||||
}
|
||||
sourceRuntimeDir := filepath.Join(exeDir, "runtime")
|
||||
if HasRuntimeBundle(sourceRuntimeDir) {
|
||||
return sourceRuntimeDir
|
||||
for _, sourceRuntimeDir := range []string{
|
||||
filepath.Join(exeDir, "build", "runtime"),
|
||||
filepath.Join(exeDir, "runtime"),
|
||||
} {
|
||||
if HasRuntimeBundle(sourceRuntimeDir) {
|
||||
return sourceRuntimeDir
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -141,7 +151,7 @@ func BangerdPath() (string, error) {
|
|||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("bangerd binary not found next to banger; build ./cmd/bangerd")
|
||||
return "", errors.New("bangerd binary not found next to banger; run `make build`")
|
||||
}
|
||||
|
||||
func RuntimeBundleHint() string {
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ func TestResolveRuntimeDirUsesInstalledLayout(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolveRuntimeDirUsesSourceCheckoutRuntimeSubdir(t *testing.T) {
|
||||
func TestResolveRuntimeDirUsesBuildRuntimeForSourceCheckoutBinary(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
runtimeDir := filepath.Join(root, "runtime")
|
||||
runtimeDir := filepath.Join(root, "build", "runtime")
|
||||
createRuntimeBundle(t, runtimeDir)
|
||||
|
||||
origExecutablePath := executablePath
|
||||
|
|
@ -53,6 +53,24 @@ func TestResolveRuntimeDirUsesSourceCheckoutRuntimeSubdir(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestResolveRuntimeDirUsesBuildRuntimeForBuildBinExecutable(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
runtimeDir := filepath.Join(root, "build", "runtime")
|
||||
createRuntimeBundle(t, runtimeDir)
|
||||
|
||||
origExecutablePath := executablePath
|
||||
executablePath = func() (string, error) {
|
||||
return filepath.Join(root, "build", "bin", "banger"), nil
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
executablePath = origExecutablePath
|
||||
})
|
||||
|
||||
if got := ResolveRuntimeDir("", ""); got != runtimeDir {
|
||||
t.Fatalf("ResolveRuntimeDir() = %q, want %q", got, runtimeDir)
|
||||
}
|
||||
}
|
||||
|
||||
func createRuntimeBundle(t *testing.T, runtimeDir string) {
|
||||
t.Helper()
|
||||
metadata := runtimebundle.BundleMetadata{
|
||||
|
|
|
|||
|
|
@ -52,7 +52,13 @@ fi
|
|||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
BANGER_BIN="${BANGER_BIN:-$REPO_ROOT/banger}"
|
||||
if [[ -z "${BANGER_BIN:-}" ]]; then
|
||||
if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then
|
||||
BANGER_BIN="$REPO_ROOT/build/bin/banger"
|
||||
else
|
||||
BANGER_BIN="$REPO_ROOT/banger"
|
||||
fi
|
||||
fi
|
||||
if [[ ! -x "$BANGER_BIN" ]]; then
|
||||
log "banger binary not found: $BANGER_BIN"
|
||||
log "run 'make build' or set BANGER_BIN"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ log() {
|
|||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./customize.sh <base-rootfs> [--out <path>] [--size <size>] [--kernel <path>] [--initrd <path>] [--docker] [--modules <dir>]
|
||||
Usage: ./scripts/customize.sh <base-rootfs> [--out <path>] [--size <size>] [--kernel <path>] [--initrd <path>] [--docker] [--modules <dir>]
|
||||
|
||||
Creates a copy of rootfs.ext4, optionally resizes it, boots a VM using the
|
||||
copy as a writable rootfs, then applies base configuration and packages.
|
||||
|
|
@ -30,9 +30,10 @@ parse_size() {
|
|||
}
|
||||
|
||||
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"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/runtime"
|
||||
fi
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
|
||||
if [[ ! -d "$RUNTIME_DIR" ]]; then
|
||||
|
|
@ -40,7 +41,7 @@ if [[ ! -d "$RUNTIME_DIR" ]]; then
|
|||
log "run 'make runtime-bundle' or set BANGER_RUNTIME_DIR"
|
||||
exit 1
|
||||
fi
|
||||
source "$RUNTIME_DIR/packages.sh"
|
||||
source "$SCRIPT_DIR/lib/packages.sh"
|
||||
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/image-build}"
|
||||
VM_ROOT="$STATE/vms"
|
||||
mkdir -p "$VM_ROOT"
|
||||
|
|
@ -83,8 +84,12 @@ resolve_banger_bin() {
|
|||
printf '%s\n' "$BANGER_BIN"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$SCRIPT_DIR/banger" ]]; then
|
||||
printf '%s\n' "$SCRIPT_DIR/banger"
|
||||
if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/build/bin/banger"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/banger"
|
||||
return
|
||||
fi
|
||||
if command -v banger >/dev/null 2>&1; then
|
||||
|
|
@ -7,7 +7,7 @@ log() {
|
|||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./interactive.sh <base-rootfs> [--out <path>] [--size <size>]
|
||||
Usage: ./scripts/interactive.sh <base-rootfs> [--out <path>] [--size <size>]
|
||||
|
||||
Creates a writable copy of the base rootfs and boots a VM so you can
|
||||
customize it manually over SSH. No automatic package/config changes
|
||||
|
|
@ -30,10 +30,11 @@ parse_size() {
|
|||
return 1
|
||||
}
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEFAULT_RUNTIME_DIR="$DIR"
|
||||
if [[ -d "$DIR/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$DIR/runtime"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/runtime"
|
||||
fi
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
|
||||
if [[ ! -d "$RUNTIME_DIR" ]]; then
|
||||
|
|
@ -77,8 +78,12 @@ resolve_banger_bin() {
|
|||
printf '%s\n' "$BANGER_BIN"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$DIR/banger" ]]; then
|
||||
printf '%s\n' "$DIR/banger"
|
||||
if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/build/bin/banger"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/banger"
|
||||
return
|
||||
fi
|
||||
if command -v banger >/dev/null 2>&1; then
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
BANGER_PACKAGES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BANGER_APT_PACKAGES_FILE="${BANGER_APT_PACKAGES_FILE:-$BANGER_PACKAGES_DIR/packages.apt}"
|
||||
readonly BANGER_PACKAGES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly BANGER_REPO_ROOT="$(cd "$BANGER_PACKAGES_DIR/../.." && pwd)"
|
||||
BANGER_APT_PACKAGES_FILE="${BANGER_APT_PACKAGES_FILE:-$BANGER_REPO_ROOT/config/packages.apt}"
|
||||
|
||||
banger_packages_file() {
|
||||
printf '%s' "$BANGER_APT_PACKAGES_FILE"
|
||||
|
|
@ -7,21 +7,21 @@ log() {
|
|||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./make-rootfs-void.sh [--out <path>] [--size <size>] [--mirror <url>] [--arch <arch>] [--packages <path>]
|
||||
Usage: ./scripts/make-rootfs-void.sh [--out <path>] [--size <size>] [--mirror <url>] [--arch <arch>] [--packages <path>]
|
||||
|
||||
Build an experimental Void Linux rootfs image plus a matching /root work-seed.
|
||||
|
||||
Defaults:
|
||||
--out ./runtime/rootfs-void.ext4
|
||||
--out ./build/runtime/rootfs-void.ext4
|
||||
--size 2G
|
||||
--mirror https://repo-default.voidlinux.org
|
||||
--arch x86_64
|
||||
--packages ./packages.void
|
||||
--packages ./config/packages.void
|
||||
|
||||
This path is experimental and local-only. If ./runtime/void-kernel exists it
|
||||
uses the staged Void kernel modules from that directory; otherwise it falls back
|
||||
to the current runtime bundle modules. It does not change the default Debian
|
||||
image flow.
|
||||
This path is experimental and local-only. If ./build/runtime/void-kernel exists
|
||||
it uses the staged Void kernel modules from that directory; otherwise it falls
|
||||
back to the current runtime bundle modules. It does not change the default
|
||||
Debian image flow.
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -53,8 +53,12 @@ resolve_banger_bin() {
|
|||
printf '%s\n' "$BANGER_BIN"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$SCRIPT_DIR/banger" ]]; then
|
||||
printf '%s\n' "$SCRIPT_DIR/banger"
|
||||
if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/build/bin/banger"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/banger"
|
||||
return
|
||||
fi
|
||||
if command -v banger >/dev/null 2>&1; then
|
||||
|
|
@ -377,13 +381,14 @@ cleanup() {
|
|||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PACKAGES_FILE="$SCRIPT_DIR/packages.void"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PACKAGES_FILE="$REPO_ROOT/config/packages.void"
|
||||
export BANGER_APT_PACKAGES_FILE="$PACKAGES_FILE"
|
||||
source "$SCRIPT_DIR/packages.sh"
|
||||
source "$SCRIPT_DIR/lib/packages.sh"
|
||||
|
||||
DEFAULT_RUNTIME_DIR="$SCRIPT_DIR"
|
||||
if [[ -d "$SCRIPT_DIR/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$SCRIPT_DIR/runtime"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/runtime"
|
||||
fi
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
|
||||
if [[ ! -d "$RUNTIME_DIR" ]]; then
|
||||
|
|
@ -401,8 +406,8 @@ ARCH="x86_64"
|
|||
MISE_VERSION="v2025.12.0"
|
||||
MISE_INSTALL_PATH="/usr/local/bin/mise"
|
||||
OPENCODE_TOOL="github:anomalyco/opencode"
|
||||
GUESTNET_BOOTSTRAP_SCRIPT="$SCRIPT_DIR/internal/guestnet/assets/bootstrap.sh"
|
||||
GUESTNET_VOID_CORE_SERVICE="$SCRIPT_DIR/internal/guestnet/assets/void-core-service.sh"
|
||||
GUESTNET_BOOTSTRAP_SCRIPT="$REPO_ROOT/internal/guestnet/assets/bootstrap.sh"
|
||||
GUESTNET_VOID_CORE_SERVICE="$REPO_ROOT/internal/guestnet/assets/void-core-service.sh"
|
||||
MODULES_DIR="$(bundle_path default_modules_dir "$RUNTIME_DIR/wtf/root/lib/modules/6.8.0-94-generic")"
|
||||
VOID_KERNEL_MODULES_DIR="$(find_latest_module_dir "$RUNTIME_DIR/void-kernel/lib/modules" || true)"
|
||||
VSOCK_AGENT="$(bundle_path vsock_agent_path "$RUNTIME_DIR/banger-vsock-agent")"
|
||||
|
|
@ -7,19 +7,25 @@ log() {
|
|||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./make-rootfs.sh [--size <size>] [--base-rootfs <path>]
|
||||
Usage: ./scripts/make-rootfs.sh [--size <size>] [--base-rootfs <path>]
|
||||
|
||||
Builds rootfs-docker.ext4 using customize.sh. If --base-rootfs is omitted,
|
||||
the first existing file is used:
|
||||
./rootfs.ext4
|
||||
Builds build/runtime/rootfs-docker.ext4 using scripts/customize.sh. If
|
||||
--base-rootfs is omitted, the first existing file is used:
|
||||
./build/runtime/rootfs.ext4
|
||||
./runtime/rootfs.ext4 (legacy fallback)
|
||||
./ubuntu-noble-rootfs/rootfs.ext4
|
||||
./ubuntu-lts/rootfs.ext4
|
||||
EOF
|
||||
}
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$DIR/packages.sh"
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DIR/runtime}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
source "$SCRIPT_DIR/lib/packages.sh"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/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"
|
||||
|
|
@ -63,10 +69,10 @@ fi
|
|||
if [[ -z "$BASE_ROOTFS" ]]; then
|
||||
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"
|
||||
elif [[ -f "$REPO_ROOT/ubuntu-noble-rootfs/rootfs.ext4" ]]; then
|
||||
BASE_ROOTFS="$REPO_ROOT/ubuntu-noble-rootfs/rootfs.ext4"
|
||||
elif [[ -f "$REPO_ROOT/ubuntu-lts/rootfs.ext4" ]]; then
|
||||
BASE_ROOTFS="$REPO_ROOT/ubuntu-lts/rootfs.ext4"
|
||||
else
|
||||
log "no base rootfs found; run 'make runtime-bundle' or pass --base-rootfs"
|
||||
exit 1
|
||||
|
|
@ -76,7 +82,7 @@ fi
|
|||
mkdir -p "$RUNTIME_DIR"
|
||||
|
||||
log "building $OUT_ROOTFS from $BASE_ROOTFS"
|
||||
exec env BANGER_RUNTIME_DIR="$RUNTIME_DIR" "$DIR/customize.sh" "$BASE_ROOTFS" \
|
||||
exec env BANGER_RUNTIME_DIR="$RUNTIME_DIR" "$SCRIPT_DIR/customize.sh" "$BASE_ROOTFS" \
|
||||
--out "$OUT_ROOTFS" \
|
||||
--size "$SIZE_SPEC" \
|
||||
--docker
|
||||
|
|
@ -7,13 +7,14 @@ log() {
|
|||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./make-void-kernel.sh [--out-dir <path>] [--mirror <url>] [--arch <arch>] [--kernel-package <name>] [--print-register-flags]
|
||||
Usage: ./scripts/make-void-kernel.sh [--out-dir <path>] [--mirror <url>] [--arch <arch>] [--kernel-package <name>] [--print-register-flags]
|
||||
|
||||
Download and stage a Void Linux kernel under ./runtime/void-kernel for the
|
||||
Download and stage a Void Linux kernel under ./build/runtime/void-kernel for
|
||||
the
|
||||
experimental Void guest flow.
|
||||
|
||||
Defaults:
|
||||
--out-dir ./runtime/void-kernel
|
||||
--out-dir ./build/runtime/void-kernel
|
||||
--mirror https://repo-default.voidlinux.org
|
||||
--arch x86_64
|
||||
--kernel-package linux6.12
|
||||
|
|
@ -223,9 +224,10 @@ cleanup() {
|
|||
}
|
||||
|
||||
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"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/runtime"
|
||||
fi
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
|
||||
OUT_DIR="$RUNTIME_DIR/void-kernel"
|
||||
|
|
@ -282,10 +284,7 @@ if [[ "$ARCH" != "x86_64" ]]; then
|
|||
log "this experimental downloader currently supports only x86_64"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "$RUNTIME_DIR" ]]; then
|
||||
log "runtime bundle not found: $RUNTIME_DIR"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
if [[ -e "$OUT_DIR" ]]; then
|
||||
log "output directory already exists: $OUT_DIR"
|
||||
log "remove it first if you want to re-stage a different Void kernel"
|
||||
|
|
@ -27,8 +27,12 @@ resolve_banger_bin() {
|
|||
printf '%s\n' "$BANGER_BIN"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$SCRIPT_DIR/banger" ]]; then
|
||||
printf '%s\n' "$SCRIPT_DIR/banger"
|
||||
if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/build/bin/banger"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/banger"
|
||||
return
|
||||
fi
|
||||
if command -v banger >/dev/null 2>&1; then
|
||||
|
|
@ -40,9 +44,10 @@ resolve_banger_bin() {
|
|||
}
|
||||
|
||||
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"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/runtime"
|
||||
fi
|
||||
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
|
||||
|
|
@ -50,7 +55,7 @@ IMAGE_NAME="${VOID_IMAGE_NAME:-void-exp}"
|
|||
BANGER_BIN="$(resolve_banger_bin)"
|
||||
ROOTFS="$RUNTIME_DIR/rootfs-void.ext4"
|
||||
WORK_SEED="$RUNTIME_DIR/rootfs-void.work-seed.ext4"
|
||||
PACKAGES="$SCRIPT_DIR/packages.void"
|
||||
PACKAGES="$REPO_ROOT/config/packages.void"
|
||||
|
||||
if [[ ! -f "$ROOTFS" ]]; then
|
||||
log "missing Void rootfs: $ROOTFS"
|
||||
|
|
@ -5,10 +5,11 @@ 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"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/runtime"
|
||||
fi
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
|
||||
SSH_KEY="$RUNTIME_DIR/id_ed25519"
|
||||
|
|
@ -35,6 +36,29 @@ SSH_COMMON_ARGS=(
|
|||
)
|
||||
OPENCODE_PORT=4096
|
||||
|
||||
resolve_banger_bin() {
|
||||
if [[ -n "${BANGER_BIN:-}" ]]; then
|
||||
printf '%s\n' "$BANGER_BIN"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/build/bin/banger"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/banger"
|
||||
return
|
||||
fi
|
||||
if command -v banger >/dev/null 2>&1; then
|
||||
command -v banger
|
||||
return
|
||||
fi
|
||||
log "banger binary not found; run 'make build' or set BANGER_BIN"
|
||||
exit 1
|
||||
}
|
||||
|
||||
BANGER_BIN="$(resolve_banger_bin)"
|
||||
|
||||
firecracker_running() {
|
||||
local pid="$1"
|
||||
local api_sock="$2"
|
||||
|
|
@ -85,7 +109,7 @@ wait_for_tcp() {
|
|||
}
|
||||
|
||||
refresh_vm_metadata() {
|
||||
if ! VM_JSON="$(./banger vm show "$VM_NAME" 2>/dev/null)"; then
|
||||
if ! VM_JSON="$("$BANGER_BIN" vm show "$VM_NAME" 2>/dev/null)"; then
|
||||
return 1
|
||||
fi
|
||||
TAP="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.tap_device // empty')"
|
||||
|
|
@ -125,13 +149,13 @@ wait_for_vm_ready() {
|
|||
|
||||
dump_diagnostics() {
|
||||
log "diagnostics for $VM_NAME"
|
||||
./banger vm show "$VM_NAME" || true
|
||||
"$BANGER_BIN" vm show "$VM_NAME" || true
|
||||
if [[ "${PID:-0}" -gt 0 ]]; then
|
||||
log "process state for pid $PID"
|
||||
ps -fp "$PID" || true
|
||||
fi
|
||||
log "recent firecracker log"
|
||||
./banger vm logs "$VM_NAME" 2>/dev/null | tail -n 200 || true
|
||||
"$BANGER_BIN" vm logs "$VM_NAME" 2>/dev/null | tail -n 200 || true
|
||||
if [[ -f "$DAEMON_LOG" ]]; then
|
||||
log "recent daemon log"
|
||||
tail -n 200 "$DAEMON_LOG" || true
|
||||
|
|
@ -153,7 +177,7 @@ dump_diagnostics() {
|
|||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./verify.sh [--nat] [--image <name>]
|
||||
Usage: ./scripts/verify.sh [--nat] [--image <name>]
|
||||
|
||||
Run a basic smoke test for the Go VM workflow.
|
||||
Use --nat to additionally verify outbound NAT and host rule cleanup.
|
||||
|
|
@ -198,20 +222,20 @@ LAST_ERROR=""
|
|||
|
||||
delete_vm() {
|
||||
if [[ -n "${VM_NAME:-}" ]]; then
|
||||
./banger vm delete "$VM_NAME"
|
||||
"$BANGER_BIN" vm delete "$VM_NAME"
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${VM_NAME:-}" ]]; then
|
||||
./banger vm delete "$VM_NAME" >/dev/null 2>&1 || true
|
||||
"$BANGER_BIN" vm delete "$VM_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
log "starting VM"
|
||||
CREATE_ARGS=(./banger vm create --name "$VM_NAME")
|
||||
CREATE_ARGS=("$BANGER_BIN" vm create --name "$VM_NAME")
|
||||
if [[ -n "$IMAGE_NAME" ]]; then
|
||||
CREATE_ARGS+=(--image "$IMAGE_NAME")
|
||||
fi
|
||||
|
|
@ -267,7 +291,7 @@ if ! wait_for_tcp "$GUEST_IP" "$OPENCODE_PORT" "$BOOT_DEADLINE"; then
|
|||
fi
|
||||
|
||||
log "asserting opencode port is reported by banger vm ports"
|
||||
if ! ./banger vm ports "$VM_NAME" | grep -F ":${OPENCODE_PORT}" >/dev/null 2>&1; then
|
||||
if ! "$BANGER_BIN" vm ports "$VM_NAME" | grep -F ":${OPENCODE_PORT}" >/dev/null 2>&1; then
|
||||
log "banger vm ports did not report ${OPENCODE_PORT}"
|
||||
dump_diagnostics
|
||||
exit 1
|
||||
|
|
@ -286,7 +310,7 @@ if ! delete_vm; then
|
|||
fi
|
||||
|
||||
log "asserting cleanup success"
|
||||
if ./banger vm show "$VM_NAME" >/dev/null 2>&1; then
|
||||
if "$BANGER_BIN" vm show "$VM_NAME" >/dev/null 2>&1; then
|
||||
log "vm still exists after delete: $VM_NAME"
|
||||
exit 1
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue