Prune legacy void/alpine + customize.sh flows
The golden-image Dockerfile + catalog pipeline replaces the entire
manual rootfs-build stack. With that shipped, the per-distro shell
flows are dead code.
Removed:
- scripts/customize.sh, scripts/interactive.sh, scripts/verify.sh
- scripts/make-rootfs{,-void,-alpine}.sh
- scripts/register-{void,alpine}-image.sh
- scripts/make-{void,alpine}-kernel.sh
- internal/imagepreset/ (only consumer was `banger internal packages`,
which fed customize.sh)
- examples/{void,alpine}.config.toml
- Makefile targets: rootfs, rootfs-void, rootfs-alpine, void-kernel,
alpine-kernel, void-register, alpine-register, void-vm, alpine-vm,
verify-void, verify-alpine, plus the ALPINE_RELEASE / *_IMAGE_NAME
/ *_VM_NAME variables
The void-6.12 kernel catalog entry is also gone — golden image pairs
with generic-6.12 and nothing else in the catalog depended on it.
Consolidated: imagemgr now holds the small DebianBasePackages list +
package-hash helper inline, so the `image build --from-image` flow
(still supported) no longer pulls from a separate imagepreset package.
Net: 3,815 lines deleted, 59 added. No runtime functionality removed
beyond the `banger internal packages` subcommand (hidden, used only
by the deleted customize.sh).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8029b2e1bc
commit
6083e2dde5
23 changed files with 73 additions and 3814 deletions
67
Makefile
67
Makefile
|
|
@ -21,11 +21,6 @@ GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort)
|
||||||
# any redundant invocations.
|
# any redundant invocations.
|
||||||
BUILD_INPUTS := $(shell find cmd internal -type f | sort)
|
BUILD_INPUTS := $(shell find cmd internal -type f | sort)
|
||||||
SHELL_SOURCES := $(shell find scripts -type f -name '*.sh' | sort)
|
SHELL_SOURCES := $(shell find scripts -type f -name '*.sh' | sort)
|
||||||
VOID_IMAGE_NAME ?= void
|
|
||||||
VOID_VM_NAME ?= void-dev
|
|
||||||
ALPINE_RELEASE ?= 3.23.3
|
|
||||||
ALPINE_IMAGE_NAME ?= alpine
|
|
||||||
ALPINE_VM_NAME ?= alpine-dev
|
|
||||||
VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev)
|
VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev)
|
||||||
COMMIT ?= $(shell git rev-parse --verify HEAD 2>/dev/null || echo unknown)
|
COMMIT ?= $(shell git rev-parse --verify HEAD 2>/dev/null || echo unknown)
|
||||||
BUILT_AT ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
BUILT_AT ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
@ -33,30 +28,19 @@ GO_LDFLAGS := -X banger/internal/buildinfo.Version=$(VERSION) -X banger/internal
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
.PHONY: help build banger bangerd test fmt tidy clean rootfs rootfs-void void-kernel void-register void-vm verify-void alpine-kernel rootfs-alpine alpine-register alpine-vm verify-alpine install bench-create lint lint-go lint-shell
|
.PHONY: help build banger bangerd test fmt tidy clean install bench-create lint lint-go lint-shell
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@printf '%s\n' \
|
@printf '%s\n' \
|
||||||
'Targets:' \
|
'Targets:' \
|
||||||
' make build Build ./build/bin/banger, ./build/bin/bangerd, and ./build/bin/banger-vsock-agent' \
|
' make build Build ./build/bin/banger, ./build/bin/bangerd, and ./build/bin/banger-vsock-agent' \
|
||||||
' make bench-create Benchmark vm create and SSH readiness with scripts/bench-create.sh' \
|
' make install Build and install banger, bangerd, and the companion vsock helper' \
|
||||||
' make install Build and install banger, bangerd, and the companion vsock helper' \
|
' make test Run go test ./...' \
|
||||||
' make test Run go test ./...' \
|
' make lint Run gofmt + go vet + shellcheck (errors)' \
|
||||||
' make lint Run gofmt + go vet + shellcheck (errors)' \
|
' 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 bench-create Benchmark vm create and SSH readiness with scripts/bench-create.sh'
|
||||||
' make rootfs Rebuild the manual Debian rootfs image in ./build/manual' \
|
|
||||||
' make void-kernel Download and stage a Void kernel, initramfs, and modules under ./build/manual/void-kernel' \
|
|
||||||
' make rootfs-void Build an experimental Void Linux rootfs and work-seed in ./build/manual' \
|
|
||||||
' 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 scripts/verify.sh against it' \
|
|
||||||
' make alpine-kernel Download and stage an Alpine virt kernel, initramfs, and modules under ./build/manual/alpine-kernel' \
|
|
||||||
' make rootfs-alpine Build an experimental Alpine Linux rootfs and work-seed in ./build/manual' \
|
|
||||||
' make alpine-register Register or update the experimental Alpine image as $(ALPINE_IMAGE_NAME)' \
|
|
||||||
' make alpine-vm Register the experimental Alpine image and create a VM named $(ALPINE_VM_NAME)' \
|
|
||||||
' make verify-alpine Register the experimental Alpine image and run scripts/verify.sh against it'
|
|
||||||
|
|
||||||
build: $(BINARIES)
|
build: $(BINARIES)
|
||||||
|
|
||||||
|
|
@ -107,36 +91,3 @@ install: build
|
||||||
$(INSTALL) -m 0755 "$(BANGER_BIN)" "$(DESTDIR)$(BINDIR)/banger"
|
$(INSTALL) -m 0755 "$(BANGER_BIN)" "$(DESTDIR)$(BINDIR)/banger"
|
||||||
$(INSTALL) -m 0755 "$(BANGERD_BIN)" "$(DESTDIR)$(BINDIR)/bangerd"
|
$(INSTALL) -m 0755 "$(BANGERD_BIN)" "$(DESTDIR)$(BINDIR)/bangerd"
|
||||||
$(INSTALL) -m 0755 "$(VSOCK_AGENT_BIN)" "$(DESTDIR)$(LIBDIR)/banger/banger-vsock-agent"
|
$(INSTALL) -m 0755 "$(VSOCK_AGENT_BIN)" "$(DESTDIR)$(LIBDIR)/banger/banger-vsock-agent"
|
||||||
|
|
||||||
rootfs:
|
|
||||||
BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/make-rootfs.sh $(ARGS)
|
|
||||||
|
|
||||||
void-kernel:
|
|
||||||
BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" ./scripts/make-void-kernel.sh $(ARGS)
|
|
||||||
|
|
||||||
rootfs-void:
|
|
||||||
BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/make-rootfs-void.sh $(ARGS)
|
|
||||||
|
|
||||||
void-register: build
|
|
||||||
BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" VOID_IMAGE_NAME="$(VOID_IMAGE_NAME)" BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/register-void-image.sh
|
|
||||||
|
|
||||||
void-vm: void-register
|
|
||||||
"$(abspath $(BANGER_BIN))" vm create --image "$(VOID_IMAGE_NAME)" --name "$(VOID_VM_NAME)"
|
|
||||||
|
|
||||||
verify-void: void-register
|
|
||||||
BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/verify.sh --image "$(VOID_IMAGE_NAME)"
|
|
||||||
|
|
||||||
alpine-kernel:
|
|
||||||
BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" ALPINE_RELEASE="$(ALPINE_RELEASE)" ./scripts/make-alpine-kernel.sh $(ARGS)
|
|
||||||
|
|
||||||
rootfs-alpine:
|
|
||||||
BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" ALPINE_RELEASE="$(ALPINE_RELEASE)" BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/make-rootfs-alpine.sh $(ARGS)
|
|
||||||
|
|
||||||
alpine-register: build
|
|
||||||
BANGER_MANUAL_DIR="$(abspath $(BUILD_MANUAL_DIR))" ALPINE_IMAGE_NAME="$(ALPINE_IMAGE_NAME)" BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/register-alpine-image.sh
|
|
||||||
|
|
||||||
alpine-vm: alpine-register
|
|
||||||
"$(abspath $(BANGER_BIN))" vm create --image "$(ALPINE_IMAGE_NAME)" --name "$(ALPINE_VM_NAME)"
|
|
||||||
|
|
||||||
verify-alpine: alpine-register
|
|
||||||
BANGER_BIN="$(abspath $(BANGER_BIN))" ./scripts/verify.sh --image "$(ALPINE_IMAGE_NAME)"
|
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,8 @@ traversal entries and unsafe symlinks are rejected.
|
||||||
**`generic-<version>`** — built from upstream kernel.org sources with
|
**`generic-<version>`** — built from upstream kernel.org sources with
|
||||||
Firecracker's official config. All essential drivers (virtio_blk,
|
Firecracker's official config. All essential drivers (virtio_blk,
|
||||||
virtio_net, ext4, vsock) compiled in — no modules, no initramfs. This
|
virtio_net, ext4, vsock) compiled in — no modules, no initramfs. This
|
||||||
is the recommended kernel for OCI-pulled images (Debian, Ubuntu,
|
is the kernel the golden image pairs with and the recommended kernel
|
||||||
Fedora, etc.). Build with `scripts/make-generic-kernel.sh`.
|
for OCI-pulled images. Build with `scripts/make-generic-kernel.sh`.
|
||||||
|
|
||||||
**`void-<version>` / `alpine-<version>`** — distro-specific kernels
|
|
||||||
built from Void/Alpine package repos. Include initramfs + modules.
|
|
||||||
These are for the `make rootfs-void` / `make rootfs-alpine` manual
|
|
||||||
flows where the initramfs is paired with its matching rootfs.
|
|
||||||
|
|
||||||
## Adding or updating an entry
|
## Adding or updating an entry
|
||||||
|
|
||||||
|
|
@ -50,8 +45,8 @@ The repo has no CI for kernel publishing yet. Catalog updates are manual
|
||||||
and infrequent (kernel version bumps every few weeks at most).
|
and infrequent (kernel version bumps every few weeks at most).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Build the kernel locally with the existing helper.
|
# 1. Build the kernel locally.
|
||||||
scripts/make-generic-kernel.sh # or: make void-kernel / make alpine-kernel
|
scripts/make-generic-kernel.sh
|
||||||
|
|
||||||
# 2. Import it into the local catalog so the canonical layout exists.
|
# 2. Import it into the local catalog so the canonical layout exists.
|
||||||
banger kernel import generic-6.12 \
|
banger kernel import generic-6.12 \
|
||||||
|
|
@ -129,26 +124,11 @@ If hosting ever moves, catalog entries can be migrated by reuploading the
|
||||||
tarballs and editing the URLs in `catalog.json` — no other code changes
|
tarballs and editing the URLs in `catalog.json` — no other code changes
|
||||||
required.
|
required.
|
||||||
|
|
||||||
## Tech debt: kernel-build scripts
|
## Tech debt
|
||||||
|
|
||||||
`scripts/make-void-kernel.sh` and `scripts/make-alpine-kernel.sh` are
|
- Kernel publishing is manual; there is no CI yet. `scripts/make-generic-kernel.sh`
|
||||||
procedural bash that fetches and patches per-distro kernel sources.
|
plus `scripts/publish-kernel.sh` is fine while refreshes are
|
||||||
Each new distro means a new bespoke script. They're "good enough"
|
infrequent and maintainer-only. CI becomes relevant once banger
|
||||||
because catalog refreshes are infrequent and only the maintainer runs
|
goes public.
|
||||||
them, but they are the bottleneck if the catalog ever wants to grow
|
- `make lint-shell` runs at `--severity=error` only. Tightening to
|
||||||
beyond two distros.
|
`--severity=warning` is a nice-to-have but low priority.
|
||||||
|
|
||||||
A future iteration should:
|
|
||||||
|
|
||||||
- Move kernel acquisition into a Go (or at least uniform) tool with a
|
|
||||||
per-distro plugin/config rather than per-distro scripts.
|
|
||||||
- Encode kernel config and required modules declaratively so a Debian
|
|
||||||
or Fedora target is a config addition, not a new script.
|
|
||||||
- Run unattended in CI once banger goes public — the manual
|
|
||||||
`scripts/publish-kernel.sh` flow scales until then.
|
|
||||||
|
|
||||||
Until that happens, `make lint-shell` only runs at `--severity=error`.
|
|
||||||
Tightening to `--severity=warning` would surface real issues in the
|
|
||||||
legacy build scripts (mostly `sudo cat > file` redirects and
|
|
||||||
heredoc-quoting concerns); fixing those is a prerequisite to bumping
|
|
||||||
the lint floor.
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# Experimental Alpine Linux guest profile for local testing.
|
|
||||||
#
|
|
||||||
# Register or promote a complete `alpine` image first, then point the daemon
|
|
||||||
# at it by name. Firecracker is resolved from PATH by default; set
|
|
||||||
# `firecracker_bin` only if you need an override.
|
|
||||||
|
|
||||||
default_image_name = "alpine"
|
|
||||||
# firecracker_bin = "/usr/bin/firecracker"
|
|
||||||
# ssh_key_path = "/abs/path/to/private/key"
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# Experimental Void Linux guest profile for local testing.
|
|
||||||
#
|
|
||||||
# Register or promote a complete `void` image first, then point the daemon
|
|
||||||
# at it by name. Firecracker is resolved from PATH by default; set
|
|
||||||
# `firecracker_bin` only if you need an override.
|
|
||||||
|
|
||||||
default_image_name = "void"
|
|
||||||
# firecracker_bin = "/usr/bin/firecracker"
|
|
||||||
# ssh_key_path = "/abs/path/to/private/key"
|
|
||||||
|
|
@ -75,7 +75,7 @@ RUN curl -fsSL https://mise.run | MISE_INSTALL_PATH=/usr/local/bin/mise sh \
|
||||||
> /etc/profile.d/mise.sh \
|
> /etc/profile.d/mise.sh \
|
||||||
&& chmod 0644 /etc/profile.d/mise.sh
|
&& chmod 0644 /etc/profile.d/mise.sh
|
||||||
|
|
||||||
# Git default branch — matches the old customize.sh opinion.
|
# Default branch for any git init inside the sandbox.
|
||||||
RUN git config --system init.defaultBranch main
|
RUN git config --system init.defaultBranch main
|
||||||
|
|
||||||
# `fd-find` installs as `fdfind` on Debian to avoid a long-standing name
|
# `fd-find` installs as `fdfind` on Debian to avoid a long-standing name
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import (
|
||||||
"banger/internal/guest"
|
"banger/internal/guest"
|
||||||
"banger/internal/hostnat"
|
"banger/internal/hostnat"
|
||||||
"banger/internal/imagecat"
|
"banger/internal/imagecat"
|
||||||
"banger/internal/imagepreset"
|
|
||||||
"banger/internal/imagepull"
|
"banger/internal/imagepull"
|
||||||
"banger/internal/model"
|
"banger/internal/model"
|
||||||
"banger/internal/paths"
|
"banger/internal/paths"
|
||||||
|
|
@ -219,7 +218,6 @@ func newInternalCommand() *cobra.Command {
|
||||||
newInternalSSHKeyPathCommand(),
|
newInternalSSHKeyPathCommand(),
|
||||||
newInternalFirecrackerPathCommand(),
|
newInternalFirecrackerPathCommand(),
|
||||||
newInternalVSockAgentPathCommand(),
|
newInternalVSockAgentPathCommand(),
|
||||||
newInternalPackagesCommand(),
|
|
||||||
newInternalMakeBundleCommand(),
|
newInternalMakeBundleCommand(),
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
|
|
@ -284,39 +282,6 @@ func newInternalVSockAgentPathCommand() *cobra.Command {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newInternalPackagesCommand() *cobra.Command {
|
|
||||||
var docker bool
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "packages <debian|void|alpine>",
|
|
||||||
Hidden: true,
|
|
||||||
Args: exactArgsUsage(1, "usage: banger internal packages <debian|void|alpine> [--docker]"),
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
var packages []string
|
|
||||||
switch strings.TrimSpace(args[0]) {
|
|
||||||
case "debian":
|
|
||||||
packages = imagepreset.DebianBasePackages()
|
|
||||||
if docker {
|
|
||||||
packages = append(packages, "docker.io")
|
|
||||||
}
|
|
||||||
case "void":
|
|
||||||
packages = imagepreset.VoidBasePackages()
|
|
||||||
case "alpine":
|
|
||||||
packages = imagepreset.AlpineBasePackages()
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown package preset %q", args[0])
|
|
||||||
}
|
|
||||||
for _, pkg := range packages {
|
|
||||||
if _, err := fmt.Fprintln(cmd.OutOrStdout(), pkg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
cmd.Flags().BoolVar(&docker, "docker", false, "include docker-specific additions")
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func newInternalMakeBundleCommand() *cobra.Command {
|
func newInternalMakeBundleCommand() *cobra.Command {
|
||||||
var (
|
var (
|
||||||
rootfsTarPath string
|
rootfsTarPath string
|
||||||
|
|
|
||||||
|
|
@ -190,24 +190,6 @@ func TestInternalNATFlagsExist(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInternalPackagesCommandSupportsAlpine(t *testing.T) {
|
|
||||||
cmd := NewBangerCommand()
|
|
||||||
var stdout bytes.Buffer
|
|
||||||
cmd.SetOut(&stdout)
|
|
||||||
cmd.SetArgs([]string{"internal", "packages", "alpine"})
|
|
||||||
|
|
||||||
if err := cmd.Execute(); err != nil {
|
|
||||||
t.Fatalf("Execute(): %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output := stdout.String()
|
|
||||||
for _, want := range []string{"alpine-base", "docker", "libgcc", "libstdc++", "mkinitfs", "openssh"} {
|
|
||||||
if !strings.Contains(output, want+"\n") {
|
|
||||||
t.Fatalf("output = %q, want package %q", output, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPSAndVMListAliasesAndFlagsExist(t *testing.T) {
|
func TestPSAndVMListAliasesAndFlagsExist(t *testing.T) {
|
||||||
root := NewBangerCommand()
|
root := NewBangerCommand()
|
||||||
ps, _, err := root.Find([]string{"ps"})
|
ps, _, err := root.Find([]string{"ps"})
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,44 @@ package imagemgr
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"banger/internal/imagepreset"
|
|
||||||
"banger/internal/system"
|
"banger/internal/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// debianBasePackages is the apt package list applied by
|
||||||
|
// `image build --from-image` to Debian-based managed rootfses. Small
|
||||||
|
// curated set: most of the developer tooling the golden image ships
|
||||||
|
// lives in the Dockerfile, not here.
|
||||||
|
var debianBasePackages = []string{
|
||||||
|
"make",
|
||||||
|
"git",
|
||||||
|
"less",
|
||||||
|
"tree",
|
||||||
|
"ca-certificates",
|
||||||
|
"curl",
|
||||||
|
"wget",
|
||||||
|
"iproute2",
|
||||||
|
"vim",
|
||||||
|
"tmux",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebianBasePackages returns a copy of the base package set.
|
||||||
|
func DebianBasePackages() []string {
|
||||||
|
return append([]string(nil), debianBasePackages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashPackages returns the hex sha256 of the package list, used as
|
||||||
|
// drift-detection metadata alongside a built rootfs.
|
||||||
|
func hashPackages(lines []string) string {
|
||||||
|
sum := sha256.Sum256([]byte(strings.Join(lines, "\n") + "\n"))
|
||||||
|
return fmt.Sprintf("%x", sum)
|
||||||
|
}
|
||||||
|
|
||||||
// ValidateRegisterPaths checks that rootfs + kernel exist and that optional
|
// ValidateRegisterPaths checks that rootfs + kernel exist and that optional
|
||||||
// artifacts, when provided, also exist.
|
// artifacts, when provided, also exist.
|
||||||
func ValidateRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir string) error {
|
func ValidateRegisterPaths(rootfsPath, workSeedPath, kernelPath, initrdPath, modulesDir string) error {
|
||||||
|
|
@ -102,7 +132,7 @@ func StageOptionalArtifactPath(artifactDir, stagedPath, name string) string {
|
||||||
// managed image build. The #feature:docker sentinel is appended when
|
// managed image build. The #feature:docker sentinel is appended when
|
||||||
// docker is requested.
|
// docker is requested.
|
||||||
func BuildMetadataPackages(docker bool) []string {
|
func BuildMetadataPackages(docker bool) []string {
|
||||||
packages := imagepreset.DebianBasePackages()
|
packages := DebianBasePackages()
|
||||||
if docker {
|
if docker {
|
||||||
packages = append(packages, "#feature:docker")
|
packages = append(packages, "#feature:docker")
|
||||||
}
|
}
|
||||||
|
|
@ -116,5 +146,5 @@ func WritePackagesMetadata(rootfsPath string, packages []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
metadataPath := rootfsPath + ".packages.sha256"
|
metadataPath := rootfsPath + ".packages.sha256"
|
||||||
return os.WriteFile(metadataPath, []byte(imagepreset.Hash(packages)+"\n"), 0o644)
|
return os.WriteFile(metadataPath, []byte(hashPackages(packages)+"\n"), 0o644)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import (
|
||||||
|
|
||||||
"banger/internal/api"
|
"banger/internal/api"
|
||||||
"banger/internal/daemon/imagemgr"
|
"banger/internal/daemon/imagemgr"
|
||||||
"banger/internal/imagepreset"
|
|
||||||
"banger/internal/kernelcat"
|
"banger/internal/kernelcat"
|
||||||
"banger/internal/model"
|
"banger/internal/model"
|
||||||
"banger/internal/system"
|
"banger/internal/system"
|
||||||
|
|
@ -86,7 +85,7 @@ func (d *Daemon) BuildImage(ctx context.Context, params api.ImageBuildParams) (i
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Image{}, err
|
return model.Image{}, err
|
||||||
}
|
}
|
||||||
packages := imagepreset.DebianBasePackages()
|
packages := imagemgr.DebianBasePackages()
|
||||||
metadataPackages := imagemgr.BuildMetadataPackages(params.Docker)
|
metadataPackages := imagemgr.BuildMetadataPackages(params.Docker)
|
||||||
spec := imageBuildSpec{
|
spec := imageBuildSpec{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
package imagepreset
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var debianBase = []string{
|
|
||||||
"make",
|
|
||||||
"git",
|
|
||||||
"less",
|
|
||||||
"tree",
|
|
||||||
"ca-certificates",
|
|
||||||
"curl",
|
|
||||||
"wget",
|
|
||||||
"iproute2",
|
|
||||||
"vim",
|
|
||||||
"tmux",
|
|
||||||
}
|
|
||||||
|
|
||||||
var voidBase = []string{
|
|
||||||
"base-minimal",
|
|
||||||
"base-devel",
|
|
||||||
"bash",
|
|
||||||
"ca-certificates",
|
|
||||||
"curl",
|
|
||||||
"docker",
|
|
||||||
"docker-compose",
|
|
||||||
"e2fsprogs",
|
|
||||||
"git",
|
|
||||||
"iproute2",
|
|
||||||
"less",
|
|
||||||
"make",
|
|
||||||
"openssh",
|
|
||||||
"procps-ng",
|
|
||||||
"runit",
|
|
||||||
"shadow",
|
|
||||||
"sudo",
|
|
||||||
"tmux",
|
|
||||||
"tree",
|
|
||||||
"vim",
|
|
||||||
"wget",
|
|
||||||
}
|
|
||||||
|
|
||||||
var alpineBase = []string{
|
|
||||||
"alpine-base",
|
|
||||||
"bash",
|
|
||||||
"ca-certificates",
|
|
||||||
"curl",
|
|
||||||
"docker",
|
|
||||||
"docker-cli-compose",
|
|
||||||
"e2fsprogs",
|
|
||||||
"git",
|
|
||||||
"iproute2",
|
|
||||||
"less",
|
|
||||||
"libgcc",
|
|
||||||
"libstdc++",
|
|
||||||
"make",
|
|
||||||
"mkinitfs",
|
|
||||||
"openssh",
|
|
||||||
"procps-ng",
|
|
||||||
"shadow",
|
|
||||||
"sudo",
|
|
||||||
"tmux",
|
|
||||||
"tree",
|
|
||||||
"vim",
|
|
||||||
"wget",
|
|
||||||
}
|
|
||||||
|
|
||||||
func DebianBasePackages() []string {
|
|
||||||
return append([]string(nil), debianBase...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func VoidBasePackages() []string {
|
|
||||||
return append([]string(nil), voidBase...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AlpineBasePackages() []string {
|
|
||||||
return append([]string(nil), alpineBase...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Hash(lines []string) string {
|
|
||||||
sum := sha256.Sum256([]byte(strings.Join(lines, "\n") + "\n"))
|
|
||||||
return fmt.Sprintf("%x", sum)
|
|
||||||
}
|
|
||||||
|
|
@ -10,16 +10,6 @@
|
||||||
"tarball_sha256": "d6f9ba2a957260063241cf9d79ae538d0c349107d37f0bfccc33281d29bd0901",
|
"tarball_sha256": "d6f9ba2a957260063241cf9d79ae538d0c349107d37f0bfccc33281d29bd0901",
|
||||||
"size_bytes": 9098722,
|
"size_bytes": 9098722,
|
||||||
"description": "Generic Firecracker kernel 6.12.8 (all drivers built-in, no initrd needed)"
|
"description": "Generic Firecracker kernel 6.12.8 (all drivers built-in, no initrd needed)"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "void-6.12",
|
|
||||||
"distro": "void",
|
|
||||||
"arch": "x86_64",
|
|
||||||
"kernel_version": "6.12.81_1",
|
|
||||||
"tarball_url": "https://kernels.thaloco.com/void-6.12-x86_64.tar.zst",
|
|
||||||
"tarball_sha256": "3de6d03c4a3b5d3b8164f20049ddcb38b32a1864ea7133f01ff7fbb56c34d428",
|
|
||||||
"size_bytes": 187734807,
|
|
||||||
"description": "Void Linux 6.12 kernel for Firecracker microVMs"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ type DiscoveredArtifacts struct {
|
||||||
ModulesDir string
|
ModulesDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
// metadataFile is the JSON dropped by scripts/make-void-kernel.sh alongside
|
// metadataFile is the optional JSON a kernel-build script can drop
|
||||||
// its staged output. We read it when present to avoid guessing at filenames.
|
// alongside its staged output to point ReadLocal at specific filenames
|
||||||
|
// without guessing.
|
||||||
type metadataFile struct {
|
type metadataFile struct {
|
||||||
KernelPath string `json:"kernel_path"`
|
KernelPath string `json:"kernel_path"`
|
||||||
InitrdPath string `json:"initrd_path"`
|
InitrdPath string `json:"initrd_path"`
|
||||||
|
|
|
||||||
|
|
@ -1,597 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[customize] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
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.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_size() {
|
|
||||||
local raw="$1"
|
|
||||||
if [[ "$raw" =~ ^([0-9]+)([KMG])?$ ]]; then
|
|
||||||
local num="${BASH_REMATCH[1]}"
|
|
||||||
local unit="${BASH_REMATCH[2]}"
|
|
||||||
case "$unit" in
|
|
||||||
K) echo $((num * 1024)) ;;
|
|
||||||
M|"") echo $((num * 1024 * 1024)) ;;
|
|
||||||
G) echo $((num * 1024 * 1024 * 1024)) ;;
|
|
||||||
esac
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/image-build}"
|
|
||||||
VM_ROOT="$STATE/vms"
|
|
||||||
mkdir -p "$VM_ROOT"
|
|
||||||
|
|
||||||
BR_DEV="br-fc"
|
|
||||||
BR_IP="172.16.0.1"
|
|
||||||
CIDR="24"
|
|
||||||
DNS_SERVER="1.1.1.1"
|
|
||||||
|
|
||||||
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; install/build banger or set BANGER_BIN"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
BANGER_BIN="$(resolve_banger_bin)"
|
|
||||||
NAT_ACTIVE=0
|
|
||||||
FC_BIN="$("$BANGER_BIN" internal firecracker-path)"
|
|
||||||
SSH_KEY="$("$BANGER_BIN" internal ssh-key-path)"
|
|
||||||
VSOCK_AGENT="$("$BANGER_BIN" internal vsock-agent-path)"
|
|
||||||
|
|
||||||
banger_nat() {
|
|
||||||
local action="$1"
|
|
||||||
"$BANGER_BIN" internal nat "$action" --guest-ip "$GUEST_IP" --tap "$TAP_DEV"
|
|
||||||
}
|
|
||||||
|
|
||||||
load_package_preset() {
|
|
||||||
local preset="$1"
|
|
||||||
local -n out="$2"
|
|
||||||
mapfile -t out < <("$BANGER_BIN" internal packages "$preset")
|
|
||||||
(( ${#out[@]} > 0 ))
|
|
||||||
}
|
|
||||||
|
|
||||||
write_rootfs_manifest_metadata() {
|
|
||||||
local rootfs_path="$1"
|
|
||||||
local manifest_hash="$2"
|
|
||||||
printf '%s\n' "$manifest_hash" > "${rootfs_path}.packages.sha256"
|
|
||||||
}
|
|
||||||
|
|
||||||
BASE_ROOTFS=""
|
|
||||||
OUT_ROOTFS=""
|
|
||||||
SIZE_SPEC=""
|
|
||||||
INSTALL_DOCKER=0
|
|
||||||
KERNEL=""
|
|
||||||
INITRD=""
|
|
||||||
MISE_VERSION="v2025.12.0"
|
|
||||||
MISE_INSTALL_PATH="/usr/local/bin/mise"
|
|
||||||
MISE_ACTIVATE_LINE='eval "$(/usr/local/bin/mise activate bash)"'
|
|
||||||
NODE_TOOL="node@22"
|
|
||||||
CLAUDE_CODE_TOOL="npm:@anthropic-ai/claude-code"
|
|
||||||
PI_TOOL="npm:@mariozechner/pi-coding-agent"
|
|
||||||
TMUX_PLUGIN_DIR="/root/.tmux/plugins"
|
|
||||||
TMUX_RESURRECT_DIR="/root/.tmux/resurrect"
|
|
||||||
TMUX_TPM_REPO="https://github.com/tmux-plugins/tpm"
|
|
||||||
TMUX_RESURRECT_REPO="https://github.com/tmux-plugins/tmux-resurrect"
|
|
||||||
TMUX_CONTINUUM_REPO="https://github.com/tmux-plugins/tmux-continuum"
|
|
||||||
TMUX_MANAGED_START="# >>> banger tmux plugins >>>"
|
|
||||||
TMUX_MANAGED_END="# <<< banger tmux plugins <<<"
|
|
||||||
MODULES_DIR=""
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--out)
|
|
||||||
OUT_ROOTFS="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--size)
|
|
||||||
SIZE_SPEC="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--kernel)
|
|
||||||
KERNEL="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--initrd)
|
|
||||||
INITRD="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--docker)
|
|
||||||
INSTALL_DOCKER=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--modules)
|
|
||||||
MODULES_DIR="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
if [[ -z "$BASE_ROOTFS" ]]; then
|
|
||||||
BASE_ROOTFS="$1"
|
|
||||||
shift
|
|
||||||
else
|
|
||||||
log "unknown option: $1"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$BASE_ROOTFS" ]]; then
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f "$BASE_ROOTFS" ]]; then
|
|
||||||
log "base rootfs not found: $BASE_ROOTFS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$OUT_ROOTFS" ]]; then
|
|
||||||
base_dir="$(dirname "$BASE_ROOTFS")"
|
|
||||||
base_name="$(basename "$BASE_ROOTFS")"
|
|
||||||
OUT_ROOTFS="${base_dir}/docker-${base_name}"
|
|
||||||
fi
|
|
||||||
if [[ "$OUT_ROOTFS" == *.ext4 ]]; then
|
|
||||||
WORK_SEED="${OUT_ROOTFS%.ext4}.work-seed.ext4"
|
|
||||||
else
|
|
||||||
WORK_SEED="${OUT_ROOTFS}.work-seed"
|
|
||||||
fi
|
|
||||||
if [[ -z "$KERNEL" ]]; then
|
|
||||||
log "kernel path is required; pass --kernel"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$KERNEL" ]]; then
|
|
||||||
log "kernel not found: $KERNEL"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -n "$INITRD" && ! -f "$INITRD" ]]; then
|
|
||||||
log "initrd not found: $INITRD"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -n "$MODULES_DIR" && ! -d "$MODULES_DIR" ]]; then
|
|
||||||
log "modules dir not found: $MODULES_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -e "$OUT_ROOTFS" ]]; then
|
|
||||||
log "output rootfs already exists: $OUT_ROOTFS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v resize2fs >/dev/null 2>&1; then
|
|
||||||
log "resize2fs required"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! command -v jq >/dev/null 2>&1; then
|
|
||||||
log "jq required"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! command -v sha256sum >/dev/null 2>&1; then
|
|
||||||
log "sha256sum required to record package preset metadata"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -x "$VSOCK_AGENT" ]]; then
|
|
||||||
log "vsock agent not found or not executable: $VSOCK_AGENT"
|
|
||||||
log "run 'make build'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
APT_PACKAGES=()
|
|
||||||
if ! load_package_preset debian APT_PACKAGES; then
|
|
||||||
log "debian package preset is empty"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! PACKAGES_HASH="$(printf '%s\n' "${APT_PACKAGES[@]}" | sha256sum | awk '{print $1}')"; then
|
|
||||||
log "failed to hash package preset"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
printf -v APT_PACKAGES_ESCAPED '%q ' "${APT_PACKAGES[@]}"
|
|
||||||
|
|
||||||
log "copying base rootfs to $OUT_ROOTFS"
|
|
||||||
cp --reflink=auto "$BASE_ROOTFS" "$OUT_ROOTFS"
|
|
||||||
|
|
||||||
if [[ -n "$SIZE_SPEC" ]]; then
|
|
||||||
SIZE_BYTES="$(parse_size "$SIZE_SPEC")"
|
|
||||||
BASE_BYTES="$(stat -c%s "$BASE_ROOTFS")"
|
|
||||||
if [[ -z "$SIZE_BYTES" || "$SIZE_BYTES" -lt "$BASE_BYTES" ]]; then
|
|
||||||
log "size must be >= base image size"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
log "resizing rootfs to $SIZE_SPEC"
|
|
||||||
truncate -s "$SIZE_BYTES" "$OUT_ROOTFS"
|
|
||||||
e2fsck -p -f "$OUT_ROOTFS" >/dev/null
|
|
||||||
resize2fs "$OUT_ROOTFS" >/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
VM_ID="$(head -c 32 /dev/urandom | xxd -p -c 256)"
|
|
||||||
VM_TAG="${VM_ID:0:8}"
|
|
||||||
VM_NAME="customize-${VM_TAG}"
|
|
||||||
VM_DIR="$VM_ROOT/$VM_ID"
|
|
||||||
mkdir -p "$VM_DIR"
|
|
||||||
|
|
||||||
API_SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/banger/fc-$VM_TAG.sock"
|
|
||||||
LOG_FILE="$VM_DIR/firecracker.log"
|
|
||||||
TAP_DEV="tap-fc-$VM_TAG"
|
|
||||||
|
|
||||||
# Allocate guest IP
|
|
||||||
NEXT_IP_FILE="$STATE/next_ip"
|
|
||||||
NEXT_IP="$(cat "$NEXT_IP_FILE" 2>/dev/null || echo 2)"
|
|
||||||
GUEST_IP="172.16.0.$NEXT_IP"
|
|
||||||
echo "$((NEXT_IP + 1))" > "$NEXT_IP_FILE"
|
|
||||||
|
|
||||||
sudo -v
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
sudo kill "${FC_PID:-}" 2>/dev/null || true
|
|
||||||
if [[ "$NAT_ACTIVE" -eq 1 ]]; then
|
|
||||||
banger_nat down >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
sudo ip link del "$TAP_DEV" 2>/dev/null || true
|
|
||||||
rm -f "$API_SOCK"
|
|
||||||
rm -rf "$VM_DIR"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
sudo mkdir -p "$(dirname "$API_SOCK")"
|
|
||||||
sudo chown "$(id -u):$(id -g)" "$(dirname "$API_SOCK")"
|
|
||||||
|
|
||||||
# Host bridge
|
|
||||||
if ! ip link show "$BR_DEV" >/dev/null 2>&1; then
|
|
||||||
log "creating host bridge $BR_DEV ($BR_IP/$CIDR)"
|
|
||||||
sudo ip link add name "$BR_DEV" type bridge
|
|
||||||
sudo ip addr add "${BR_IP}/${CIDR}" dev "$BR_DEV"
|
|
||||||
sudo ip link set "$BR_DEV" up
|
|
||||||
else
|
|
||||||
sudo ip link set "$BR_DEV" up
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "creating tap device $TAP_DEV"
|
|
||||||
TAP_USER="${SUDO_UID:-$(id -u)}"
|
|
||||||
TAP_GROUP="${SUDO_GID:-$(id -g)}"
|
|
||||||
sudo ip tuntap add dev "$TAP_DEV" mode tap user "$TAP_USER" group "$TAP_GROUP"
|
|
||||||
sudo ip link set "$TAP_DEV" master "$BR_DEV"
|
|
||||||
sudo ip link set "$TAP_DEV" up
|
|
||||||
sudo ip link set "$BR_DEV" up
|
|
||||||
|
|
||||||
log "starting firecracker process"
|
|
||||||
rm -f "$API_SOCK"
|
|
||||||
nohup sudo -E "$FC_BIN" --api-sock "$API_SOCK" >"$LOG_FILE" 2>&1 &
|
|
||||||
FC_PID="$!"
|
|
||||||
|
|
||||||
log "waiting for firecracker api socket"
|
|
||||||
for _ in $(seq 1 200); do
|
|
||||||
[[ -S "$API_SOCK" ]] && break
|
|
||||||
sleep 0.02
|
|
||||||
done
|
|
||||||
[[ -S "$API_SOCK" ]] || { log "firecracker api socket not ready"; exit 1; }
|
|
||||||
|
|
||||||
log "configuring machine"
|
|
||||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"vcpu_count": 2,
|
|
||||||
"mem_size_mib": 1024,
|
|
||||||
"smt": false
|
|
||||||
}' >/dev/null
|
|
||||||
|
|
||||||
KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rootfstype=ext4 rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0:${VM_NAME}:eth0:off:${DNS_SERVER} hostname=${VM_NAME} systemd.mask=home.mount systemd.mask=var.mount"
|
|
||||||
|
|
||||||
INITRD_JSON=""
|
|
||||||
if [[ -n "$INITRD" ]]; then
|
|
||||||
INITRD_JSON=", \"initrd_path\": \"$INITRD\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/boot-source \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"kernel_image_path\": \"$KERNEL\",
|
|
||||||
\"boot_args\": \"$KCMD\"${INITRD_JSON}
|
|
||||||
}" >/dev/null
|
|
||||||
|
|
||||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/drives/rootfs \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"drive_id\": \"rootfs\",
|
|
||||||
\"path_on_host\": \"$OUT_ROOTFS\",
|
|
||||||
\"is_root_device\": true,
|
|
||||||
\"is_read_only\": false
|
|
||||||
}" >/dev/null
|
|
||||||
|
|
||||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/network-interfaces/eth0 \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"iface_id\": \"eth0\",
|
|
||||||
\"host_dev_name\": \"$TAP_DEV\"
|
|
||||||
}" >/dev/null
|
|
||||||
|
|
||||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/actions \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{ "action_type": "InstanceStart" }' >/dev/null
|
|
||||||
|
|
||||||
SUDO_CHILD_PID="$(pgrep -n -f "$API_SOCK" || true)"
|
|
||||||
if [[ -n "$SUDO_CHILD_PID" ]]; then
|
|
||||||
FC_PID="$SUDO_CHILD_PID"
|
|
||||||
fi
|
|
||||||
|
|
||||||
VM_CONFIG_JSON="$(sudo -E curl --unix-socket "$API_SOCK" -sS http://localhost/vm/config)"
|
|
||||||
CREATED_AT="$(date -Iseconds)"
|
|
||||||
jq -n \
|
|
||||||
--arg id "$VM_ID" \
|
|
||||||
--arg name "$VM_NAME" \
|
|
||||||
--arg pid "$FC_PID" \
|
|
||||||
--arg created_at "$CREATED_AT" \
|
|
||||||
--arg guest_ip "$GUEST_IP" \
|
|
||||||
--arg tap "$TAP_DEV" \
|
|
||||||
--arg api_sock "$API_SOCK" \
|
|
||||||
--arg log "$LOG_FILE" \
|
|
||||||
--arg rootfs "$OUT_ROOTFS" \
|
|
||||||
--arg kernel "$KERNEL" \
|
|
||||||
--argjson config "$VM_CONFIG_JSON" \
|
|
||||||
'{meta:{id:$id,name:$name,pid:$pid,created_at:$created_at,guest_ip:$guest_ip,tap:$tap,api_sock:$api_sock,log:$log,rootfs:$rootfs,kernel:$kernel},config:$config}' \
|
|
||||||
> "$VM_DIR/vm.json"
|
|
||||||
|
|
||||||
log "enabling NAT for customization"
|
|
||||||
banger_nat up >/dev/null
|
|
||||||
NAT_ACTIVE=1
|
|
||||||
|
|
||||||
log "waiting for SSH"
|
|
||||||
SSH_READY=0
|
|
||||||
for _ in $(seq 1 60); do
|
|
||||||
if ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
||||||
"root@${GUEST_IP}" "true" >/dev/null 2>&1; then
|
|
||||||
SSH_READY=1
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
if [[ "$SSH_READY" -ne 1 ]]; then
|
|
||||||
log "ssh did not become ready on $GUEST_IP"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "configuring guest"
|
|
||||||
log "installing vsock agent"
|
|
||||||
scp -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
||||||
"$VSOCK_AGENT" "root@${GUEST_IP}:/usr/local/bin/banger-vsock-agent" >/dev/null
|
|
||||||
|
|
||||||
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
||||||
"root@${GUEST_IP}" bash -lc "set -e
|
|
||||||
printf 'nameserver %s\n' \"$DNS_SERVER\" > /etc/resolv.conf
|
|
||||||
echo \"$VM_NAME\" > /etc/hostname
|
|
||||||
printf '127.0.0.1 localhost\n127.0.1.1 %s\n' \"$VM_NAME\" > /etc/hosts
|
|
||||||
touch /etc/fstab
|
|
||||||
sed -i '\|^/dev/vdb[[:space:]]\+/home[[:space:]]|d; \|^/dev/vdc[[:space:]]\+/var[[:space:]]|d' /etc/fstab
|
|
||||||
if ! grep -q '^tmpfs /run ' /etc/fstab; then
|
|
||||||
echo 'tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0' >> /etc/fstab
|
|
||||||
fi
|
|
||||||
if ! grep -q '^tmpfs /tmp ' /etc/fstab; then
|
|
||||||
echo 'tmpfs /tmp tmpfs defaults,nodev,nosuid,mode=1777 0 0' >> /etc/fstab
|
|
||||||
fi
|
|
||||||
apt-get update
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -y upgrade
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -y install ${APT_PACKAGES_ESCAPED}
|
|
||||||
curl -fsSL https://mise.run | MISE_INSTALL_PATH="$MISE_INSTALL_PATH" MISE_VERSION="$MISE_VERSION" sh
|
|
||||||
"$MISE_INSTALL_PATH" use -g "$NODE_TOOL"
|
|
||||||
"$MISE_INSTALL_PATH" use -g github:anomalyco/opencode
|
|
||||||
"$MISE_INSTALL_PATH" use -g "$CLAUDE_CODE_TOOL"
|
|
||||||
"$MISE_INSTALL_PATH" use -g "$PI_TOOL"
|
|
||||||
"$MISE_INSTALL_PATH" reshim
|
|
||||||
if [[ ! -e /root/.local/share/mise/shims/node ]]; then
|
|
||||||
echo 'node shim not found after mise install' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -e /root/.local/share/mise/shims/npm ]]; then
|
|
||||||
echo 'npm shim not found after mise install' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -e /root/.local/share/mise/shims/opencode ]]; then
|
|
||||||
echo 'opencode shim not found after mise install' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -e /root/.local/share/mise/shims/claude ]]; then
|
|
||||||
echo 'claude shim not found after mise install' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -e /root/.local/share/mise/shims/pi ]]; then
|
|
||||||
echo 'pi shim not found after mise install' >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
ln -snf /root/.local/share/mise/shims/node /usr/local/bin/node
|
|
||||||
ln -snf /root/.local/share/mise/shims/npm /usr/local/bin/npm
|
|
||||||
ln -snf /root/.local/share/mise/shims/opencode /usr/local/bin/opencode
|
|
||||||
ln -snf /root/.local/share/mise/shims/claude /usr/local/bin/claude
|
|
||||||
ln -snf /root/.local/share/mise/shims/pi /usr/local/bin/pi
|
|
||||||
mkdir -p /etc/profile.d
|
|
||||||
cat > /etc/profile.d/mise.sh <<'MISEPROFILE'
|
|
||||||
if [ -n \"\${BASH_VERSION:-}\" ] && [ -x \"$MISE_INSTALL_PATH\" ]; then
|
|
||||||
eval \"\$($MISE_INSTALL_PATH activate bash)\"
|
|
||||||
fi
|
|
||||||
MISEPROFILE
|
|
||||||
chmod 0644 /etc/profile.d/mise.sh
|
|
||||||
touch /etc/bash.bashrc
|
|
||||||
if ! grep -Fqx '$MISE_ACTIVATE_LINE' /etc/bash.bashrc; then
|
|
||||||
printf '\n%s\n' '$MISE_ACTIVATE_LINE' >> /etc/bash.bashrc
|
|
||||||
fi
|
|
||||||
if [[ \"$INSTALL_DOCKER\" == \"1\" ]]; then
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -y remove containerd || true
|
|
||||||
if ! DEBIAN_FRONTEND=noninteractive apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then
|
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get -y install docker.io
|
|
||||||
fi
|
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
|
||||||
systemctl enable --now docker || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
rm -f /root/get-docker /root/get-docker.sh /tmp/get-docker /tmp/get-docker.sh
|
|
||||||
chmod 0755 /usr/local/bin/banger-vsock-agent
|
|
||||||
mkdir -p /etc/modules-load.d /etc/systemd/system
|
|
||||||
cat > /etc/systemd/system/banger-opencode.service <<'EOF'
|
|
||||||
[Unit]
|
|
||||||
Description=Banger opencode server
|
|
||||||
After=network.target
|
|
||||||
RequiresMountsFor=/root
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
Environment=HOME=/root
|
|
||||||
WorkingDirectory=/root
|
|
||||||
ExecStart=/usr/local/bin/opencode serve --hostname 0.0.0.0 --port 4096
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=1
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
chmod 0644 /etc/systemd/system/banger-opencode.service
|
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
|
||||||
systemctl daemon-reload || true
|
|
||||||
systemctl enable --now banger-opencode.service || true
|
|
||||||
fi
|
|
||||||
cat > /etc/modules-load.d/banger-vsock.conf <<'EOF'
|
|
||||||
vsock
|
|
||||||
vmw_vsock_virtio_transport
|
|
||||||
EOF
|
|
||||||
chmod 0644 /etc/modules-load.d/banger-vsock.conf
|
|
||||||
cat > /etc/systemd/system/banger-vsock-agent.service <<'EOF'
|
|
||||||
[Unit]
|
|
||||||
Description=Banger vsock agent
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/usr/local/bin/banger-vsock-agent
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=1
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
chmod 0644 /etc/systemd/system/banger-vsock-agent.service
|
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
|
||||||
systemctl daemon-reload || true
|
|
||||||
systemctl enable --now banger-vsock-agent.service || true
|
|
||||||
fi
|
|
||||||
git config --system init.defaultBranch main
|
|
||||||
"
|
|
||||||
|
|
||||||
log "configuring tmux resurrect"
|
|
||||||
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
||||||
"root@${GUEST_IP}" bash -se <<EOF
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
install_tmux_plugin() {
|
|
||||||
local dir="\$1"
|
|
||||||
local repo="\$2"
|
|
||||||
|
|
||||||
if [[ -d "\$dir/.git" ]]; then
|
|
||||||
git -C "\$dir" fetch --depth 1 origin
|
|
||||||
git -C "\$dir" reset --hard FETCH_HEAD
|
|
||||||
else
|
|
||||||
rm -rf "\$dir"
|
|
||||||
git clone --depth 1 "\$repo" "\$dir"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdir -p "$TMUX_PLUGIN_DIR" "$TMUX_RESURRECT_DIR"
|
|
||||||
install_tmux_plugin "$TMUX_PLUGIN_DIR/tpm" "$TMUX_TPM_REPO"
|
|
||||||
install_tmux_plugin "$TMUX_PLUGIN_DIR/tmux-resurrect" "$TMUX_RESURRECT_REPO"
|
|
||||||
install_tmux_plugin "$TMUX_PLUGIN_DIR/tmux-continuum" "$TMUX_CONTINUUM_REPO"
|
|
||||||
|
|
||||||
TMUX_CONF="/root/.tmux.conf"
|
|
||||||
tmp_tmux_conf="\$(mktemp)"
|
|
||||||
if [[ -f "\$TMUX_CONF" ]]; then
|
|
||||||
awk -v begin="$TMUX_MANAGED_START" -v end="$TMUX_MANAGED_END" '
|
|
||||||
\$0 == begin { skip = 1; next }
|
|
||||||
\$0 == end { skip = 0; next }
|
|
||||||
!skip { print }
|
|
||||||
' "\$TMUX_CONF" > "\$tmp_tmux_conf"
|
|
||||||
else
|
|
||||||
: > "\$tmp_tmux_conf"
|
|
||||||
fi
|
|
||||||
if [[ -s "\$tmp_tmux_conf" ]]; then
|
|
||||||
printf '\n' >> "\$tmp_tmux_conf"
|
|
||||||
fi
|
|
||||||
cat >> "\$tmp_tmux_conf" <<'TMUXCONF'
|
|
||||||
$TMUX_MANAGED_START
|
|
||||||
set -g @plugin 'tmux-plugins/tpm'
|
|
||||||
set -g @plugin 'tmux-plugins/tmux-resurrect'
|
|
||||||
set -g @plugin 'tmux-plugins/tmux-continuum'
|
|
||||||
set -g @continuum-save-interval '15'
|
|
||||||
set -g @continuum-restore 'off'
|
|
||||||
set -g @resurrect-dir '/root/.tmux/resurrect'
|
|
||||||
run '~/.tmux/plugins/tpm/tpm'
|
|
||||||
$TMUX_MANAGED_END
|
|
||||||
TMUXCONF
|
|
||||||
mv "\$tmp_tmux_conf" "\$TMUX_CONF"
|
|
||||||
chmod 0644 "\$TMUX_CONF"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [[ -n "$MODULES_DIR" ]]; then
|
|
||||||
MODULES_BASE="$(basename "$MODULES_DIR")"
|
|
||||||
log "copying kernel modules ($MODULES_BASE) into guest"
|
|
||||||
tar -C "$(dirname "$MODULES_DIR")" -cf - "$MODULES_BASE" | \
|
|
||||||
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
||||||
"root@${GUEST_IP}" bash -lc "set -e
|
|
||||||
mkdir -p /lib/modules
|
|
||||||
tar -C /lib/modules -xf -
|
|
||||||
depmod -a \"$MODULES_BASE\"
|
|
||||||
mkdir -p /etc/modules-load.d
|
|
||||||
printf 'nf_tables\nnft_chain_nat\nveth\nbr_netfilter\noverlay\n' > /etc/modules-load.d/docker-netfilter.conf
|
|
||||||
mkdir -p /etc/sysctl.d
|
|
||||||
cat > /etc/sysctl.d/99-docker.conf <<'SYSCTL'
|
|
||||||
net.bridge.bridge-nf-call-iptables = 1
|
|
||||||
net.bridge.bridge-nf-call-ip6tables = 1
|
|
||||||
net.ipv4.ip_forward = 1
|
|
||||||
SYSCTL
|
|
||||||
sysctl --system >/dev/null 2>&1 || true
|
|
||||||
sync
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "shutting down guest"
|
|
||||||
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
||||||
"root@${GUEST_IP}" bash -lc "sync" || true
|
|
||||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/actions \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{ "action_type": "SendCtrlAltDel" }' >/dev/null || true
|
|
||||||
for _ in $(seq 1 200); do
|
|
||||||
if ! ps -p "$FC_PID" >/dev/null 2>&1; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 0.05
|
|
||||||
done
|
|
||||||
write_rootfs_manifest_metadata "$OUT_ROOTFS" "$PACKAGES_HASH"
|
|
||||||
log "building work seed $WORK_SEED"
|
|
||||||
"$BANGER_BIN" internal work-seed --rootfs "$OUT_ROOTFS" --out "$WORK_SEED"
|
|
||||||
log "done"
|
|
||||||
|
|
@ -1,306 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[interactive] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: ./scripts/interactive.sh <base-rootfs> --kernel <path> [--initrd <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
|
|
||||||
are applied.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_size() {
|
|
||||||
local raw="$1"
|
|
||||||
if [[ "$raw" =~ ^([0-9]+)([KMG])?$ ]]; then
|
|
||||||
local num="${BASH_REMATCH[1]}"
|
|
||||||
local unit="${BASH_REMATCH[2]}"
|
|
||||||
case "$unit" in
|
|
||||||
K) echo $((num * 1024)) ;;
|
|
||||||
M|"") echo $((num * 1024 * 1024)) ;;
|
|
||||||
G) echo $((num * 1024 * 1024 * 1024)) ;;
|
|
||||||
esac
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/interactive}"
|
|
||||||
VM_ROOT="$STATE/vms"
|
|
||||||
mkdir -p "$VM_ROOT"
|
|
||||||
|
|
||||||
BR_DEV="br-fc"
|
|
||||||
BR_IP="172.16.0.1"
|
|
||||||
CIDR="24"
|
|
||||||
DNS_SERVER="1.1.1.1"
|
|
||||||
|
|
||||||
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; install/build banger or set BANGER_BIN"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
BANGER_BIN="$(resolve_banger_bin)"
|
|
||||||
NAT_ACTIVE=0
|
|
||||||
FC_BIN="$("$BANGER_BIN" internal firecracker-path)"
|
|
||||||
SSH_KEY="$("$BANGER_BIN" internal ssh-key-path)"
|
|
||||||
KERNEL=""
|
|
||||||
INITRD=""
|
|
||||||
|
|
||||||
banger_nat() {
|
|
||||||
local action="$1"
|
|
||||||
"$BANGER_BIN" internal nat "$action" --guest-ip "$GUEST_IP" --tap "$TAP_DEV"
|
|
||||||
}
|
|
||||||
|
|
||||||
BASE_ROOTFS=""
|
|
||||||
OUT_ROOTFS=""
|
|
||||||
SIZE_SPEC=""
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--out)
|
|
||||||
OUT_ROOTFS="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--size)
|
|
||||||
SIZE_SPEC="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--kernel)
|
|
||||||
KERNEL="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--initrd)
|
|
||||||
INITRD="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
if [[ -z "$BASE_ROOTFS" ]]; then
|
|
||||||
BASE_ROOTFS="$1"
|
|
||||||
shift
|
|
||||||
else
|
|
||||||
log "unknown option: $1"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$BASE_ROOTFS" ]]; then
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$BASE_ROOTFS" ]]; then
|
|
||||||
log "base rootfs not found: $BASE_ROOTFS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -z "$KERNEL" ]]; then
|
|
||||||
log "kernel path is required; pass --kernel"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$KERNEL" ]]; then
|
|
||||||
log "kernel not found: $KERNEL"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -n "$INITRD" && ! -f "$INITRD" ]]; then
|
|
||||||
log "initrd not found: $INITRD"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$OUT_ROOTFS" ]]; then
|
|
||||||
base_dir="$(dirname "$BASE_ROOTFS")"
|
|
||||||
base_name="$(basename "$BASE_ROOTFS")"
|
|
||||||
OUT_ROOTFS="${base_dir}/rw-${base_name}"
|
|
||||||
fi
|
|
||||||
if [[ -e "$OUT_ROOTFS" ]]; then
|
|
||||||
log "output rootfs already exists: $OUT_ROOTFS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "copying base rootfs to $OUT_ROOTFS"
|
|
||||||
cp --reflink=auto "$BASE_ROOTFS" "$OUT_ROOTFS"
|
|
||||||
|
|
||||||
if [[ -n "$SIZE_SPEC" ]]; then
|
|
||||||
SIZE_BYTES="$(parse_size "$SIZE_SPEC")"
|
|
||||||
BASE_BYTES="$(stat -c%s "$BASE_ROOTFS")"
|
|
||||||
if [[ -z "$SIZE_BYTES" || "$SIZE_BYTES" -lt "$BASE_BYTES" ]]; then
|
|
||||||
log "size must be >= base image size"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
log "resizing rootfs to $SIZE_SPEC"
|
|
||||||
truncate -s "$SIZE_BYTES" "$OUT_ROOTFS"
|
|
||||||
e2fsck -p -f "$OUT_ROOTFS" >/dev/null
|
|
||||||
resize2fs "$OUT_ROOTFS" >/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
VM_ID="$(head -c 32 /dev/urandom | xxd -p -c 256)"
|
|
||||||
VM_TAG="${VM_ID:0:8}"
|
|
||||||
VM_NAME="interactive-${VM_TAG}"
|
|
||||||
VM_DIR="$VM_ROOT/$VM_ID"
|
|
||||||
mkdir -p "$VM_DIR"
|
|
||||||
|
|
||||||
API_SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/banger/fc-$VM_TAG.sock"
|
|
||||||
LOG_FILE="$VM_DIR/firecracker.log"
|
|
||||||
TAP_DEV="tap-fc-$VM_TAG"
|
|
||||||
|
|
||||||
# Allocate guest IP
|
|
||||||
NEXT_IP_FILE="$STATE/next_ip"
|
|
||||||
NEXT_IP="$(cat "$NEXT_IP_FILE" 2>/dev/null || echo 2)"
|
|
||||||
GUEST_IP="172.16.0.$NEXT_IP"
|
|
||||||
echo "$((NEXT_IP + 1))" > "$NEXT_IP_FILE"
|
|
||||||
|
|
||||||
sudo -v
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
sudo kill "${FC_PID:-}" 2>/dev/null || true
|
|
||||||
if [[ "$NAT_ACTIVE" -eq 1 ]]; then
|
|
||||||
banger_nat down >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
sudo ip link del "$TAP_DEV" 2>/dev/null || true
|
|
||||||
rm -f "$API_SOCK"
|
|
||||||
rm -rf "$VM_DIR"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
sudo mkdir -p "$(dirname "$API_SOCK")"
|
|
||||||
sudo chown "$(id -u):$(id -g)" "$(dirname "$API_SOCK")"
|
|
||||||
|
|
||||||
# Host bridge
|
|
||||||
if ! ip link show "$BR_DEV" >/dev/null 2>&1; then
|
|
||||||
log "creating host bridge $BR_DEV ($BR_IP/$CIDR)"
|
|
||||||
sudo ip link add name "$BR_DEV" type bridge
|
|
||||||
sudo ip addr add "${BR_IP}/${CIDR}" dev "$BR_DEV"
|
|
||||||
sudo ip link set "$BR_DEV" up
|
|
||||||
else
|
|
||||||
sudo ip link set "$BR_DEV" up
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "creating tap device $TAP_DEV"
|
|
||||||
TAP_USER="${SUDO_UID:-$(id -u)}"
|
|
||||||
TAP_GROUP="${SUDO_GID:-$(id -g)}"
|
|
||||||
sudo ip tuntap add dev "$TAP_DEV" mode tap user "$TAP_USER" group "$TAP_GROUP"
|
|
||||||
sudo ip link set "$TAP_DEV" master "$BR_DEV"
|
|
||||||
sudo ip link set "$TAP_DEV" up
|
|
||||||
sudo ip link set "$BR_DEV" up
|
|
||||||
|
|
||||||
log "starting firecracker process"
|
|
||||||
rm -f "$API_SOCK"
|
|
||||||
nohup sudo -E "$FC_BIN" --api-sock "$API_SOCK" >"$LOG_FILE" 2>&1 &
|
|
||||||
FC_PID="$!"
|
|
||||||
|
|
||||||
log "waiting for firecracker api socket"
|
|
||||||
for _ in $(seq 1 200); do
|
|
||||||
[[ -S "$API_SOCK" ]] && break
|
|
||||||
sleep 0.02
|
|
||||||
done
|
|
||||||
[[ -S "$API_SOCK" ]] || { log "firecracker api socket not ready"; exit 1; }
|
|
||||||
|
|
||||||
log "configuring machine"
|
|
||||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"vcpu_count": 2,
|
|
||||||
"mem_size_mib": 1024,
|
|
||||||
"smt": false
|
|
||||||
}' >/dev/null
|
|
||||||
|
|
||||||
KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rootfstype=ext4 rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0:${VM_NAME}:eth0:off:${DNS_SERVER} hostname=${VM_NAME} systemd.mask=home.mount systemd.mask=var.mount"
|
|
||||||
|
|
||||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/boot-source \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"kernel_image_path\": \"$KERNEL\",
|
|
||||||
\"boot_args\": \"$KCMD\",
|
|
||||||
\"initrd_path\": \"$INITRD\"
|
|
||||||
}" >/dev/null
|
|
||||||
|
|
||||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/drives/rootfs \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"drive_id\": \"rootfs\",
|
|
||||||
\"path_on_host\": \"$OUT_ROOTFS\",
|
|
||||||
\"is_root_device\": true,
|
|
||||||
\"is_read_only\": false
|
|
||||||
}" >/dev/null
|
|
||||||
|
|
||||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/network-interfaces/eth0 \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"iface_id\": \"eth0\",
|
|
||||||
\"host_dev_name\": \"$TAP_DEV\"
|
|
||||||
}" >/dev/null
|
|
||||||
|
|
||||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/actions \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{ "action_type": "InstanceStart" }' >/dev/null
|
|
||||||
|
|
||||||
SUDO_CHILD_PID="$(pgrep -n -f "$API_SOCK" || true)"
|
|
||||||
if [[ -n "$SUDO_CHILD_PID" ]]; then
|
|
||||||
FC_PID="$SUDO_CHILD_PID"
|
|
||||||
fi
|
|
||||||
|
|
||||||
VM_CONFIG_JSON="$(sudo -E curl --unix-socket "$API_SOCK" -sS http://localhost/vm/config)"
|
|
||||||
CREATED_AT="$(date -Iseconds)"
|
|
||||||
jq -n \
|
|
||||||
--arg id "$VM_ID" \
|
|
||||||
--arg name "$VM_NAME" \
|
|
||||||
--arg pid "$FC_PID" \
|
|
||||||
--arg created_at "$CREATED_AT" \
|
|
||||||
--arg guest_ip "$GUEST_IP" \
|
|
||||||
--arg tap "$TAP_DEV" \
|
|
||||||
--arg api_sock "$API_SOCK" \
|
|
||||||
--arg log "$LOG_FILE" \
|
|
||||||
--arg rootfs "$OUT_ROOTFS" \
|
|
||||||
--arg kernel "$KERNEL" \
|
|
||||||
--argjson config "$VM_CONFIG_JSON" \
|
|
||||||
'{meta:{id:$id,name:$name,pid:$pid,created_at:$created_at,guest_ip:$guest_ip,tap:$tap,api_sock:$api_sock,log:$log,rootfs:$rootfs,kernel:$kernel},config:$config}' \
|
|
||||||
> "$VM_DIR/vm.json"
|
|
||||||
|
|
||||||
log "enabling NAT for interactive session"
|
|
||||||
banger_nat up >/dev/null
|
|
||||||
NAT_ACTIVE=1
|
|
||||||
|
|
||||||
log "waiting for SSH"
|
|
||||||
log "guest ip: $GUEST_IP"
|
|
||||||
log "ssh: ssh -i \"$SSH_KEY\" root@${GUEST_IP}"
|
|
||||||
for _ in $(seq 1 60); do
|
|
||||||
if ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
|
||||||
"root@${GUEST_IP}" "true" >/dev/null 2>&1; then
|
|
||||||
log "ssh ready"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
log "output rootfs: $OUT_ROOTFS"
|
|
||||||
log "press Ctrl+C to stop and clean up"
|
|
||||||
|
|
||||||
while kill -0 "$FC_PID" >/dev/null 2>&1; do
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
@ -1,363 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[make-alpine-kernel] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: ./scripts/make-alpine-kernel.sh [--out-dir <path>] [--release <x.y.z>] [--mirror <url>] [--arch <arch>] [--print-register-flags]
|
|
||||||
|
|
||||||
Download and stage an Alpine Linux virt kernel under ./build/manual/alpine-kernel
|
|
||||||
for the experimental Alpine guest flow.
|
|
||||||
|
|
||||||
Defaults:
|
|
||||||
--out-dir ./build/manual/alpine-kernel
|
|
||||||
--release 3.23.3
|
|
||||||
--mirror https://dl-cdn.alpinelinux.org/alpine
|
|
||||||
--arch x86_64
|
|
||||||
|
|
||||||
The staged output contains:
|
|
||||||
boot/vmlinuz-<version> Alpine virt kernel image
|
|
||||||
boot/initramfs-<version>.img Matching Alpine initramfs
|
|
||||||
boot/config-<version> Alpine kernel config when present
|
|
||||||
lib/modules/<version>/ Matching kernel modules from modloop-virt
|
|
||||||
|
|
||||||
If --print-register-flags is passed, the script does not download anything. It
|
|
||||||
prints the banger image register flags for an existing staged Alpine kernel.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
require_command() {
|
|
||||||
local name="$1"
|
|
||||||
command -v "$name" >/dev/null 2>&1 || {
|
|
||||||
log "required command not found: $name"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
check_elf() {
|
|
||||||
local path="$1"
|
|
||||||
readelf -h "$path" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
find_latest_matching() {
|
|
||||||
local dir="$1"
|
|
||||||
local pattern="$2"
|
|
||||||
if [[ ! -d "$dir" ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
find "$dir" -maxdepth 1 -type f -name "$pattern" | sort | tail -n 1
|
|
||||||
}
|
|
||||||
|
|
||||||
find_latest_module_dir() {
|
|
||||||
local root="$1"
|
|
||||||
local dir=""
|
|
||||||
if [[ ! -d "$root" ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
while IFS= read -r dir; do
|
|
||||||
if [[ -d "$dir/kernel" || -f "$dir/modules.dep" || -f "$dir/modules.dep.bin" ]]; then
|
|
||||||
printf '%s\n' "$dir"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done < <(find "$root" -mindepth 1 -maxdepth 1 -type d | sort)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
find_tar_entry() {
|
|
||||||
local archive="$1"
|
|
||||||
local needle="$2"
|
|
||||||
local entry=""
|
|
||||||
|
|
||||||
while IFS= read -r entry; do
|
|
||||||
case "$entry" in
|
|
||||||
"$needle"|*/"$needle")
|
|
||||||
printf '%s\n' "$entry"
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done < <(tar -tf "$archive")
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
find_tar_config_entry() {
|
|
||||||
local archive="$1"
|
|
||||||
local entry=""
|
|
||||||
|
|
||||||
while IFS= read -r entry; do
|
|
||||||
case "$entry" in
|
|
||||||
config-*-virt|*/config-*-virt)
|
|
||||||
printf '%s\n' "$entry"
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done < <(tar -tf "$archive")
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_release_branch() {
|
|
||||||
local release="$1"
|
|
||||||
printf 'v%s\n' "${release%.*}"
|
|
||||||
}
|
|
||||||
|
|
||||||
extract_vmlinux() {
|
|
||||||
local image="$1"
|
|
||||||
local out="$2"
|
|
||||||
local tmp="$TMP_DIR/vmlinux.extract"
|
|
||||||
|
|
||||||
if check_elf "$image"; then
|
|
||||||
install -m 0644 "$image" "$out"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
try_decompress() {
|
|
||||||
local header="$1"
|
|
||||||
local marker="$2"
|
|
||||||
local command="$3"
|
|
||||||
local pos=""
|
|
||||||
|
|
||||||
while IFS= read -r pos; do
|
|
||||||
[[ -n "$pos" ]] || continue
|
|
||||||
pos="${pos%%:*}"
|
|
||||||
tail -c+"$pos" "$image" | eval "$command" >"$tmp" 2>/dev/null || true
|
|
||||||
if check_elf "$tmp"; then
|
|
||||||
install -m 0644 "$tmp" "$out"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done < <(tr "$header\n$marker" "\n$marker=" < "$image" | grep -abo "^$marker" || true)
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
try_decompress '\037\213\010' "xy" "gunzip" && return 0
|
|
||||||
try_decompress '\3757zXZ\000' "abcde" "unxz" && return 0
|
|
||||||
try_decompress "BZh" "xy" "bunzip2" && return 0
|
|
||||||
try_decompress '\135\000\000\000' "xxx" "unlzma" && return 0
|
|
||||||
try_decompress '\002!L\030' "xxx" "lz4 -d" && return 0
|
|
||||||
try_decompress '(\265/\375' "xxx" "unzstd" && return 0
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
print_register_flags() {
|
|
||||||
local kernel=""
|
|
||||||
local initrd=""
|
|
||||||
local modules=""
|
|
||||||
|
|
||||||
kernel="$(find_latest_matching "$OUT_DIR/boot" 'vmlinux-*' || true)"
|
|
||||||
if [[ -z "$kernel" ]]; then
|
|
||||||
kernel="$(find_latest_matching "$OUT_DIR/boot" 'vmlinuz-*' || true)"
|
|
||||||
fi
|
|
||||||
initrd="$(find_latest_matching "$OUT_DIR/boot" 'initramfs-*' || true)"
|
|
||||||
modules="$(find_latest_module_dir "$OUT_DIR/lib/modules" || true)"
|
|
||||||
|
|
||||||
if [[ -z "$kernel" || -z "$modules" ]]; then
|
|
||||||
log "staged Alpine kernel not found under $OUT_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf -- '--kernel %q ' "$kernel"
|
|
||||||
if [[ -n "$initrd" ]]; then
|
|
||||||
printf -- '--initrd %q ' "$initrd"
|
|
||||||
fi
|
|
||||||
printf -- '--modules %q\n' "$modules"
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [[ "${MODLOOP_MOUNTED:-0}" == "1" ]] && [[ -n "${MODLOOP_MOUNT:-}" ]]; then
|
|
||||||
sudo umount "$MODLOOP_MOUNT" || true
|
|
||||||
fi
|
|
||||||
if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR:-}" ]]; then
|
|
||||||
rm -rf "$TMP_DIR"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
MANUAL_DIR="${BANGER_MANUAL_DIR:-$REPO_ROOT/build/manual}"
|
|
||||||
OUT_DIR="$MANUAL_DIR/alpine-kernel"
|
|
||||||
RELEASE="${ALPINE_RELEASE:-3.23.3}"
|
|
||||||
MIRROR="https://dl-cdn.alpinelinux.org/alpine"
|
|
||||||
ARCH="x86_64"
|
|
||||||
PRINT_REGISTER_FLAGS=0
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--out-dir)
|
|
||||||
OUT_DIR="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--release)
|
|
||||||
RELEASE="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--mirror)
|
|
||||||
MIRROR="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--arch)
|
|
||||||
ARCH="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--print-register-flags)
|
|
||||||
PRINT_REGISTER_FLAGS=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log "unknown option: $1"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$PRINT_REGISTER_FLAGS" == "1" ]]; then
|
|
||||||
print_register_flags
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$ARCH" != "x86_64" ]]; then
|
|
||||||
log "unsupported arch: $ARCH"
|
|
||||||
log "this experimental builder currently supports only x86_64"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -d "$OUT_DIR" ]]; then
|
|
||||||
log "output directory already exists: $OUT_DIR"
|
|
||||||
log "remove it first if you want to re-stage a different Alpine kernel"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
require_command curl
|
|
||||||
require_command tar
|
|
||||||
require_command sha256sum
|
|
||||||
require_command install
|
|
||||||
require_command find
|
|
||||||
require_command cp
|
|
||||||
require_command readelf
|
|
||||||
require_command file
|
|
||||||
require_command tail
|
|
||||||
require_command grep
|
|
||||||
require_command cut
|
|
||||||
require_command gzip
|
|
||||||
require_command xz
|
|
||||||
require_command bzip2
|
|
||||||
|
|
||||||
if command -v unsquashfs >/dev/null 2>&1; then
|
|
||||||
USE_UNSQUASHFS=1
|
|
||||||
else
|
|
||||||
USE_UNSQUASHFS=0
|
|
||||||
require_command sudo
|
|
||||||
require_command mount
|
|
||||||
require_command umount
|
|
||||||
fi
|
|
||||||
|
|
||||||
TMP_DIR="$(mktemp -d -t banger-alpine-kernel-XXXXXX)"
|
|
||||||
EXTRACT_DIR="$TMP_DIR/extract"
|
|
||||||
MODLOOP_DIR="$TMP_DIR/modloop"
|
|
||||||
MODLOOP_MOUNT="$TMP_DIR/modloop.mount"
|
|
||||||
ARCHIVE="$TMP_DIR/alpine-netboot.tar.gz"
|
|
||||||
MODLOOP_MOUNTED=0
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
mkdir -p "$EXTRACT_DIR" "$MODLOOP_DIR" "$MODLOOP_MOUNT"
|
|
||||||
|
|
||||||
BRANCH="$(resolve_release_branch "$RELEASE")"
|
|
||||||
RELEASE_DIR="$MIRROR/$BRANCH/releases/$ARCH"
|
|
||||||
ARCHIVE_URL="$RELEASE_DIR/alpine-netboot-$RELEASE-$ARCH.tar.gz"
|
|
||||||
SHA256_URL="$ARCHIVE_URL.sha256"
|
|
||||||
|
|
||||||
log "downloading Alpine netboot bundle from $ARCHIVE_URL"
|
|
||||||
curl -fsSL "$ARCHIVE_URL" -o "$ARCHIVE"
|
|
||||||
expected_sha="$(curl -fsSL "$SHA256_URL" | awk '{print $1}')"
|
|
||||||
actual_sha="$(sha256sum "$ARCHIVE" | awk '{print $1}')"
|
|
||||||
if [[ -z "$expected_sha" ]]; then
|
|
||||||
log "failed to read SHA256 from $SHA256_URL"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ "$expected_sha" != "$actual_sha" ]]; then
|
|
||||||
log "sha256 mismatch for $ARCHIVE_URL"
|
|
||||||
log "expected: $expected_sha"
|
|
||||||
log "actual: $actual_sha"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VMLINUX_ENTRY="$(find_tar_entry "$ARCHIVE" 'vmlinuz-virt' || true)"
|
|
||||||
INITRD_ENTRY="$(find_tar_entry "$ARCHIVE" 'initramfs-virt' || true)"
|
|
||||||
MODLOOP_ENTRY="$(find_tar_entry "$ARCHIVE" 'modloop-virt' || true)"
|
|
||||||
CONFIG_ENTRY="$(find_tar_config_entry "$ARCHIVE" || true)"
|
|
||||||
|
|
||||||
if [[ -z "$VMLINUX_ENTRY" || -z "$INITRD_ENTRY" || -z "$MODLOOP_ENTRY" ]]; then
|
|
||||||
log "Alpine netboot bundle is missing expected virt boot artifacts"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "extracting Alpine virt boot artifacts"
|
|
||||||
tar_args=("$VMLINUX_ENTRY" "$INITRD_ENTRY" "$MODLOOP_ENTRY")
|
|
||||||
if [[ -n "$CONFIG_ENTRY" ]]; then
|
|
||||||
tar_args+=("$CONFIG_ENTRY")
|
|
||||||
fi
|
|
||||||
tar -xf "$ARCHIVE" -C "$EXTRACT_DIR" "${tar_args[@]}"
|
|
||||||
|
|
||||||
VMLINUX_SRC="$EXTRACT_DIR/$VMLINUX_ENTRY"
|
|
||||||
INITRD_SRC="$EXTRACT_DIR/$INITRD_ENTRY"
|
|
||||||
MODLOOP_SRC="$EXTRACT_DIR/$MODLOOP_ENTRY"
|
|
||||||
CONFIG_SRC=""
|
|
||||||
if [[ -n "$CONFIG_ENTRY" ]]; then
|
|
||||||
CONFIG_SRC="$EXTRACT_DIR/$CONFIG_ENTRY"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$USE_UNSQUASHFS" == "1" ]]; then
|
|
||||||
log "extracting kernel modules with unsquashfs"
|
|
||||||
unsquashfs -f -d "$MODLOOP_DIR" "$MODLOOP_SRC" >/dev/null
|
|
||||||
else
|
|
||||||
log "extracting kernel modules with a read-only loop mount"
|
|
||||||
sudo mount -o loop,ro "$MODLOOP_SRC" "$MODLOOP_MOUNT"
|
|
||||||
MODLOOP_MOUNTED=1
|
|
||||||
cp -a "$MODLOOP_MOUNT/." "$MODLOOP_DIR/"
|
|
||||||
sudo umount "$MODLOOP_MOUNT"
|
|
||||||
MODLOOP_MOUNTED=0
|
|
||||||
fi
|
|
||||||
|
|
||||||
MODULES_ROOT=""
|
|
||||||
if [[ -d "$MODLOOP_DIR/modules" ]]; then
|
|
||||||
MODULES_ROOT="$MODLOOP_DIR/modules"
|
|
||||||
elif [[ -d "$MODLOOP_DIR/lib/modules" ]]; then
|
|
||||||
MODULES_ROOT="$MODLOOP_DIR/lib/modules"
|
|
||||||
fi
|
|
||||||
if [[ -z "$MODULES_ROOT" ]]; then
|
|
||||||
log "extracted modloop is missing a modules directory"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
MODULES_SRC="$(find_latest_module_dir "$MODULES_ROOT" || true)"
|
|
||||||
if [[ -z "$MODULES_SRC" ]]; then
|
|
||||||
log "failed to locate a kernel modules tree inside modloop-virt"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
KERNEL_VERSION="$(basename "$MODULES_SRC")"
|
|
||||||
mkdir -p "$OUT_DIR/boot" "$OUT_DIR/lib/modules"
|
|
||||||
install -m 0644 "$VMLINUX_SRC" "$OUT_DIR/boot/vmlinuz-$KERNEL_VERSION"
|
|
||||||
install -m 0644 "$INITRD_SRC" "$OUT_DIR/boot/initramfs-$KERNEL_VERSION.img"
|
|
||||||
if [[ -n "$CONFIG_SRC" && -f "$CONFIG_SRC" ]]; then
|
|
||||||
install -m 0644 "$CONFIG_SRC" "$OUT_DIR/boot/config-$KERNEL_VERSION"
|
|
||||||
fi
|
|
||||||
cp -a "$MODULES_SRC" "$OUT_DIR/lib/modules/"
|
|
||||||
|
|
||||||
log "extracting Firecracker kernel from vmlinuz-$KERNEL_VERSION"
|
|
||||||
if ! extract_vmlinux "$VMLINUX_SRC" "$OUT_DIR/boot/vmlinux-$KERNEL_VERSION"; then
|
|
||||||
log "failed to extract an uncompressed vmlinux from $VMLINUX_SRC"
|
|
||||||
log "raw kernel image type: $(file -b "$VMLINUX_SRC")"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "staged Alpine kernel artifacts in $OUT_DIR"
|
|
||||||
log "kernel version: $KERNEL_VERSION"
|
|
||||||
|
|
@ -1,722 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[make-rootfs-alpine] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: ./scripts/make-rootfs-alpine.sh [--out <path>] [--size <size>] [--release <x.y.z>] [--mirror <url>] [--arch <arch>]
|
|
||||||
|
|
||||||
Build an experimental Alpine Linux rootfs image plus a matching /root work-seed.
|
|
||||||
|
|
||||||
Defaults:
|
|
||||||
--out ./build/manual/rootfs-alpine.ext4
|
|
||||||
--size 2G
|
|
||||||
--release 3.23.3
|
|
||||||
--mirror https://dl-cdn.alpinelinux.org/alpine
|
|
||||||
--arch x86_64
|
|
||||||
|
|
||||||
This path is experimental and local-only. If ./build/manual/alpine-kernel exists
|
|
||||||
it uses the staged Alpine kernel modules from that directory. It does not change
|
|
||||||
the default Debian image flow.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_size() {
|
|
||||||
local raw="$1"
|
|
||||||
if [[ "$raw" =~ ^([0-9]+)([KMG])?$ ]]; then
|
|
||||||
local num="${BASH_REMATCH[1]}"
|
|
||||||
local unit="${BASH_REMATCH[2]}"
|
|
||||||
case "$unit" in
|
|
||||||
K) printf '%s\n' $((num * 1024)) ;;
|
|
||||||
M|"") printf '%s\n' $((num * 1024 * 1024)) ;;
|
|
||||||
G) printf '%s\n' $((num * 1024 * 1024 * 1024)) ;;
|
|
||||||
esac
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
require_command() {
|
|
||||||
local name="$1"
|
|
||||||
command -v "$name" >/dev/null 2>&1 || {
|
|
||||||
log "required command not found: $name"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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; build it first with 'make build' or set BANGER_BIN"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
find_latest_module_dir() {
|
|
||||||
local root="$1"
|
|
||||||
if [[ ! -d "$root" ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
find "$root" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_release_branch() {
|
|
||||||
local release="$1"
|
|
||||||
printf 'v%s\n' "${release%.*}"
|
|
||||||
}
|
|
||||||
|
|
||||||
load_package_preset() {
|
|
||||||
local preset="$1"
|
|
||||||
local -n out="$2"
|
|
||||||
mapfile -t out < <("$BANGER_BIN" internal packages "$preset")
|
|
||||||
(( ${#out[@]} > 0 ))
|
|
||||||
}
|
|
||||||
|
|
||||||
write_rootfs_manifest_metadata() {
|
|
||||||
local rootfs_path="$1"
|
|
||||||
local manifest_hash="$2"
|
|
||||||
printf '%s\n' "$manifest_hash" > "${rootfs_path}.packages.sha256"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_root_authorized_key() {
|
|
||||||
local public_key
|
|
||||||
public_key="$(ssh-keygen -y -f "$SSH_KEY")"
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/root/.ssh"
|
|
||||||
printf '%s\n' "$public_key" | sudo tee "$ROOT_MOUNT/root/.ssh/authorized_keys" >/dev/null
|
|
||||||
sudo chmod 700 "$ROOT_MOUNT/root/.ssh"
|
|
||||||
sudo chmod 600 "$ROOT_MOUNT/root/.ssh/authorized_keys"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_sshd_include() {
|
|
||||||
local cfg="$ROOT_MOUNT/etc/ssh/sshd_config"
|
|
||||||
local tmp_cfg="$TMP_DIR/sshd_config"
|
|
||||||
local include_line="Include /etc/ssh/sshd_config.d/*.conf"
|
|
||||||
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/etc/ssh/sshd_config.d"
|
|
||||||
if sudo test -f "$cfg"; then
|
|
||||||
sudo cat "$cfg" > "$tmp_cfg"
|
|
||||||
else
|
|
||||||
: > "$tmp_cfg"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! grep -Eq '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf([[:space:]]|$)' "$tmp_cfg"; then
|
|
||||||
{
|
|
||||||
printf '%s\n' "$include_line"
|
|
||||||
cat "$tmp_cfg"
|
|
||||||
} > "${tmp_cfg}.new"
|
|
||||||
mv "${tmp_cfg}.new" "$tmp_cfg"
|
|
||||||
sudo install -m 0644 "$tmp_cfg" "$cfg"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize_root_shell() {
|
|
||||||
local passwd="$ROOT_MOUNT/etc/passwd"
|
|
||||||
local shells="$ROOT_MOUNT/etc/shells"
|
|
||||||
local wanted_shell="/bin/bash"
|
|
||||||
local tmp_passwd="$TMP_DIR/passwd"
|
|
||||||
local root_shell=""
|
|
||||||
|
|
||||||
if [[ ! -x "$ROOT_MOUNT$wanted_shell" ]]; then
|
|
||||||
log "required root shell is missing from the Alpine image: $wanted_shell"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$shells" ]]; then
|
|
||||||
log "Alpine image is missing /etc/shells"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! sudo grep -Fxq "$wanted_shell" "$shells"; then
|
|
||||||
log "Alpine image does not allow $wanted_shell in /etc/shells"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo cat "$passwd" > "$tmp_passwd"
|
|
||||||
awk -F: -v OFS=: -v shell="$wanted_shell" '
|
|
||||||
$1 == "root" {
|
|
||||||
$7 = shell
|
|
||||||
found = 1
|
|
||||||
}
|
|
||||||
{ print }
|
|
||||||
END {
|
|
||||||
if (!found) {
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "$tmp_passwd" > "${tmp_passwd}.new" || {
|
|
||||||
log "failed to rewrite root shell in /etc/passwd"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
mv "${tmp_passwd}.new" "$tmp_passwd"
|
|
||||||
sudo install -m 0644 "$tmp_passwd" "$passwd"
|
|
||||||
|
|
||||||
root_shell="$(sudo awk -F: '$1 == "root" { print $7 }' "$passwd")"
|
|
||||||
if [[ "$root_shell" != "$wanted_shell" ]]; then
|
|
||||||
log "root shell normalization failed: expected $wanted_shell, got ${root_shell:-<empty>}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
configure_root_bash_prompt() {
|
|
||||||
local bashrc="$ROOT_MOUNT/root/.bashrc"
|
|
||||||
local bash_profile="$ROOT_MOUNT/root/.bash_profile"
|
|
||||||
local profile_prompt="$ROOT_MOUNT/etc/profile.d/banger-bash-prompt.sh"
|
|
||||||
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/root" "$ROOT_MOUNT/etc/profile.d"
|
|
||||||
cat <<'EOF' | sudo tee "$bashrc" >/dev/null
|
|
||||||
# banger: default interactive prompt for experimental Alpine guests
|
|
||||||
case "$-" in
|
|
||||||
*i*) ;;
|
|
||||||
*) return ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ -z "${BANGER_MISE_ACTIVATED:-}" ] && [ -x '/usr/local/bin/mise' ]; then
|
|
||||||
export BANGER_MISE_ACTIVATED=1
|
|
||||||
eval "$(/usr/local/bin/mise activate bash)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
PS1='\u@\h:\w\$ '
|
|
||||||
EOF
|
|
||||||
cat <<'EOF' | sudo tee "$bash_profile" >/dev/null
|
|
||||||
if [ -f ~/.bashrc ]; then
|
|
||||||
. ~/.bashrc
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
cat <<'EOF' | sudo tee "$profile_prompt" >/dev/null
|
|
||||||
case "$-" in
|
|
||||||
*i*) ;;
|
|
||||||
*) return 0 2>/dev/null || exit 0 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ -n "${BASH_VERSION:-}" ]; then
|
|
||||||
PS1='\u@\h:\w\$ '
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
sudo chmod 0644 "$bashrc" "$bash_profile" "$profile_prompt"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_guest_network_bootstrap() {
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/usr/local/libexec"
|
|
||||||
sudo install -m 0755 "$GUESTNET_BOOTSTRAP_SCRIPT" "$ROOT_MOUNT/usr/local/libexec/banger-network-bootstrap"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_openrc_services() {
|
|
||||||
local initd_dir="$ROOT_MOUNT/etc/init.d"
|
|
||||||
|
|
||||||
sudo mkdir -p "$initd_dir"
|
|
||||||
|
|
||||||
cat <<'EOF' | sudo tee "$initd_dir/banger-network" >/dev/null
|
|
||||||
#!/sbin/openrc-run
|
|
||||||
description="Banger guest network bootstrap"
|
|
||||||
|
|
||||||
depend() {
|
|
||||||
need localmount
|
|
||||||
before sshd docker banger-opencode
|
|
||||||
provide net
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
ebegin "Configuring guest network"
|
|
||||||
/usr/local/libexec/banger-network-bootstrap
|
|
||||||
eend $?
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat <<'EOF' | sudo tee "$initd_dir/banger-docker-preflight" >/dev/null
|
|
||||||
#!/sbin/openrc-run
|
|
||||||
description="Banger Docker kernel preflight"
|
|
||||||
|
|
||||||
depend() {
|
|
||||||
after modules
|
|
||||||
before docker
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
ebegin "Preparing Docker kernel state"
|
|
||||||
for module in nf_tables nft_chain_nat veth br_netfilter overlay; do
|
|
||||||
modprobe "$module" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
if command -v sysctl >/dev/null 2>&1; then
|
|
||||||
sysctl -p /etc/sysctl.d/99-docker.conf >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
eend 0
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat <<'EOF' | sudo tee "$initd_dir/banger-vsock-agent" >/dev/null
|
|
||||||
#!/sbin/openrc-run
|
|
||||||
description="Banger vsock agent"
|
|
||||||
pidfile="/run/${RC_SVCNAME}.pid"
|
|
||||||
command="/usr/local/bin/banger-vsock-agent"
|
|
||||||
|
|
||||||
depend() {
|
|
||||||
need localmount
|
|
||||||
before banger-network sshd docker banger-opencode
|
|
||||||
}
|
|
||||||
|
|
||||||
start_pre() {
|
|
||||||
modprobe vsock 2>/dev/null || true
|
|
||||||
modprobe vmw_vsock_virtio_transport 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
ebegin "Starting ${RC_SVCNAME}"
|
|
||||||
start-stop-daemon --start --exec "$command" --background --make-pidfile --pidfile "$pidfile"
|
|
||||||
eend $?
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
ebegin "Stopping ${RC_SVCNAME}"
|
|
||||||
start-stop-daemon --stop --exec "$command" --pidfile "$pidfile"
|
|
||||||
eend $?
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat <<'EOF' | sudo tee "$initd_dir/banger-opencode" >/dev/null
|
|
||||||
#!/sbin/openrc-run
|
|
||||||
description="Banger opencode server"
|
|
||||||
pidfile="/run/${RC_SVCNAME}.pid"
|
|
||||||
command="/usr/local/bin/opencode"
|
|
||||||
command_args="serve --hostname 0.0.0.0 --port 4096"
|
|
||||||
|
|
||||||
depend() {
|
|
||||||
need localmount
|
|
||||||
after banger-network
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
ebegin "Starting ${RC_SVCNAME}"
|
|
||||||
HOME=/root start-stop-daemon --start --exec "$command" --background --make-pidfile --pidfile "$pidfile" --chdir /root -- $command_args
|
|
||||||
eend $?
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
ebegin "Stopping ${RC_SVCNAME}"
|
|
||||||
start-stop-daemon --stop --exec "$command" --pidfile "$pidfile"
|
|
||||||
eend $?
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo chmod 0755 \
|
|
||||||
"$initd_dir/banger-network" \
|
|
||||||
"$initd_dir/banger-docker-preflight" \
|
|
||||||
"$initd_dir/banger-vsock-agent" \
|
|
||||||
"$initd_dir/banger-opencode"
|
|
||||||
}
|
|
||||||
|
|
||||||
configure_docker_bootstrap() {
|
|
||||||
local modules_conf="$ROOT_MOUNT/etc/modules-load.d/docker-netfilter.conf"
|
|
||||||
local sysctl_conf="$ROOT_MOUNT/etc/sysctl.d/99-docker.conf"
|
|
||||||
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/etc/modules-load.d" "$ROOT_MOUNT/etc/sysctl.d"
|
|
||||||
cat <<'EOF' | sudo tee "$modules_conf" >/dev/null
|
|
||||||
nf_tables
|
|
||||||
nft_chain_nat
|
|
||||||
veth
|
|
||||||
br_netfilter
|
|
||||||
overlay
|
|
||||||
EOF
|
|
||||||
cat <<'EOF' | sudo tee "$sysctl_conf" >/dev/null
|
|
||||||
net.bridge.bridge-nf-call-iptables = 1
|
|
||||||
net.bridge.bridge-nf-call-ip6tables = 1
|
|
||||||
net.ipv4.ip_forward = 1
|
|
||||||
EOF
|
|
||||||
sudo chmod 0644 "$modules_conf" "$sysctl_conf"
|
|
||||||
}
|
|
||||||
|
|
||||||
configure_vsock_modules() {
|
|
||||||
local modules_conf="$ROOT_MOUNT/etc/modules-load.d/banger-vsock.conf"
|
|
||||||
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/etc/modules-load.d"
|
|
||||||
cat <<'EOF' | sudo tee "$modules_conf" >/dev/null
|
|
||||||
vsock
|
|
||||||
vmw_vsock_virtio_transport
|
|
||||||
EOF
|
|
||||||
sudo chmod 0644 "$modules_conf"
|
|
||||||
}
|
|
||||||
|
|
||||||
configure_apk_repositories() {
|
|
||||||
local repositories="$ROOT_MOUNT/etc/apk/repositories"
|
|
||||||
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/etc/apk"
|
|
||||||
cat <<EOF | sudo tee "$repositories" >/dev/null
|
|
||||||
$APK_RELEASE_URL/main
|
|
||||||
$APK_RELEASE_URL/community
|
|
||||||
EOF
|
|
||||||
sudo chmod 0644 "$repositories"
|
|
||||||
if [[ -r /etc/resolv.conf ]]; then
|
|
||||||
sudo install -m 0644 /etc/resolv.conf "$ROOT_MOUNT/etc/resolv.conf"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
build_alpine_initramfs() {
|
|
||||||
local kernel_version="$1"
|
|
||||||
local guest_output="/boot/initramfs-${kernel_version}.img"
|
|
||||||
local stage_output="$MANUAL_DIR/alpine-kernel/boot/initramfs-${kernel_version}.img"
|
|
||||||
local mkinitfs_dir="$ROOT_MOUNT/etc/mkinitfs"
|
|
||||||
local mkinitfs_conf="$mkinitfs_dir/mkinitfs.conf"
|
|
||||||
|
|
||||||
sudo mkdir -p "$mkinitfs_dir" "$ROOT_MOUNT/boot" "$MANUAL_DIR/alpine-kernel/boot"
|
|
||||||
cat <<'EOF' | sudo tee "$mkinitfs_conf" >/dev/null
|
|
||||||
features="ata base ide scsi usb virtio ext4 nvme"
|
|
||||||
EOF
|
|
||||||
sudo chmod 0644 "$mkinitfs_conf"
|
|
||||||
|
|
||||||
log "building Alpine initramfs for kernel $kernel_version"
|
|
||||||
sudo env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
|
|
||||||
chroot "$ROOT_MOUNT" /bin/sh -se <<EOF
|
|
||||||
set -eu
|
|
||||||
mkinitfs -c /etc/mkinitfs/mkinitfs.conf -b / -o "$guest_output" "$kernel_version"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [[ ! -f "$ROOT_MOUNT$guest_output" ]]; then
|
|
||||||
log "mkinitfs did not produce $guest_output inside the guest rootfs"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sudo install -m 0644 "$ROOT_MOUNT$guest_output" "$stage_output"
|
|
||||||
}
|
|
||||||
|
|
||||||
mount_chroot_support() {
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/dev" "$ROOT_MOUNT/dev/pts" "$ROOT_MOUNT/proc" "$ROOT_MOUNT/sys"
|
|
||||||
sudo mount --bind /dev "$ROOT_MOUNT/dev"
|
|
||||||
DEV_MOUNTED=1
|
|
||||||
sudo mount --bind /dev/pts "$ROOT_MOUNT/dev/pts"
|
|
||||||
DEVPTS_MOUNTED=1
|
|
||||||
sudo mount -t proc proc "$ROOT_MOUNT/proc"
|
|
||||||
PROC_MOUNTED=1
|
|
||||||
sudo mount -t sysfs sys "$ROOT_MOUNT/sys"
|
|
||||||
SYS_MOUNTED=1
|
|
||||||
}
|
|
||||||
|
|
||||||
install_mise_and_opencode() {
|
|
||||||
local profile_mise="$ROOT_MOUNT/etc/profile.d/mise.sh"
|
|
||||||
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/etc/profile.d"
|
|
||||||
if [[ -r /etc/resolv.conf ]]; then
|
|
||||||
sudo install -m 0644 /etc/resolv.conf "$ROOT_MOUNT/etc/resolv.conf"
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo env \
|
|
||||||
HOME=/root \
|
|
||||||
PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin \
|
|
||||||
chroot "$ROOT_MOUNT" /bin/sh -se <<EOF
|
|
||||||
set -eu
|
|
||||||
curl -fsSL https://mise.run | MISE_INSTALL_PATH="$MISE_INSTALL_PATH" MISE_VERSION="$MISE_VERSION" sh
|
|
||||||
"$MISE_INSTALL_PATH" use -g "$OPENCODE_TOOL"
|
|
||||||
"$MISE_INSTALL_PATH" reshim
|
|
||||||
if [ ! -e /root/.local/share/mise/shims/opencode ]; then
|
|
||||||
echo "opencode shim not found after mise install" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
ln -snf /root/.local/share/mise/shims/opencode /usr/local/bin/opencode
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat <<'EOF' | sudo tee "$profile_mise" >/dev/null
|
|
||||||
if [ -n "${BASH_VERSION:-}" ] && [ -z "${BANGER_MISE_ACTIVATED:-}" ] && [ -x '/usr/local/bin/mise' ]; then
|
|
||||||
export BANGER_MISE_ACTIVATED=1
|
|
||||||
eval "$(/usr/local/bin/mise activate bash)"
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
sudo chmod 0644 "$profile_mise"
|
|
||||||
}
|
|
||||||
|
|
||||||
enable_openrc_services() {
|
|
||||||
sudo env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin chroot "$ROOT_MOUNT" /bin/sh -se <<'EOF'
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
add_service() {
|
|
||||||
local service="$1"
|
|
||||||
local runlevel="$2"
|
|
||||||
if [ ! -x "/etc/init.d/$service" ]; then
|
|
||||||
echo "missing OpenRC service: $service" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
rc-update add "$service" "$runlevel" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
for service in devfs dmesg mdev; do
|
|
||||||
add_service "$service" sysinit
|
|
||||||
done
|
|
||||||
for service in hwdrivers modules sysctl hostname bootmisc cgroups; do
|
|
||||||
add_service "$service" boot
|
|
||||||
done
|
|
||||||
for service in banger-network sshd banger-docker-preflight docker banger-vsock-agent banger-opencode; do
|
|
||||||
add_service "$service" default
|
|
||||||
done
|
|
||||||
for service in mount-ro killprocs; do
|
|
||||||
add_service "$service" shutdown
|
|
||||||
done
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [[ "${SYS_MOUNTED:-0}" == "1" ]] && command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$ROOT_MOUNT/sys"; then
|
|
||||||
sudo umount "$ROOT_MOUNT/sys" || true
|
|
||||||
fi
|
|
||||||
if [[ "${PROC_MOUNTED:-0}" == "1" ]] && command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$ROOT_MOUNT/proc"; then
|
|
||||||
sudo umount "$ROOT_MOUNT/proc" || true
|
|
||||||
fi
|
|
||||||
if [[ "${DEVPTS_MOUNTED:-0}" == "1" ]] && command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$ROOT_MOUNT/dev/pts"; then
|
|
||||||
sudo umount "$ROOT_MOUNT/dev/pts" || true
|
|
||||||
fi
|
|
||||||
if [[ "${DEV_MOUNTED:-0}" == "1" ]] && command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$ROOT_MOUNT/dev"; then
|
|
||||||
sudo umount "$ROOT_MOUNT/dev" || true
|
|
||||||
fi
|
|
||||||
if [[ -n "${ROOT_MOUNT:-}" ]] && command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$ROOT_MOUNT"; then
|
|
||||||
sudo umount "$ROOT_MOUNT" || true
|
|
||||||
fi
|
|
||||||
if [[ "${BUILD_DONE:-0}" != "1" ]]; then
|
|
||||||
rm -f "${OUT_ROOTFS:-}" "${WORK_SEED:-}" "${OUT_ROOTFS:-}.packages.sha256"
|
|
||||||
fi
|
|
||||||
if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR:-}" ]]; then
|
|
||||||
rm -rf "$TMP_DIR"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
MANUAL_DIR="${BANGER_MANUAL_DIR:-$REPO_ROOT/build/manual}"
|
|
||||||
BANGER_BIN="$(resolve_banger_bin)"
|
|
||||||
SSH_KEY="$("$BANGER_BIN" internal ssh-key-path)"
|
|
||||||
OUT_ROOTFS="$MANUAL_DIR/rootfs-alpine.ext4"
|
|
||||||
SIZE_SPEC="2G"
|
|
||||||
RELEASE="${ALPINE_RELEASE:-3.23.3}"
|
|
||||||
MIRROR="https://dl-cdn.alpinelinux.org/alpine"
|
|
||||||
ARCH="x86_64"
|
|
||||||
MISE_VERSION="v2025.12.0"
|
|
||||||
MISE_INSTALL_PATH="/usr/local/bin/mise"
|
|
||||||
OPENCODE_TOOL="github:anomalyco/opencode"
|
|
||||||
GUESTNET_BOOTSTRAP_SCRIPT="$REPO_ROOT/internal/guestnet/assets/bootstrap.sh"
|
|
||||||
MODULES_DIR=""
|
|
||||||
ALPINE_KERNEL_MODULES_DIR="$(find_latest_module_dir "$MANUAL_DIR/alpine-kernel/lib/modules" || true)"
|
|
||||||
VSOCK_AGENT="$("$BANGER_BIN" internal vsock-agent-path)"
|
|
||||||
if [[ -n "$ALPINE_KERNEL_MODULES_DIR" ]]; then
|
|
||||||
MODULES_DIR="$ALPINE_KERNEL_MODULES_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--out)
|
|
||||||
OUT_ROOTFS="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--size)
|
|
||||||
SIZE_SPEC="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--release)
|
|
||||||
RELEASE="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--mirror)
|
|
||||||
MIRROR="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--arch)
|
|
||||||
ARCH="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log "unknown option: $1"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$ARCH" != "x86_64" ]]; then
|
|
||||||
log "unsupported arch: $ARCH"
|
|
||||||
log "this experimental builder currently supports only x86_64"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$MODULES_DIR" || ! -d "$MODULES_DIR" ]]; then
|
|
||||||
log "modules dir not found; run 'make alpine-kernel' first"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -x "$VSOCK_AGENT" ]]; then
|
|
||||||
log "vsock agent not found or not executable: $VSOCK_AGENT"
|
|
||||||
log "run 'make build'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$GUESTNET_BOOTSTRAP_SCRIPT" ]]; then
|
|
||||||
log "guest network bootstrap script not found: $GUESTNET_BOOTSTRAP_SCRIPT"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -e "$OUT_ROOTFS" ]]; then
|
|
||||||
log "output rootfs already exists: $OUT_ROOTFS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
require_command curl
|
|
||||||
require_command tar
|
|
||||||
require_command sudo
|
|
||||||
require_command mkfs.ext4
|
|
||||||
require_command ssh-keygen
|
|
||||||
require_command mount
|
|
||||||
require_command umount
|
|
||||||
require_command install
|
|
||||||
require_command find
|
|
||||||
require_command awk
|
|
||||||
require_command sed
|
|
||||||
require_command sha256sum
|
|
||||||
require_command truncate
|
|
||||||
require_command mountpoint
|
|
||||||
require_command chroot
|
|
||||||
require_command cp
|
|
||||||
|
|
||||||
ALPINE_PACKAGES=()
|
|
||||||
if ! load_package_preset alpine ALPINE_PACKAGES; then
|
|
||||||
log "alpine package preset is empty"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! PACKAGES_HASH="$(printf '%s\n' "${ALPINE_PACKAGES[@]}" | sha256sum | awk '{print $1}')"; then
|
|
||||||
log "failed to hash package preset"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! SIZE_BYTES="$(parse_size "$SIZE_SPEC")"; then
|
|
||||||
log "invalid size: $SIZE_SPEC"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$OUT_ROOTFS" == *.ext4 ]]; then
|
|
||||||
WORK_SEED="${OUT_ROOTFS%.ext4}.work-seed.ext4"
|
|
||||||
else
|
|
||||||
WORK_SEED="${OUT_ROOTFS}.work-seed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
BRANCH="$(resolve_release_branch "$RELEASE")"
|
|
||||||
RELEASE_DIR="$MIRROR/$BRANCH/releases/$ARCH"
|
|
||||||
MINIROOTFS_URL="$RELEASE_DIR/alpine-minirootfs-$RELEASE-$ARCH.tar.gz"
|
|
||||||
MINIROOTFS_SHA256_URL="$MINIROOTFS_URL.sha256"
|
|
||||||
APK_RELEASE_URL="$MIRROR/$BRANCH"
|
|
||||||
|
|
||||||
TMP_DIR="$(mktemp -d -t banger-alpine-rootfs-XXXXXX)"
|
|
||||||
MINIROOTFS_ARCHIVE="$TMP_DIR/alpine-minirootfs.tar.gz"
|
|
||||||
ROOT_MOUNT="$TMP_DIR/rootfs"
|
|
||||||
BUILD_DONE=0
|
|
||||||
DEV_MOUNTED=0
|
|
||||||
DEVPTS_MOUNTED=0
|
|
||||||
PROC_MOUNTED=0
|
|
||||||
SYS_MOUNTED=0
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
mkdir -p "$ROOT_MOUNT"
|
|
||||||
|
|
||||||
log "downloading Alpine minirootfs from $MINIROOTFS_URL"
|
|
||||||
curl -fsSL "$MINIROOTFS_URL" -o "$MINIROOTFS_ARCHIVE"
|
|
||||||
expected_sha="$(curl -fsSL "$MINIROOTFS_SHA256_URL" | awk '{print $1}')"
|
|
||||||
actual_sha="$(sha256sum "$MINIROOTFS_ARCHIVE" | awk '{print $1}')"
|
|
||||||
if [[ -z "$expected_sha" ]]; then
|
|
||||||
log "failed to read SHA256 from $MINIROOTFS_SHA256_URL"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ "$expected_sha" != "$actual_sha" ]]; then
|
|
||||||
log "sha256 mismatch for $MINIROOTFS_URL"
|
|
||||||
log "expected: $expected_sha"
|
|
||||||
log "actual: $actual_sha"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "creating $OUT_ROOTFS ($SIZE_SPEC)"
|
|
||||||
truncate -s "$SIZE_BYTES" "$OUT_ROOTFS"
|
|
||||||
mkfs.ext4 -F -m 0 -L banger-alpine-root "$OUT_ROOTFS" >/dev/null
|
|
||||||
sudo mount -o loop "$OUT_ROOTFS" "$ROOT_MOUNT"
|
|
||||||
|
|
||||||
log "unpacking Alpine minirootfs"
|
|
||||||
sudo tar -xzf "$MINIROOTFS_ARCHIVE" -C "$ROOT_MOUNT"
|
|
||||||
configure_apk_repositories
|
|
||||||
mount_chroot_support
|
|
||||||
|
|
||||||
log "installing Alpine packages into the rootfs"
|
|
||||||
sudo env HOME=/root PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin \
|
|
||||||
chroot "$ROOT_MOUNT" /bin/sh -se <<EOF
|
|
||||||
set -eu
|
|
||||||
apk update
|
|
||||||
apk add --no-cache ${ALPINE_PACKAGES[*]}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
log "copying staged Alpine kernel modules into the guest"
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/lib/modules"
|
|
||||||
sudo cp -a "$MODULES_DIR" "$ROOT_MOUNT/lib/modules/"
|
|
||||||
KERNEL_VERSION="$(basename "$MODULES_DIR")"
|
|
||||||
|
|
||||||
log "installing the guest-side vsock agent"
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/usr/local/bin"
|
|
||||||
sudo install -m 0755 "$VSOCK_AGENT" "$ROOT_MOUNT/usr/local/bin/banger-vsock-agent"
|
|
||||||
|
|
||||||
log "preparing SSH, OpenRC, and Docker bootstrap"
|
|
||||||
install_guest_network_bootstrap
|
|
||||||
install_openrc_services
|
|
||||||
ensure_sshd_include
|
|
||||||
configure_docker_bootstrap
|
|
||||||
configure_vsock_modules
|
|
||||||
normalize_root_shell
|
|
||||||
configure_root_bash_prompt
|
|
||||||
log "installing mise and opencode"
|
|
||||||
install_mise_and_opencode
|
|
||||||
install_root_authorized_key
|
|
||||||
build_alpine_initramfs "$KERNEL_VERSION"
|
|
||||||
|
|
||||||
sudo touch "$ROOT_MOUNT/etc/fstab" "$ROOT_MOUNT/etc/hostname"
|
|
||||||
sudo chroot "$ROOT_MOUNT" /usr/bin/ssh-keygen -A
|
|
||||||
enable_openrc_services
|
|
||||||
|
|
||||||
sudo chroot "$ROOT_MOUNT" /bin/sh -se <<'EOF'
|
|
||||||
set -eu
|
|
||||||
git config --system init.defaultBranch main
|
|
||||||
EOF
|
|
||||||
|
|
||||||
log "removing bulky caches, docs, and stale installer artifacts from the experimental image"
|
|
||||||
sudo rm -rf \
|
|
||||||
"$ROOT_MOUNT/var/cache/apk" \
|
|
||||||
"$ROOT_MOUNT/usr/share/doc" \
|
|
||||||
"$ROOT_MOUNT/usr/share/info" \
|
|
||||||
"$ROOT_MOUNT/usr/share/man"
|
|
||||||
sudo rm -f \
|
|
||||||
"$ROOT_MOUNT/root/get-docker" \
|
|
||||||
"$ROOT_MOUNT/root/get-docker.sh" \
|
|
||||||
"$ROOT_MOUNT/tmp/get-docker" \
|
|
||||||
"$ROOT_MOUNT/tmp/get-docker.sh"
|
|
||||||
sudo rm -rf \
|
|
||||||
"$ROOT_MOUNT/root/.cache/mise" \
|
|
||||||
"$ROOT_MOUNT/root/.cache/opencode" \
|
|
||||||
"$ROOT_MOUNT/root/.local/share/mise/downloads" \
|
|
||||||
"$ROOT_MOUNT/root/.local/share/mise/tmp"
|
|
||||||
|
|
||||||
sudo umount "$ROOT_MOUNT/sys"
|
|
||||||
SYS_MOUNTED=0
|
|
||||||
sudo umount "$ROOT_MOUNT/proc"
|
|
||||||
PROC_MOUNTED=0
|
|
||||||
sudo umount "$ROOT_MOUNT/dev/pts"
|
|
||||||
DEVPTS_MOUNTED=0
|
|
||||||
sudo umount "$ROOT_MOUNT/dev"
|
|
||||||
DEV_MOUNTED=0
|
|
||||||
sudo umount "$ROOT_MOUNT"
|
|
||||||
|
|
||||||
write_rootfs_manifest_metadata "$OUT_ROOTFS" "$PACKAGES_HASH"
|
|
||||||
|
|
||||||
log "building work-seed $WORK_SEED"
|
|
||||||
"$BANGER_BIN" internal work-seed --rootfs "$OUT_ROOTFS" --out "$WORK_SEED"
|
|
||||||
|
|
||||||
BUILD_DONE=1
|
|
||||||
log "built experimental Alpine rootfs: $OUT_ROOTFS"
|
|
||||||
log "built experimental Alpine work-seed: $WORK_SEED"
|
|
||||||
|
|
@ -1,616 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[make-rootfs-void] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: ./scripts/make-rootfs-void.sh [--out <path>] [--size <size>] [--mirror <url>] [--arch <arch>]
|
|
||||||
|
|
||||||
Build an experimental Void Linux rootfs image plus a matching /root work-seed.
|
|
||||||
|
|
||||||
Defaults:
|
|
||||||
--out ./build/manual/rootfs-void.ext4
|
|
||||||
--size 4G
|
|
||||||
--mirror https://repo-default.voidlinux.org
|
|
||||||
--arch x86_64
|
|
||||||
|
|
||||||
This path is experimental and local-only. If ./build/manual/void-kernel exists
|
|
||||||
it uses the staged Void kernel modules from that directory. It does not change
|
|
||||||
the default Debian image flow.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_size() {
|
|
||||||
local raw="$1"
|
|
||||||
if [[ "$raw" =~ ^([0-9]+)([KMG])?$ ]]; then
|
|
||||||
local num="${BASH_REMATCH[1]}"
|
|
||||||
local unit="${BASH_REMATCH[2]}"
|
|
||||||
case "$unit" in
|
|
||||||
K) printf '%s\n' $((num * 1024)) ;;
|
|
||||||
M|"") printf '%s\n' $((num * 1024 * 1024)) ;;
|
|
||||||
G) printf '%s\n' $((num * 1024 * 1024 * 1024)) ;;
|
|
||||||
esac
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
require_command() {
|
|
||||||
local name="$1"
|
|
||||||
command -v "$name" >/dev/null 2>&1 || {
|
|
||||||
log "required command not found: $name"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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; build it first with 'make build' or set BANGER_BIN"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize_mirror() {
|
|
||||||
local mirror="${1%/}"
|
|
||||||
mirror="${mirror%/current}"
|
|
||||||
mirror="${mirror%/static}"
|
|
||||||
printf '%s\n' "$mirror"
|
|
||||||
}
|
|
||||||
|
|
||||||
find_latest_module_dir() {
|
|
||||||
local root="$1"
|
|
||||||
if [[ ! -d "$root" ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
find "$root" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1
|
|
||||||
}
|
|
||||||
|
|
||||||
find_static_binary() {
|
|
||||||
local name="$1"
|
|
||||||
find "$STATIC_DIR" -type f \( -name "$name" -o -name "$name.static" \) -perm -u+x | sort | head -n 1
|
|
||||||
}
|
|
||||||
|
|
||||||
find_static_keys_dir() {
|
|
||||||
find "$STATIC_DIR" -type d -path '*/var/db/xbps/keys' | sort | head -n 1
|
|
||||||
}
|
|
||||||
|
|
||||||
load_package_preset() {
|
|
||||||
local preset="$1"
|
|
||||||
local -n out="$2"
|
|
||||||
mapfile -t out < <("$BANGER_BIN" internal packages "$preset")
|
|
||||||
(( ${#out[@]} > 0 ))
|
|
||||||
}
|
|
||||||
|
|
||||||
write_rootfs_manifest_metadata() {
|
|
||||||
local rootfs_path="$1"
|
|
||||||
local manifest_hash="$2"
|
|
||||||
printf '%s\n' "$manifest_hash" > "${rootfs_path}.packages.sha256"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_root_authorized_key() {
|
|
||||||
local public_key
|
|
||||||
public_key="$(ssh-keygen -y -f "$SSH_KEY")"
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/root/.ssh"
|
|
||||||
printf '%s\n' "$public_key" | sudo tee "$ROOT_MOUNT/root/.ssh/authorized_keys" >/dev/null
|
|
||||||
sudo chmod 700 "$ROOT_MOUNT/root/.ssh"
|
|
||||||
sudo chmod 600 "$ROOT_MOUNT/root/.ssh/authorized_keys"
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_sshd_include() {
|
|
||||||
local cfg="$ROOT_MOUNT/etc/ssh/sshd_config"
|
|
||||||
local tmp_cfg="$TMP_DIR/sshd_config"
|
|
||||||
local include_line="Include /etc/ssh/sshd_config.d/*.conf"
|
|
||||||
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/etc/ssh/sshd_config.d"
|
|
||||||
if sudo test -f "$cfg"; then
|
|
||||||
sudo cat "$cfg" > "$tmp_cfg"
|
|
||||||
else
|
|
||||||
: > "$tmp_cfg"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! grep -Eq '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf([[:space:]]|$)' "$tmp_cfg"; then
|
|
||||||
{
|
|
||||||
printf '%s\n' "$include_line"
|
|
||||||
cat "$tmp_cfg"
|
|
||||||
} > "${tmp_cfg}.new"
|
|
||||||
mv "${tmp_cfg}.new" "$tmp_cfg"
|
|
||||||
sudo install -m 0644 "$tmp_cfg" "$cfg"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
install_vsock_service() {
|
|
||||||
local service_dir="$ROOT_MOUNT/etc/sv/banger-vsock-agent"
|
|
||||||
local run_path="$service_dir/run"
|
|
||||||
local finish_path="$service_dir/finish"
|
|
||||||
|
|
||||||
sudo mkdir -p "$service_dir"
|
|
||||||
cat <<'EOF' | sudo tee "$run_path" >/dev/null
|
|
||||||
#!/bin/sh
|
|
||||||
modprobe vsock 2>/dev/null || true
|
|
||||||
modprobe vmw_vsock_virtio_transport 2>/dev/null || true
|
|
||||||
exec /usr/local/bin/banger-vsock-agent
|
|
||||||
EOF
|
|
||||||
cat <<'EOF' | sudo tee "$finish_path" >/dev/null
|
|
||||||
#!/bin/sh
|
|
||||||
exit 0
|
|
||||||
EOF
|
|
||||||
sudo chmod 0755 "$run_path" "$finish_path"
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/etc/runit/runsvdir/default"
|
|
||||||
sudo ln -snf /etc/sv/banger-vsock-agent "$ROOT_MOUNT/etc/runit/runsvdir/default/banger-vsock-agent"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_opencode_service() {
|
|
||||||
local service_dir="$ROOT_MOUNT/etc/sv/banger-opencode"
|
|
||||||
local run_path="$service_dir/run"
|
|
||||||
local finish_path="$service_dir/finish"
|
|
||||||
|
|
||||||
sudo mkdir -p "$service_dir"
|
|
||||||
cat <<'EOF' | sudo tee "$run_path" >/dev/null
|
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
export HOME=/root
|
|
||||||
cd /root
|
|
||||||
exec /usr/local/bin/opencode serve --hostname 0.0.0.0 --port 4096
|
|
||||||
EOF
|
|
||||||
cat <<'EOF' | sudo tee "$finish_path" >/dev/null
|
|
||||||
#!/bin/sh
|
|
||||||
exit 0
|
|
||||||
EOF
|
|
||||||
sudo chmod 0755 "$run_path" "$finish_path"
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/etc/runit/runsvdir/default"
|
|
||||||
sudo ln -snf /etc/sv/banger-opencode "$ROOT_MOUNT/etc/runit/runsvdir/default/banger-opencode"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_guest_network_bootstrap() {
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/usr/local/libexec" "$ROOT_MOUNT/etc/runit/core-services"
|
|
||||||
sudo install -m 0755 "$GUESTNET_BOOTSTRAP_SCRIPT" "$ROOT_MOUNT/usr/local/libexec/banger-network-bootstrap"
|
|
||||||
sudo install -m 0644 "$GUESTNET_VOID_CORE_SERVICE" "$ROOT_MOUNT/etc/runit/core-services/20-banger-network.sh"
|
|
||||||
}
|
|
||||||
|
|
||||||
configure_docker_bootstrap() {
|
|
||||||
local modules_conf="$ROOT_MOUNT/etc/modules-load.d/docker-netfilter.conf"
|
|
||||||
local sysctl_conf="$ROOT_MOUNT/etc/sysctl.d/99-docker.conf"
|
|
||||||
local service_dir="$ROOT_MOUNT/etc/sv/docker"
|
|
||||||
local run_path="$service_dir/run"
|
|
||||||
local orig_run_path="$service_dir/run.orig"
|
|
||||||
local preflight_path="$ROOT_MOUNT/usr/local/bin/banger-docker-preflight"
|
|
||||||
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/etc/modules-load.d" "$ROOT_MOUNT/etc/sysctl.d" "$ROOT_MOUNT/usr/local/bin"
|
|
||||||
cat <<'EOF' | sudo tee "$modules_conf" >/dev/null
|
|
||||||
nf_tables
|
|
||||||
nft_chain_nat
|
|
||||||
veth
|
|
||||||
br_netfilter
|
|
||||||
overlay
|
|
||||||
EOF
|
|
||||||
cat <<'EOF' | sudo tee "$sysctl_conf" >/dev/null
|
|
||||||
net.bridge.bridge-nf-call-iptables = 1
|
|
||||||
net.bridge.bridge-nf-call-ip6tables = 1
|
|
||||||
net.ipv4.ip_forward = 1
|
|
||||||
EOF
|
|
||||||
cat <<'EOF' | sudo tee "$preflight_path" >/dev/null
|
|
||||||
#!/bin/sh
|
|
||||||
for module in nf_tables nft_chain_nat veth br_netfilter overlay; do
|
|
||||||
modprobe "$module" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
if command -v sysctl >/dev/null 2>&1; then
|
|
||||||
sysctl --load /etc/sysctl.d/99-docker.conf >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [[ ! -f "$run_path" ]]; then
|
|
||||||
log "Void rootfs is missing /etc/sv/docker/run after docker install"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sudo install -m 0755 "$run_path" "$orig_run_path"
|
|
||||||
cat <<'EOF' | sudo tee "$run_path" >/dev/null
|
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
/usr/local/bin/banger-docker-preflight
|
|
||||||
exec /etc/sv/docker/run.orig
|
|
||||||
EOF
|
|
||||||
sudo chmod 0644 "$modules_conf" "$sysctl_conf"
|
|
||||||
sudo chmod 0755 "$preflight_path" "$run_path" "$orig_run_path"
|
|
||||||
}
|
|
||||||
|
|
||||||
enable_sshd_service() {
|
|
||||||
if [[ ! -d "$ROOT_MOUNT/etc/sv/sshd" ]]; then
|
|
||||||
log "Void rootfs is missing /etc/sv/sshd after openssh install"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/etc/runit/runsvdir/default"
|
|
||||||
sudo ln -snf /etc/sv/sshd "$ROOT_MOUNT/etc/runit/runsvdir/default/sshd"
|
|
||||||
}
|
|
||||||
|
|
||||||
enable_docker_service() {
|
|
||||||
if [[ ! -d "$ROOT_MOUNT/etc/sv/docker" ]]; then
|
|
||||||
log "Void rootfs is missing /etc/sv/docker after docker install"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/etc/runit/runsvdir/default"
|
|
||||||
sudo ln -snf /etc/sv/docker "$ROOT_MOUNT/etc/runit/runsvdir/default/docker"
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize_root_shell() {
|
|
||||||
local passwd="$ROOT_MOUNT/etc/passwd"
|
|
||||||
local shells="$ROOT_MOUNT/etc/shells"
|
|
||||||
local wanted_shell="/bin/bash"
|
|
||||||
local tmp_passwd="$TMP_DIR/passwd"
|
|
||||||
local root_shell=""
|
|
||||||
|
|
||||||
if [[ ! -x "$ROOT_MOUNT$wanted_shell" ]]; then
|
|
||||||
log "required root shell is missing from the Void image: $wanted_shell"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$shells" ]]; then
|
|
||||||
log "Void image is missing /etc/shells"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! sudo grep -Fxq "$wanted_shell" "$shells"; then
|
|
||||||
log "Void image does not allow $wanted_shell in /etc/shells"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo cat "$passwd" > "$tmp_passwd"
|
|
||||||
awk -F: -v OFS=: -v shell="$wanted_shell" '
|
|
||||||
$1 == "root" {
|
|
||||||
$7 = shell
|
|
||||||
found = 1
|
|
||||||
}
|
|
||||||
{ print }
|
|
||||||
END {
|
|
||||||
if (!found) {
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "$tmp_passwd" > "${tmp_passwd}.new" || {
|
|
||||||
log "failed to rewrite root shell in /etc/passwd"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
mv "${tmp_passwd}.new" "$tmp_passwd"
|
|
||||||
sudo install -m 0644 "$tmp_passwd" "$passwd"
|
|
||||||
|
|
||||||
root_shell="$(sudo awk -F: '$1 == "root" { print $7 }' "$passwd")"
|
|
||||||
if [[ "$root_shell" != "$wanted_shell" ]]; then
|
|
||||||
log "root shell normalization failed: expected $wanted_shell, got ${root_shell:-<empty>}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
configure_root_bash_prompt() {
|
|
||||||
local bashrc="$ROOT_MOUNT/root/.bashrc"
|
|
||||||
local bash_profile="$ROOT_MOUNT/root/.bash_profile"
|
|
||||||
local profile_prompt="$ROOT_MOUNT/etc/profile.d/banger-bash-prompt.sh"
|
|
||||||
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/root" "$ROOT_MOUNT/etc/profile.d"
|
|
||||||
cat <<'EOF' | sudo tee "$bashrc" >/dev/null
|
|
||||||
# banger: default interactive prompt for experimental Void guests
|
|
||||||
case "$-" in
|
|
||||||
*i*) ;;
|
|
||||||
*) return ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ -z "${BANGER_MISE_ACTIVATED:-}" ] && [ -x '/usr/local/bin/mise' ]; then
|
|
||||||
export BANGER_MISE_ACTIVATED=1
|
|
||||||
eval "$(/usr/local/bin/mise activate bash)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
PS1='\u@\h:\w\$ '
|
|
||||||
EOF
|
|
||||||
cat <<'EOF' | sudo tee "$bash_profile" >/dev/null
|
|
||||||
if [ -f ~/.bashrc ]; then
|
|
||||||
. ~/.bashrc
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
cat <<'EOF' | sudo tee "$profile_prompt" >/dev/null
|
|
||||||
case "$-" in
|
|
||||||
*i*) ;;
|
|
||||||
*) return 0 2>/dev/null || exit 0 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ -n "${BASH_VERSION:-}" ]; then
|
|
||||||
PS1='\u@\h:\w\$ '
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
sudo chmod 0644 "$bashrc" "$bash_profile" "$profile_prompt"
|
|
||||||
}
|
|
||||||
|
|
||||||
install_guest_tools() {
|
|
||||||
local profile_mise="$ROOT_MOUNT/etc/profile.d/mise.sh"
|
|
||||||
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/etc/profile.d"
|
|
||||||
if [[ -r /etc/resolv.conf ]]; then
|
|
||||||
sudo install -m 0644 /etc/resolv.conf "$ROOT_MOUNT/etc/resolv.conf"
|
|
||||||
fi
|
|
||||||
|
|
||||||
sudo env HOME=/root PATH=/usr/local/bin:/usr/bin:/bin chroot "$ROOT_MOUNT" /bin/bash -se <<EOF
|
|
||||||
set -euo pipefail
|
|
||||||
curl -fsSL https://mise.run | MISE_INSTALL_PATH="$MISE_INSTALL_PATH" MISE_VERSION="$MISE_VERSION" sh
|
|
||||||
"$MISE_INSTALL_PATH" use -g "$NODE_TOOL"
|
|
||||||
"$MISE_INSTALL_PATH" use -g "$OPENCODE_TOOL"
|
|
||||||
"$MISE_INSTALL_PATH" use -g "$CLAUDE_CODE_TOOL"
|
|
||||||
"$MISE_INSTALL_PATH" use -g "$PI_TOOL"
|
|
||||||
"$MISE_INSTALL_PATH" reshim
|
|
||||||
if [[ ! -e /root/.local/share/mise/shims/node ]]; then
|
|
||||||
echo "node shim not found after mise install" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -e /root/.local/share/mise/shims/npm ]]; then
|
|
||||||
echo "npm shim not found after mise install" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -e /root/.local/share/mise/shims/opencode ]]; then
|
|
||||||
echo "opencode shim not found after mise install" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -e /root/.local/share/mise/shims/claude ]]; then
|
|
||||||
echo "claude shim not found after mise install" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -e /root/.local/share/mise/shims/pi ]]; then
|
|
||||||
echo "pi shim not found after mise install" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
ln -snf /root/.local/share/mise/shims/node /usr/local/bin/node
|
|
||||||
ln -snf /root/.local/share/mise/shims/npm /usr/local/bin/npm
|
|
||||||
ln -snf /root/.local/share/mise/shims/opencode /usr/local/bin/opencode
|
|
||||||
ln -snf /root/.local/share/mise/shims/claude /usr/local/bin/claude
|
|
||||||
ln -snf /root/.local/share/mise/shims/pi /usr/local/bin/pi
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat <<'EOF' | sudo tee "$profile_mise" >/dev/null
|
|
||||||
if [ -n "${BASH_VERSION:-}" ] && [ -z "${BANGER_MISE_ACTIVATED:-}" ] && [ -x '/usr/local/bin/mise' ]; then
|
|
||||||
export BANGER_MISE_ACTIVATED=1
|
|
||||||
eval "$(/usr/local/bin/mise activate bash)"
|
|
||||||
fi
|
|
||||||
EOF
|
|
||||||
sudo chmod 0644 "$profile_mise"
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [[ -n "${ROOT_MOUNT:-}" ]] && command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$ROOT_MOUNT"; then
|
|
||||||
sudo umount "$ROOT_MOUNT" || true
|
|
||||||
fi
|
|
||||||
if [[ "${BUILD_DONE:-0}" != "1" ]]; then
|
|
||||||
rm -f "${OUT_ROOTFS:-}" "${WORK_SEED:-}" "${OUT_ROOTFS:-}.packages.sha256"
|
|
||||||
fi
|
|
||||||
if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR:-}" ]]; then
|
|
||||||
rm -rf "$TMP_DIR"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
MANUAL_DIR="${BANGER_MANUAL_DIR:-$REPO_ROOT/build/manual}"
|
|
||||||
BANGER_BIN="$(resolve_banger_bin)"
|
|
||||||
SSH_KEY="$("$BANGER_BIN" internal ssh-key-path)"
|
|
||||||
OUT_ROOTFS="$MANUAL_DIR/rootfs-void.ext4"
|
|
||||||
SIZE_SPEC="4G"
|
|
||||||
MIRROR="https://repo-default.voidlinux.org"
|
|
||||||
ARCH="x86_64"
|
|
||||||
MISE_VERSION="v2025.12.0"
|
|
||||||
MISE_INSTALL_PATH="/usr/local/bin/mise"
|
|
||||||
NODE_TOOL="node@22"
|
|
||||||
OPENCODE_TOOL="github:anomalyco/opencode"
|
|
||||||
CLAUDE_CODE_TOOL="npm:@anthropic-ai/claude-code"
|
|
||||||
PI_TOOL="npm:@mariozechner/pi-coding-agent"
|
|
||||||
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=""
|
|
||||||
VOID_KERNEL_MODULES_DIR="$(find_latest_module_dir "$MANUAL_DIR/void-kernel/lib/modules" || true)"
|
|
||||||
VSOCK_AGENT="$("$BANGER_BIN" internal vsock-agent-path)"
|
|
||||||
if [[ -n "$VOID_KERNEL_MODULES_DIR" ]]; then
|
|
||||||
MODULES_DIR="$VOID_KERNEL_MODULES_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--out)
|
|
||||||
OUT_ROOTFS="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--size)
|
|
||||||
SIZE_SPEC="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--mirror)
|
|
||||||
MIRROR="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--arch)
|
|
||||||
ARCH="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log "unknown option: $1"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
MIRROR="$(normalize_mirror "$MIRROR")"
|
|
||||||
REPO_URL="$MIRROR/current"
|
|
||||||
STATIC_ARCHIVE_URL="$MIRROR/static/xbps-static-latest.x86_64-musl.tar.xz"
|
|
||||||
|
|
||||||
if [[ "$ARCH" != "x86_64" ]]; then
|
|
||||||
log "unsupported arch: $ARCH"
|
|
||||||
log "this experimental builder currently supports only x86_64-glibc"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$MODULES_DIR" || ! -d "$MODULES_DIR" ]]; then
|
|
||||||
log "modules dir not found; run 'make void-kernel' first"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -x "$VSOCK_AGENT" ]]; then
|
|
||||||
log "vsock agent not found or not executable: $VSOCK_AGENT"
|
|
||||||
log "run 'make build'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$GUESTNET_BOOTSTRAP_SCRIPT" ]]; then
|
|
||||||
log "guest network bootstrap script not found: $GUESTNET_BOOTSTRAP_SCRIPT"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$GUESTNET_VOID_CORE_SERVICE" ]]; then
|
|
||||||
log "guest network core-service shim not found: $GUESTNET_VOID_CORE_SERVICE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -e "$OUT_ROOTFS" ]]; then
|
|
||||||
log "output rootfs already exists: $OUT_ROOTFS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
require_command curl
|
|
||||||
require_command tar
|
|
||||||
require_command sudo
|
|
||||||
require_command mkfs.ext4
|
|
||||||
require_command ssh-keygen
|
|
||||||
require_command mount
|
|
||||||
require_command umount
|
|
||||||
require_command install
|
|
||||||
require_command find
|
|
||||||
require_command awk
|
|
||||||
require_command sed
|
|
||||||
require_command sha256sum
|
|
||||||
require_command truncate
|
|
||||||
require_command mountpoint
|
|
||||||
|
|
||||||
VOID_PACKAGES=()
|
|
||||||
if ! load_package_preset void VOID_PACKAGES; then
|
|
||||||
log "void package preset is empty"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! PACKAGES_HASH="$(printf '%s\n' "${VOID_PACKAGES[@]}" | sha256sum | awk '{print $1}')"; then
|
|
||||||
log "failed to hash package preset"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ! SIZE_BYTES="$(parse_size "$SIZE_SPEC")"; then
|
|
||||||
log "invalid size: $SIZE_SPEC"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$OUT_ROOTFS" == *.ext4 ]]; then
|
|
||||||
WORK_SEED="${OUT_ROOTFS%.ext4}.work-seed.ext4"
|
|
||||||
else
|
|
||||||
WORK_SEED="${OUT_ROOTFS}.work-seed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
TMP_DIR="$(mktemp -d -t banger-void-rootfs-XXXXXX)"
|
|
||||||
STATIC_DIR="$TMP_DIR/static"
|
|
||||||
ROOT_MOUNT="$TMP_DIR/rootfs"
|
|
||||||
STATIC_ARCHIVE="$TMP_DIR/xbps-static.tar.xz"
|
|
||||||
BUILD_DONE=0
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
mkdir -p "$STATIC_DIR" "$ROOT_MOUNT"
|
|
||||||
|
|
||||||
log "downloading static XBPS from $STATIC_ARCHIVE_URL"
|
|
||||||
curl -fsSL "$STATIC_ARCHIVE_URL" -o "$STATIC_ARCHIVE"
|
|
||||||
tar -xf "$STATIC_ARCHIVE" -C "$STATIC_DIR"
|
|
||||||
|
|
||||||
XBPS_INSTALL="$(find_static_binary xbps-install)"
|
|
||||||
XBPS_QUERY="$(find_static_binary xbps-query)"
|
|
||||||
STATIC_KEYS_DIR="$(find_static_keys_dir)"
|
|
||||||
|
|
||||||
if [[ -z "$XBPS_INSTALL" || ! -x "$XBPS_INSTALL" ]]; then
|
|
||||||
log "failed to locate xbps-install in the static archive"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -z "$STATIC_KEYS_DIR" || ! -d "$STATIC_KEYS_DIR" ]]; then
|
|
||||||
log "failed to locate Void repository keys in the static archive"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "creating $OUT_ROOTFS ($SIZE_SPEC)"
|
|
||||||
truncate -s "$SIZE_BYTES" "$OUT_ROOTFS"
|
|
||||||
mkfs.ext4 -F -m 0 -L banger-void-root "$OUT_ROOTFS" >/dev/null
|
|
||||||
sudo mount -o loop "$OUT_ROOTFS" "$ROOT_MOUNT"
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/var/db/xbps/keys"
|
|
||||||
sudo cp -a "$STATIC_KEYS_DIR/." "$ROOT_MOUNT/var/db/xbps/keys/"
|
|
||||||
|
|
||||||
log "installing Void packages into the rootfs"
|
|
||||||
sudo env XBPS_ARCH="$ARCH" "$XBPS_INSTALL" -S -y -r "$ROOT_MOUNT" -R "$REPO_URL" "${VOID_PACKAGES[@]}"
|
|
||||||
|
|
||||||
if [[ -n "$XBPS_QUERY" && -x "$XBPS_QUERY" ]]; then
|
|
||||||
log "installed package set:"
|
|
||||||
sudo env XBPS_ARCH="$ARCH" "$XBPS_QUERY" -r "$ROOT_MOUNT" -l | awk '/^ii/ {print " " $2}' || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$VOID_KERNEL_MODULES_DIR" ]]; then
|
|
||||||
log "copying staged Void kernel modules into the guest"
|
|
||||||
else
|
|
||||||
log "copying bundled kernel modules into the guest"
|
|
||||||
fi
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/lib/modules"
|
|
||||||
sudo cp -a "$MODULES_DIR" "$ROOT_MOUNT/lib/modules/"
|
|
||||||
|
|
||||||
log "installing the guest-side vsock agent"
|
|
||||||
sudo mkdir -p "$ROOT_MOUNT/usr/local/bin"
|
|
||||||
sudo install -m 0755 "$VSOCK_AGENT" "$ROOT_MOUNT/usr/local/bin/banger-vsock-agent"
|
|
||||||
|
|
||||||
log "preparing SSH and runit services"
|
|
||||||
install_guest_network_bootstrap
|
|
||||||
ensure_sshd_include
|
|
||||||
enable_sshd_service
|
|
||||||
install_vsock_service
|
|
||||||
configure_docker_bootstrap
|
|
||||||
enable_docker_service
|
|
||||||
normalize_root_shell
|
|
||||||
configure_root_bash_prompt
|
|
||||||
log "installing guest tools"
|
|
||||||
install_guest_tools
|
|
||||||
install_opencode_service
|
|
||||||
install_root_authorized_key
|
|
||||||
sudo touch "$ROOT_MOUNT/etc/fstab" "$ROOT_MOUNT/etc/hostname"
|
|
||||||
sudo chroot "$ROOT_MOUNT" /usr/bin/ssh-keygen -A
|
|
||||||
|
|
||||||
log "removing bulky caches, docs, and stale installer artifacts from the experimental image"
|
|
||||||
sudo rm -rf \
|
|
||||||
"$ROOT_MOUNT/var/cache/xbps" \
|
|
||||||
"$ROOT_MOUNT/usr/share/doc" \
|
|
||||||
"$ROOT_MOUNT/usr/share/info" \
|
|
||||||
"$ROOT_MOUNT/usr/share/man"
|
|
||||||
sudo rm -f \
|
|
||||||
"$ROOT_MOUNT/root/get-docker" \
|
|
||||||
"$ROOT_MOUNT/root/get-docker.sh" \
|
|
||||||
"$ROOT_MOUNT/root/.cache/opencode" \
|
|
||||||
"$ROOT_MOUNT/tmp/get-docker" \
|
|
||||||
"$ROOT_MOUNT/tmp/get-docker.sh"
|
|
||||||
sudo rm -rf \
|
|
||||||
"$ROOT_MOUNT/root/.cache/mise" \
|
|
||||||
"$ROOT_MOUNT/root/.local/share/mise/downloads" \
|
|
||||||
"$ROOT_MOUNT/root/.local/share/mise/tmp"
|
|
||||||
|
|
||||||
sudo umount "$ROOT_MOUNT"
|
|
||||||
|
|
||||||
write_rootfs_manifest_metadata "$OUT_ROOTFS" "$PACKAGES_HASH"
|
|
||||||
|
|
||||||
log "building work-seed $WORK_SEED"
|
|
||||||
"$BANGER_BIN" internal work-seed --rootfs "$OUT_ROOTFS" --out "$WORK_SEED"
|
|
||||||
|
|
||||||
BUILD_DONE=1
|
|
||||||
log "built experimental Void rootfs: $OUT_ROOTFS"
|
|
||||||
log "built experimental Void work-seed: $WORK_SEED"
|
|
||||||
log "use examples/void.config.toml as the local config override template"
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[make-rootfs] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: ./scripts/make-rootfs.sh --kernel <path> [--initrd <path>] [--modules <dir>] [--size <size>] [--base-rootfs <path>]
|
|
||||||
|
|
||||||
Builds build/manual/rootfs-docker.ext4 using scripts/customize.sh. If
|
|
||||||
--base-rootfs is omitted, the first existing file is used:
|
|
||||||
./build/manual/rootfs-base.ext4
|
|
||||||
./ubuntu-noble-rootfs/rootfs.ext4
|
|
||||||
./ubuntu-lts/rootfs.ext4
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
MANUAL_DIR="${BANGER_MANUAL_DIR:-$REPO_ROOT/build/manual}"
|
|
||||||
OUT_ROOTFS="$MANUAL_DIR/rootfs-docker.ext4"
|
|
||||||
SIZE_SPEC="6G"
|
|
||||||
BASE_ROOTFS=""
|
|
||||||
KERNEL_PATH=""
|
|
||||||
INITRD_PATH=""
|
|
||||||
MODULES_DIR=""
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--size)
|
|
||||||
SIZE_SPEC="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--base-rootfs)
|
|
||||||
BASE_ROOTFS="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--kernel)
|
|
||||||
KERNEL_PATH="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--initrd)
|
|
||||||
INITRD_PATH="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--modules)
|
|
||||||
MODULES_DIR="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log "unknown option: $1"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$BASE_ROOTFS" ]]; then
|
|
||||||
if [[ -f "$MANUAL_DIR/rootfs-base.ext4" ]]; then
|
|
||||||
BASE_ROOTFS="$MANUAL_DIR/rootfs-base.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; pass --base-rootfs"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$KERNEL_PATH" ]]; then
|
|
||||||
log "kernel path is required; pass --kernel"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$MANUAL_DIR"
|
|
||||||
|
|
||||||
log "building $OUT_ROOTFS from $BASE_ROOTFS"
|
|
||||||
args=(
|
|
||||||
"$SCRIPT_DIR/customize.sh"
|
|
||||||
"$BASE_ROOTFS"
|
|
||||||
--out "$OUT_ROOTFS"
|
|
||||||
--size "$SIZE_SPEC"
|
|
||||||
--kernel "$KERNEL_PATH"
|
|
||||||
--docker
|
|
||||||
)
|
|
||||||
if [[ -n "$INITRD_PATH" ]]; then
|
|
||||||
args+=(--initrd "$INITRD_PATH")
|
|
||||||
fi
|
|
||||||
if [[ -n "$MODULES_DIR" ]]; then
|
|
||||||
args+=(--modules "$MODULES_DIR")
|
|
||||||
fi
|
|
||||||
exec "${args[@]}"
|
|
||||||
|
|
@ -1,386 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[make-void-kernel] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
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 ./build/manual/void-kernel for
|
|
||||||
the
|
|
||||||
experimental Void guest flow.
|
|
||||||
|
|
||||||
Defaults:
|
|
||||||
--out-dir ./build/manual/void-kernel
|
|
||||||
--mirror https://repo-default.voidlinux.org
|
|
||||||
--arch x86_64
|
|
||||||
--kernel-package linux6.12
|
|
||||||
|
|
||||||
The staged output contains:
|
|
||||||
boot/vmlinux-<version> Firecracker-usable kernel extracted from vmlinuz
|
|
||||||
boot/vmlinuz-<version> Raw distro boot image from the Void package
|
|
||||||
boot/initramfs-<version>.img Matching initramfs generated with dracut
|
|
||||||
boot/config-<version> Void kernel config
|
|
||||||
lib/modules/<version>/ Matching kernel modules tree
|
|
||||||
|
|
||||||
If --print-register-flags is passed, the script does not download anything. It
|
|
||||||
prints the banger image register flags for an existing staged Void kernel.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
require_command() {
|
|
||||||
local name="$1"
|
|
||||||
command -v "$name" >/dev/null 2>&1 || {
|
|
||||||
log "required command not found: $name"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
normalize_mirror() {
|
|
||||||
local mirror="${1%/}"
|
|
||||||
mirror="${mirror%/current}"
|
|
||||||
mirror="${mirror%/static}"
|
|
||||||
printf '%s\n' "$mirror"
|
|
||||||
}
|
|
||||||
|
|
||||||
find_static_binary() {
|
|
||||||
local name="$1"
|
|
||||||
find "$STATIC_DIR" -type f \( -name "$name" -o -name "$name.static" \) -perm -u+x | sort | head -n 1
|
|
||||||
}
|
|
||||||
|
|
||||||
find_static_keys_dir() {
|
|
||||||
find "$STATIC_DIR" -type d -path '*/var/db/xbps/keys' | sort | head -n 1
|
|
||||||
}
|
|
||||||
|
|
||||||
find_latest_matching() {
|
|
||||||
local dir="$1"
|
|
||||||
local pattern="$2"
|
|
||||||
if [[ ! -d "$dir" ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
find "$dir" -maxdepth 1 -type f -name "$pattern" | sort | tail -n 1
|
|
||||||
}
|
|
||||||
|
|
||||||
find_latest_module_dir() {
|
|
||||||
local root="$1"
|
|
||||||
if [[ ! -d "$root" ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
find "$root" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1
|
|
||||||
}
|
|
||||||
|
|
||||||
print_register_flags() {
|
|
||||||
local kernel=""
|
|
||||||
local initrd=""
|
|
||||||
local modules=""
|
|
||||||
|
|
||||||
kernel="$(find_latest_matching "$OUT_DIR/boot" 'vmlinux-*' || true)"
|
|
||||||
initrd="$(find_latest_matching "$OUT_DIR/boot" 'initramfs-*' || true)"
|
|
||||||
modules="$(find_latest_module_dir "$OUT_DIR/lib/modules" || true)"
|
|
||||||
|
|
||||||
if [[ -z "$kernel" || -z "$modules" ]]; then
|
|
||||||
log "staged Void kernel not found under $OUT_DIR"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf -- '--kernel %q ' "$kernel"
|
|
||||||
if [[ -n "$initrd" ]]; then
|
|
||||||
printf -- '--initrd %q ' "$initrd"
|
|
||||||
fi
|
|
||||||
printf -- '--modules %q\n' "$modules"
|
|
||||||
}
|
|
||||||
|
|
||||||
check_elf() {
|
|
||||||
local path="$1"
|
|
||||||
readelf -h "$path" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_stage_root_layout() {
|
|
||||||
mkdir -p "$STAGE_ROOT/usr"
|
|
||||||
|
|
||||||
if [[ ! -e "$STAGE_ROOT/bin" ]]; then
|
|
||||||
ln -snf usr/bin "$STAGE_ROOT/bin"
|
|
||||||
fi
|
|
||||||
if [[ ! -e "$STAGE_ROOT/sbin" ]]; then
|
|
||||||
ln -snf usr/bin "$STAGE_ROOT/sbin"
|
|
||||||
fi
|
|
||||||
if [[ ! -e "$STAGE_ROOT/usr/sbin" ]]; then
|
|
||||||
ln -snf bin "$STAGE_ROOT/usr/sbin"
|
|
||||||
fi
|
|
||||||
if [[ ! -e "$STAGE_ROOT/lib" ]]; then
|
|
||||||
ln -snf usr/lib "$STAGE_ROOT/lib"
|
|
||||||
fi
|
|
||||||
if [[ ! -e "$STAGE_ROOT/lib64" ]]; then
|
|
||||||
ln -snf usr/lib "$STAGE_ROOT/lib64"
|
|
||||||
fi
|
|
||||||
if [[ ! -e "$STAGE_ROOT/usr/lib64" ]]; then
|
|
||||||
ln -snf lib "$STAGE_ROOT/usr/lib64"
|
|
||||||
fi
|
|
||||||
if [[ -x "$STAGE_ROOT/usr/bin/udevd" ]]; then
|
|
||||||
mkdir -p "$STAGE_ROOT/usr/lib/udev" "$STAGE_ROOT/usr/lib/systemd"
|
|
||||||
if [[ ! -e "$STAGE_ROOT/usr/lib/udev/udevd" ]]; then
|
|
||||||
ln -snf ../../bin/udevd "$STAGE_ROOT/usr/lib/udev/udevd"
|
|
||||||
fi
|
|
||||||
if [[ ! -e "$STAGE_ROOT/usr/lib/systemd/systemd-udevd" ]]; then
|
|
||||||
ln -snf ../../bin/udevd "$STAGE_ROOT/usr/lib/systemd/systemd-udevd"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
sync_host_dracut_tree() {
|
|
||||||
if [[ ! -d /usr/lib/dracut ]]; then
|
|
||||||
log "host dracut support files not found under /usr/lib/dracut"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
rm -rf "$STAGE_ROOT/usr/lib/dracut"
|
|
||||||
mkdir -p "$STAGE_ROOT/usr/lib"
|
|
||||||
cp -a /usr/lib/dracut "$STAGE_ROOT/usr/lib/dracut"
|
|
||||||
}
|
|
||||||
|
|
||||||
build_initramfs() {
|
|
||||||
local kver="$1"
|
|
||||||
local modules_dir="$2"
|
|
||||||
local out="$3"
|
|
||||||
local config_dir="$TMP_DIR/dracut.conf.d"
|
|
||||||
local tmpdir="$TMP_DIR/dracut-tmp"
|
|
||||||
local force_drivers="virtio virtio_ring virtio_mmio virtio_blk virtio_net virtio_console ext4 vsock vmw_vsock_virtio_transport"
|
|
||||||
|
|
||||||
mkdir -p "$config_dir" "$tmpdir"
|
|
||||||
ensure_stage_root_layout
|
|
||||||
sync_host_dracut_tree
|
|
||||||
|
|
||||||
log "generating initramfs for kernel $kver with host dracut against the staged Void sysroot"
|
|
||||||
env dracutbasedir="/usr/lib/dracut" dracut \
|
|
||||||
--force \
|
|
||||||
--kver "$kver" \
|
|
||||||
--sysroot "$STAGE_ROOT" \
|
|
||||||
--kmoddir "$modules_dir" \
|
|
||||||
--conf /dev/null \
|
|
||||||
--confdir "$config_dir" \
|
|
||||||
--tmpdir "$tmpdir" \
|
|
||||||
--no-hostonly \
|
|
||||||
--filesystems "ext4" \
|
|
||||||
--force-drivers "$force_drivers" \
|
|
||||||
--gzip \
|
|
||||||
"$out"
|
|
||||||
}
|
|
||||||
|
|
||||||
extract_vmlinux() {
|
|
||||||
local image="$1"
|
|
||||||
local out="$2"
|
|
||||||
local tmp="$TMP_DIR/vmlinux.extract"
|
|
||||||
|
|
||||||
if check_elf "$image"; then
|
|
||||||
install -m 0644 "$image" "$out"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
try_decompress() {
|
|
||||||
local header="$1"
|
|
||||||
local marker="$2"
|
|
||||||
local command="$3"
|
|
||||||
local pos=""
|
|
||||||
|
|
||||||
while IFS= read -r pos; do
|
|
||||||
[[ -n "$pos" ]] || continue
|
|
||||||
pos="${pos%%:*}"
|
|
||||||
tail -c+"$pos" "$image" | eval "$command" >"$tmp" 2>/dev/null || true
|
|
||||||
if check_elf "$tmp"; then
|
|
||||||
install -m 0644 "$tmp" "$out"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done < <(tr "$header\n$marker" "\n$marker=" < "$image" | grep -abo "^$marker" || true)
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
try_decompress '\037\213\010' "xy" "gunzip" && return 0
|
|
||||||
try_decompress '\3757zXZ\000' "abcde" "unxz" && return 0
|
|
||||||
try_decompress "BZh" "xy" "bunzip2" && return 0
|
|
||||||
try_decompress '\135\000\000\000' "xxx" "unlzma" && return 0
|
|
||||||
try_decompress '\002!L\030' "xxx" "lz4 -d" && return 0
|
|
||||||
try_decompress '(\265/\375' "xxx" "unzstd" && return 0
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_kernel_package_file() {
|
|
||||||
local escaped_name=""
|
|
||||||
escaped_name="$(printf '%s\n' "$KERNEL_PACKAGE" | sed 's/[.[\*^$()+?{|]/\\&/g')"
|
|
||||||
|
|
||||||
curl -fsSL "$REPO_URL/" |
|
|
||||||
grep -o "${escaped_name}-[0-9][^\" >]*\\.${ARCH}\\.xbps" |
|
|
||||||
sort -u |
|
|
||||||
tail -n 1
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR:-}" ]]; then
|
|
||||||
rm -rf "$TMP_DIR"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
MANUAL_DIR="${BANGER_MANUAL_DIR:-$REPO_ROOT/build/manual}"
|
|
||||||
OUT_DIR="$MANUAL_DIR/void-kernel"
|
|
||||||
MIRROR="https://repo-default.voidlinux.org"
|
|
||||||
ARCH="x86_64"
|
|
||||||
KERNEL_PACKAGE="linux6.12"
|
|
||||||
PRINT_REGISTER_FLAGS=0
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--out-dir)
|
|
||||||
OUT_DIR="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--mirror)
|
|
||||||
MIRROR="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--arch)
|
|
||||||
ARCH="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--kernel-package)
|
|
||||||
KERNEL_PACKAGE="${2:-}"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--print-register-flags)
|
|
||||||
PRINT_REGISTER_FLAGS=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log "unknown option: $1"
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
MIRROR="$(normalize_mirror "$MIRROR")"
|
|
||||||
REPO_URL="$MIRROR/current"
|
|
||||||
STATIC_ARCHIVE_URL="$MIRROR/static/xbps-static-latest.x86_64-musl.tar.xz"
|
|
||||||
|
|
||||||
if [[ "$PRINT_REGISTER_FLAGS" == "1" ]]; then
|
|
||||||
print_register_flags
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$ARCH" != "x86_64" ]]; then
|
|
||||||
log "unsupported arch: $ARCH"
|
|
||||||
log "this experimental downloader currently supports only x86_64"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
mkdir -p "$(dirname "$OUT_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"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
require_command curl
|
|
||||||
require_command tar
|
|
||||||
require_command cp
|
|
||||||
require_command find
|
|
||||||
require_command grep
|
|
||||||
require_command cut
|
|
||||||
require_command readelf
|
|
||||||
require_command file
|
|
||||||
require_command install
|
|
||||||
require_command tail
|
|
||||||
require_command xz
|
|
||||||
require_command gzip
|
|
||||||
require_command bzip2
|
|
||||||
require_command dracut
|
|
||||||
|
|
||||||
TMP_DIR="$(mktemp -d -t banger-void-kernel-XXXXXX)"
|
|
||||||
STATIC_DIR="$TMP_DIR/static"
|
|
||||||
STAGE_ROOT="$TMP_DIR/root"
|
|
||||||
STAGE_OUT="$TMP_DIR/out"
|
|
||||||
STATIC_ARCHIVE="$TMP_DIR/xbps-static.tar.xz"
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
mkdir -p "$STATIC_DIR" "$STAGE_ROOT/var/db/xbps/keys" "$STAGE_OUT/boot" "$STAGE_OUT/lib/modules"
|
|
||||||
|
|
||||||
log "downloading static XBPS from $STATIC_ARCHIVE_URL"
|
|
||||||
curl -fsSL "$STATIC_ARCHIVE_URL" -o "$STATIC_ARCHIVE"
|
|
||||||
tar -xf "$STATIC_ARCHIVE" -C "$STATIC_DIR"
|
|
||||||
|
|
||||||
XBPS_INSTALL="$(find_static_binary xbps-install)"
|
|
||||||
STATIC_KEYS_DIR="$(find_static_keys_dir)"
|
|
||||||
if [[ -z "$XBPS_INSTALL" || ! -x "$XBPS_INSTALL" ]]; then
|
|
||||||
log "failed to locate xbps-install in the static archive"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ -z "$STATIC_KEYS_DIR" || ! -d "$STATIC_KEYS_DIR" ]]; then
|
|
||||||
log "failed to locate Void repository keys in the static archive"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cp -a "$STATIC_KEYS_DIR/." "$STAGE_ROOT/var/db/xbps/keys/"
|
|
||||||
|
|
||||||
KERNEL_PACKAGE_FILE="$(resolve_kernel_package_file)"
|
|
||||||
if [[ -z "$KERNEL_PACKAGE_FILE" ]]; then
|
|
||||||
log "failed to resolve a package file for $KERNEL_PACKAGE in $REPO_URL"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "staging $KERNEL_PACKAGE_FILE into a temporary root"
|
|
||||||
env XBPS_ARCH="$ARCH" "$XBPS_INSTALL" -S -y -U -r "$STAGE_ROOT" -R "$REPO_URL" linux-base "$KERNEL_PACKAGE" dracut eudev >/dev/null
|
|
||||||
|
|
||||||
VMLINUX_RAW="$(find_latest_matching "$STAGE_ROOT/boot" 'vmlinuz-*' || true)"
|
|
||||||
KERNEL_CONFIG="$(find_latest_matching "$STAGE_ROOT/boot" 'config-*' || true)"
|
|
||||||
MODULES_DIR="$(find_latest_module_dir "$STAGE_ROOT/usr/lib/modules" || true)"
|
|
||||||
KERNEL_VERSION="$(basename "$MODULES_DIR")"
|
|
||||||
INITRAMFS_NAME="initramfs-${KERNEL_VERSION}.img"
|
|
||||||
INITRAMFS_RAW="$STAGE_OUT/boot/$INITRAMFS_NAME"
|
|
||||||
|
|
||||||
if [[ -z "$VMLINUX_RAW" || -z "$KERNEL_CONFIG" || -z "$MODULES_DIR" ]]; then
|
|
||||||
log "staged Void kernel is missing expected boot artifacts"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -x "$STAGE_ROOT/usr/bin/udevd" ]]; then
|
|
||||||
log "staged Void sysroot is missing /usr/bin/udevd after package install"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VMLINUX_BASE="$(basename "$VMLINUX_RAW")"
|
|
||||||
VMLINUX_OUT="$STAGE_OUT/boot/vmlinux-${VMLINUX_BASE#vmlinuz-}"
|
|
||||||
install -m 0644 "$VMLINUX_RAW" "$STAGE_OUT/boot/$VMLINUX_BASE"
|
|
||||||
install -m 0644 "$KERNEL_CONFIG" "$STAGE_OUT/boot/$(basename "$KERNEL_CONFIG")"
|
|
||||||
build_initramfs "$KERNEL_VERSION" "$MODULES_DIR" "$INITRAMFS_RAW"
|
|
||||||
cp -a "$MODULES_DIR" "$STAGE_OUT/lib/modules/"
|
|
||||||
|
|
||||||
log "extracting Firecracker kernel from $(basename "$VMLINUX_RAW")"
|
|
||||||
if ! extract_vmlinux "$VMLINUX_RAW" "$VMLINUX_OUT"; then
|
|
||||||
log "failed to extract an uncompressed vmlinux from $VMLINUX_RAW"
|
|
||||||
log "raw kernel image type: $(file -b "$VMLINUX_RAW")"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat >"$STAGE_OUT/metadata.json" <<EOF
|
|
||||||
{
|
|
||||||
"package": "$KERNEL_PACKAGE_FILE",
|
|
||||||
"kernel_path": "$OUT_DIR/boot/$(basename "$VMLINUX_OUT")",
|
|
||||||
"raw_kernel_path": "$OUT_DIR/boot/$VMLINUX_BASE",
|
|
||||||
"config_path": "$OUT_DIR/boot/$(basename "$KERNEL_CONFIG")",
|
|
||||||
"initrd_path": "$OUT_DIR/boot/$INITRAMFS_NAME",
|
|
||||||
"modules_dir": "$OUT_DIR/lib/modules/$(basename "$MODULES_DIR")"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
mv "$STAGE_OUT" "$OUT_DIR"
|
|
||||||
|
|
||||||
log "staged Void kernel artifacts in $OUT_DIR"
|
|
||||||
log "kernel image: $OUT_DIR/boot/$(basename "$VMLINUX_OUT")"
|
|
||||||
log "initrd image: $OUT_DIR/boot/$INITRAMFS_NAME"
|
|
||||||
log "modules dir: $OUT_DIR/lib/modules/$(basename "$MODULES_DIR")"
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[register-alpine-image] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
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; build it first with 'make build' or set BANGER_BIN"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
RUNTIME_DIR="${BANGER_MANUAL_DIR:-$REPO_ROOT/build/manual}"
|
|
||||||
IMAGE_NAME="${ALPINE_IMAGE_NAME:-alpine}"
|
|
||||||
KERNEL_REF="${ALPINE_KERNEL_REF:-$IMAGE_NAME}"
|
|
||||||
BANGER_BIN="$(resolve_banger_bin)"
|
|
||||||
ROOTFS="$RUNTIME_DIR/rootfs-alpine.ext4"
|
|
||||||
WORK_SEED="$RUNTIME_DIR/rootfs-alpine.work-seed.ext4"
|
|
||||||
|
|
||||||
if [[ ! -f "$ROOTFS" ]]; then
|
|
||||||
log "missing Alpine rootfs: $ROOTFS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$WORK_SEED" ]]; then
|
|
||||||
log "missing Alpine work-seed: $WORK_SEED"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -d "$RUNTIME_DIR/alpine-kernel" ]]; then
|
|
||||||
log "missing staged Alpine kernel artifacts: $RUNTIME_DIR/alpine-kernel"
|
|
||||||
log "run 'make alpine-kernel' before registering $IMAGE_NAME"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "importing Alpine kernel from $RUNTIME_DIR/alpine-kernel as $KERNEL_REF"
|
|
||||||
"$BANGER_BIN" kernel import "$KERNEL_REF" \
|
|
||||||
--from "$RUNTIME_DIR/alpine-kernel" \
|
|
||||||
--distro alpine \
|
|
||||||
--arch x86_64
|
|
||||||
|
|
||||||
log "registering image $IMAGE_NAME with kernel-ref $KERNEL_REF"
|
|
||||||
"$BANGER_BIN" image register \
|
|
||||||
--name "$IMAGE_NAME" \
|
|
||||||
--rootfs "$ROOTFS" \
|
|
||||||
--work-seed "$WORK_SEED" \
|
|
||||||
--docker \
|
|
||||||
--kernel-ref "$KERNEL_REF"
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[register-void-image] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
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; build it first with 'make build' or set BANGER_BIN"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
RUNTIME_DIR="${BANGER_MANUAL_DIR:-$REPO_ROOT/build/manual}"
|
|
||||||
IMAGE_NAME="${VOID_IMAGE_NAME:-void}"
|
|
||||||
KERNEL_REF="${VOID_KERNEL_REF:-$IMAGE_NAME}"
|
|
||||||
BANGER_BIN="$(resolve_banger_bin)"
|
|
||||||
ROOTFS="$RUNTIME_DIR/rootfs-void.ext4"
|
|
||||||
WORK_SEED="$RUNTIME_DIR/rootfs-void.work-seed.ext4"
|
|
||||||
|
|
||||||
if [[ ! -f "$ROOTFS" ]]; then
|
|
||||||
log "missing Void rootfs: $ROOTFS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$WORK_SEED" ]]; then
|
|
||||||
log "missing Void work-seed: $WORK_SEED"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ ! -d "$RUNTIME_DIR/void-kernel" ]]; then
|
|
||||||
log "missing staged Void kernel artifacts: $RUNTIME_DIR/void-kernel"
|
|
||||||
log "run 'make void-kernel' before registering $IMAGE_NAME"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "importing Void kernel from $RUNTIME_DIR/void-kernel as $KERNEL_REF"
|
|
||||||
"$BANGER_BIN" kernel import "$KERNEL_REF" \
|
|
||||||
--from "$RUNTIME_DIR/void-kernel" \
|
|
||||||
--distro void \
|
|
||||||
--arch x86_64
|
|
||||||
|
|
||||||
log "registering image $IMAGE_NAME with kernel-ref $KERNEL_REF"
|
|
||||||
"$BANGER_BIN" image register \
|
|
||||||
--name "$IMAGE_NAME" \
|
|
||||||
--rootfs "$ROOTFS" \
|
|
||||||
--work-seed "$WORK_SEED" \
|
|
||||||
--kernel-ref "$KERNEL_REF"
|
|
||||||
|
|
@ -1,334 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[verify] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
DAEMON_LOG="${XDG_STATE_HOME:-$HOME/.local/state}/banger/bangerd.log"
|
|
||||||
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)"
|
|
||||||
SSH_KEY="$("$BANGER_BIN" internal ssh-key-path)"
|
|
||||||
if [[ ! -f "$SSH_KEY" ]]; then
|
|
||||||
log "ssh key not found: $SSH_KEY"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
SSH_COMMON_ARGS=(
|
|
||||||
-F /dev/null
|
|
||||||
-i "$SSH_KEY"
|
|
||||||
-o IdentitiesOnly=yes
|
|
||||||
-o BatchMode=yes
|
|
||||||
-o PreferredAuthentications=publickey
|
|
||||||
-o PasswordAuthentication=no
|
|
||||||
-o KbdInteractiveAuthentication=no
|
|
||||||
-o StrictHostKeyChecking=no
|
|
||||||
-o UserKnownHostsFile=/dev/null
|
|
||||||
)
|
|
||||||
|
|
||||||
firecracker_running() {
|
|
||||||
local pid="$1"
|
|
||||||
local api_sock="$2"
|
|
||||||
local cmdline=""
|
|
||||||
|
|
||||||
if [[ -z "$pid" || "$pid" -le 0 || -z "$api_sock" ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
if [[ ! -r "/proc/$pid/cmdline" ]]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
cmdline="$(cat "/proc/$pid/cmdline" 2>/dev/null | tr '\0' ' ' || true)"
|
|
||||||
[[ "$cmdline" == *firecracker* && "$cmdline" == *"$api_sock"* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
pooled_tap() {
|
|
||||||
local tap="$1"
|
|
||||||
[[ "$tap" == tap-pool-* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_ssh() {
|
|
||||||
local guest_ip="$1"
|
|
||||||
local deadline="$2"
|
|
||||||
|
|
||||||
while ((SECONDS < deadline)); do
|
|
||||||
if ssh "${SSH_COMMON_ARGS[@]}" -o ConnectTimeout=2 "root@${guest_ip}" "true" >/dev/null 2>&1; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_tcp() {
|
|
||||||
local host="$1"
|
|
||||||
local port="$2"
|
|
||||||
local deadline="$3"
|
|
||||||
|
|
||||||
while ((SECONDS < deadline)); do
|
|
||||||
if (exec 3<>/dev/tcp/"$host"/"$port") >/dev/null 2>&1; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh_vm_metadata() {
|
|
||||||
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')"
|
|
||||||
VM_DIR="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.vm_dir // empty')"
|
|
||||||
GUEST_IP="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.guest_ip // empty')"
|
|
||||||
API_SOCK="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.api_sock_path // empty')"
|
|
||||||
PID="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.pid // 0')"
|
|
||||||
VM_STATE="$(printf '%s\n' "$VM_JSON" | jq -r '.state // empty')"
|
|
||||||
LAST_ERROR="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.last_error // empty')"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_vm_ready() {
|
|
||||||
local deadline="$1"
|
|
||||||
|
|
||||||
while ((SECONDS < deadline)); do
|
|
||||||
if ! refresh_vm_metadata; then
|
|
||||||
sleep 1
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
if [[ "$VM_STATE" == "error" || -n "$LAST_ERROR" ]]; then
|
|
||||||
return 2
|
|
||||||
fi
|
|
||||||
if [[ -n "$API_SOCK" && "${PID:-0}" -gt 0 ]] && ! firecracker_running "$PID" "$API_SOCK"; then
|
|
||||||
return 3
|
|
||||||
fi
|
|
||||||
if [[ "$VM_STATE" == "running" && -n "$GUEST_IP" && -n "$TAP" && -n "$VM_DIR" && -n "$API_SOCK" && "${PID:-0}" -gt 0 ]]; then
|
|
||||||
if [[ -S "$API_SOCK" ]] && ip link show "$TAP" >/dev/null 2>&1; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
dump_diagnostics() {
|
|
||||||
log "diagnostics for $VM_NAME"
|
|
||||||
"$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_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
|
|
||||||
fi
|
|
||||||
if [[ -n "${TAP:-}" ]]; then
|
|
||||||
log "tap state for $TAP"
|
|
||||||
ip link show "$TAP" || true
|
|
||||||
fi
|
|
||||||
if [[ -n "${API_SOCK:-}" ]]; then
|
|
||||||
log "api socket $API_SOCK"
|
|
||||||
ls -l "$API_SOCK" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
if (( NAT_ENABLED )) && [[ -n "${UPLINK:-}" && -n "${GUEST_IP:-}" && -n "${TAP:-}" ]]; then
|
|
||||||
log "nat rules for ${GUEST_IP} via ${UPLINK}"
|
|
||||||
sudo iptables -t nat -S POSTROUTING | grep "${GUEST_IP}/32" || true
|
|
||||||
sudo iptables -S FORWARD | grep "$TAP" || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
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.
|
|
||||||
Use --image to verify a non-default image such as void.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
NAT_ENABLED=0
|
|
||||||
IMAGE_NAME=""
|
|
||||||
BOOT_TIMEOUT_SECS="${VERIFY_BOOT_TIMEOUT_SECS:-90}"
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--nat)
|
|
||||||
NAT_ENABLED=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--image)
|
|
||||||
IMAGE_NAME="${2:-}"
|
|
||||||
if [[ -z "$IMAGE_NAME" ]]; then
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
VM_NAME="verify-$(date +%s)"
|
|
||||||
VM_JSON=""
|
|
||||||
TAP=""
|
|
||||||
VM_DIR=""
|
|
||||||
GUEST_IP=""
|
|
||||||
UPLINK=""
|
|
||||||
API_SOCK=""
|
|
||||||
PID="0"
|
|
||||||
VM_STATE=""
|
|
||||||
LAST_ERROR=""
|
|
||||||
|
|
||||||
delete_vm() {
|
|
||||||
if [[ -n "${VM_NAME:-}" ]]; then
|
|
||||||
"$BANGER_BIN" vm delete "$VM_NAME"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
if [[ -n "${VM_NAME:-}" ]]; then
|
|
||||||
"$BANGER_BIN" vm delete "$VM_NAME" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
log "starting VM"
|
|
||||||
CREATE_ARGS=("$BANGER_BIN" vm create --name "$VM_NAME")
|
|
||||||
if [[ -n "$IMAGE_NAME" ]]; then
|
|
||||||
CREATE_ARGS+=(--image "$IMAGE_NAME")
|
|
||||||
fi
|
|
||||||
if (( NAT_ENABLED )); then
|
|
||||||
CREATE_ARGS+=(--nat)
|
|
||||||
fi
|
|
||||||
"${CREATE_ARGS[@]}" >/dev/null
|
|
||||||
|
|
||||||
BOOT_DEADLINE=$((SECONDS + BOOT_TIMEOUT_SECS))
|
|
||||||
|
|
||||||
log "waiting for VM runtime readiness"
|
|
||||||
if wait_for_vm_ready "$BOOT_DEADLINE"; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
status=$?
|
|
||||||
case "$status" in
|
|
||||||
2) log "vm entered an error state before becoming ready" ;;
|
|
||||||
3) log "firecracker exited before the guest became ready" ;;
|
|
||||||
*) log "vm did not become ready before timeout" ;;
|
|
||||||
esac
|
|
||||||
dump_diagnostics
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if (( NAT_ENABLED )); then
|
|
||||||
UPLINK="$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')"
|
|
||||||
if [[ -z "$UPLINK" ]]; then
|
|
||||||
log "failed to detect uplink interface"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
log "asserting NAT rules are installed"
|
|
||||||
sudo iptables -t nat -C POSTROUTING -s "${GUEST_IP}/32" -o "$UPLINK" -j MASQUERADE
|
|
||||||
sudo iptables -C FORWARD -i "$TAP" -o "$UPLINK" -j ACCEPT
|
|
||||||
sudo iptables -C FORWARD -i "$UPLINK" -o "$TAP" -m state --state RELATED,ESTABLISHED -j ACCEPT
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "asserting VM is reachable via SSH"
|
|
||||||
if ! wait_for_ssh "$GUEST_IP" "$BOOT_DEADLINE"; then
|
|
||||||
log "ssh did not become ready for ${GUEST_IP}"
|
|
||||||
dump_diagnostics
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
ssh "${SSH_COMMON_ARGS[@]}" "root@${GUEST_IP}" "uname -a" >/dev/null
|
|
||||||
|
|
||||||
log "asserting opencode is available and listening in the guest"
|
|
||||||
ssh "${SSH_COMMON_ARGS[@]}" "root@${GUEST_IP}" "command -v opencode >/dev/null 2>&1 && ss -H -lntp | awk '\$4 ~ /:${OPENCODE_PORT}\$/ { found = 1 } END { exit found ? 0 : 1 }'" >/dev/null
|
|
||||||
|
|
||||||
log "asserting opencode server is reachable from the host"
|
|
||||||
if ! wait_for_tcp "$GUEST_IP" "$OPENCODE_PORT" "$BOOT_DEADLINE"; then
|
|
||||||
log "opencode server did not become reachable at ${GUEST_IP}:${OPENCODE_PORT}"
|
|
||||||
dump_diagnostics
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "asserting opencode port is reported by banger vm ports"
|
|
||||||
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
|
|
||||||
fi
|
|
||||||
|
|
||||||
if (( NAT_ENABLED )); then
|
|
||||||
log "asserting VM has outbound network access"
|
|
||||||
ssh "${SSH_COMMON_ARGS[@]}" "root@${GUEST_IP}" "curl -fsS https://example.com >/dev/null" >/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "cleaning up VM"
|
|
||||||
if ! delete_vm; then
|
|
||||||
log "vm delete failed for $VM_NAME"
|
|
||||||
dump_diagnostics
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "asserting cleanup success"
|
|
||||||
if "$BANGER_BIN" vm show "$VM_NAME" >/dev/null 2>&1; then
|
|
||||||
log "vm still exists after delete: $VM_NAME"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if ip link show "$TAP" >/dev/null 2>&1; then
|
|
||||||
if pooled_tap "$TAP"; then
|
|
||||||
log "tap returned to idle pool: $TAP"
|
|
||||||
else
|
|
||||||
log "tap still exists: $TAP"
|
|
||||||
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"
|
|
||||||
15
todos
Normal file
15
todos
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
when developing, vm creation may fail, and firecracker logs need to be manually looked into, we should add a convenient way of digging through it. or perhaps log the last lines of it when vm creation fails
|
||||||
|
|
||||||
|
`banger vm run` can hang waiting for ssh if things go south with sshd inside the vm. I think we should have a timeout instead of hanging forever. And also log what the user can do in such scenario.
|
||||||
|
|
||||||
|
some commands are expected to take a while, it'd be good to at least show an indicator that banger is not hanging but rather doing something expected
|
||||||
|
|
||||||
|
perhaps add an "interactive flag"?
|
||||||
|
|
||||||
|
my computer is not the usual computer that users may have. perhaps it would be a good idea to screen the hardware we're working on so that we can set reasonable defaults for people when installing?
|
||||||
|
|
||||||
|
versioning and releasing could use some love
|
||||||
|
|
||||||
|
coverage would be somewhat nice to have
|
||||||
|
|
||||||
|
regular users have no idea how to point their machine DNS to use the banger dns server. they need a tutorial/docs for this
|
||||||
Loading…
Add table
Add a link
Reference in a new issue