Add vsock-backed SSH session reminders
Remind users when a VM is still running after hanger vm ssh exits instead of silently dropping them back to the host shell.\n\nAttach a Firecracker vsock device to each VM, persist the host vsock path/CID,\nadd a new guest-side banger-vsock-pingd responder to the runtime bundle and both\nimage-build paths, and expose a vm.ping RPC that the CLI and TUI call after SSH\nreturns. Doctor and start/build preflight now validate the helper plus\n/dev/vhost-vsock so the feature fails early and clearly.\n\nValidated with go mod tidy, bash -n customize.sh, git diff --check, make build,\nand GOCACHE=/tmp/banger-gocache go test ./... outside the sandbox because the\ndaemon tests need real Unix/UDP sockets. Rebuild the image/rootfs used for new\nVMs so the guest ping service is present.
This commit is contained in:
parent
4930d82cb9
commit
08ef706e3f
31 changed files with 912 additions and 75 deletions
|
|
@ -9,11 +9,11 @@
|
|||
- The daemon keeps state under XDG directories rather than the old repo-local `state/` layout.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- `make build` builds `./banger` and `./bangerd`.
|
||||
- `make build` builds `./banger`, `./bangerd`, and the bundled `./runtime/banger-vsock-pingd` guest helper.
|
||||
- `make runtime-bundle` bootstraps `./runtime/` from the archive referenced by `RUNTIME_MANIFEST`; the checked-in `runtime-bundle.toml` is only a template.
|
||||
- `banger` validates required host tools per command and reports actionable missing-tool errors; do not assume one workstation's package set.
|
||||
- `./banger vm create --name testbox` creates and starts a VM.
|
||||
- `./banger vm ssh testbox` connects to a running guest.
|
||||
- `./banger vm ssh testbox` connects to a running guest and reminds the user if the VM is still running when the session exits.
|
||||
- `./banger vm stop testbox` stops a VM while preserving its disks.
|
||||
- `./banger vm stop vm-a vm-b vm-c` and `./banger vm set --nat web-1 web-2` are supported; multi-VM lifecycle and `set` actions fan out concurrently through the CLI.
|
||||
- `./banger doctor` reports runtime bundle, host tool, feature, and image-build readiness from the same Go checks used by the daemon.
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
- Primary automated coverage is `go test ./...`.
|
||||
- Manual verification for VM lifecycle changes: `./banger vm create`, confirm SSH access, then stop/delete the VM.
|
||||
- For host-integration changes, run `./banger doctor` as a quick readiness check before the live VM smoke.
|
||||
- Rebuilt images now include `mise`, `opencode`, and `tmux-resurrect`/`tmux-continuum` defaults for `root`; if you change guest provisioning, document whether users need to rebuild `./runtime/rootfs-docker.ext4` or another base image to pick it up.
|
||||
- Rebuilt images now include `mise`, `opencode`, `tmux-resurrect`/`tmux-continuum` defaults for `root`, and the `banger-vsock-pingd` service used by the SSH reminder path; if you change guest provisioning, document whether users need to rebuild `./runtime/rootfs-docker.ext4` or another base image to pick it up.
|
||||
- If you add a new operational workflow, document how to exercise it in `README.md`.
|
||||
- For NAT changes, verify both guest outbound access and host rule cleanup, for example with `./verify.sh --nat`.
|
||||
|
||||
|
|
|
|||
9
Makefile
9
Makefile
|
|
@ -12,8 +12,9 @@ RUNTIME_MANIFEST ?= runtime-bundle.toml
|
|||
RUNTIME_SOURCE_DIR ?= runtime
|
||||
RUNTIME_ARCHIVE ?= dist/banger-runtime.tar.gz
|
||||
BINARIES := banger bangerd
|
||||
RUNTIME_HELPERS := $(RUNTIME_SOURCE_DIR)/banger-vsock-pingd
|
||||
GO_SOURCES := $(shell find cmd internal -type f -name '*.go' | sort)
|
||||
RUNTIME_EXECUTABLES := firecracker customize.sh packages.sh namegen
|
||||
RUNTIME_EXECUTABLES := firecracker customize.sh packages.sh namegen banger-vsock-pingd
|
||||
RUNTIME_DATA_FILES := packages.apt id_ed25519 rootfs-docker.ext4
|
||||
RUNTIME_OPTIONAL_DATA_FILES := rootfs.ext4 bundle.json
|
||||
RUNTIME_BOOT_FILES := wtf/root/boot/vmlinux-6.8.0-94-generic wtf/root/boot/initrd.img-6.8.0-94-generic
|
||||
|
|
@ -36,7 +37,7 @@ help:
|
|||
' make clean Remove built Go binaries' \
|
||||
' make rootfs Rebuild the source-checkout default rootfs image in ./runtime'
|
||||
|
||||
build: $(BINARIES)
|
||||
build: $(BINARIES) $(RUNTIME_HELPERS)
|
||||
|
||||
banger: $(GO_SOURCES) go.mod go.sum
|
||||
$(GO) build -o ./banger ./cmd/banger
|
||||
|
|
@ -44,6 +45,10 @@ banger: $(GO_SOURCES) go.mod go.sum
|
|||
bangerd: $(GO_SOURCES) go.mod go.sum
|
||||
$(GO) build -o ./bangerd ./cmd/bangerd
|
||||
|
||||
$(RUNTIME_SOURCE_DIR)/banger-vsock-pingd: $(GO_SOURCES) go.mod go.sum
|
||||
mkdir -p "$(RUNTIME_SOURCE_DIR)"
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -o "$(RUNTIME_SOURCE_DIR)/banger-vsock-pingd" ./cmd/banger-vsock-pingd
|
||||
|
||||
test:
|
||||
$(GO) test ./...
|
||||
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -4,6 +4,7 @@ Persistent Firecracker development VMs managed through a Go daemon, CLI, and TUI
|
|||
|
||||
## Requirements
|
||||
- Linux host with KVM (`/dev/kvm` access)
|
||||
- Vsock support for post-SSH liveness reminders (`/dev/vhost-vsock`)
|
||||
- Core VM lifecycle: `sudo`, `ip`, `dmsetup`, `losetup`, `blockdev`, `truncate`, `pgrep`, `chown`, `chmod`, `kill`
|
||||
- Guest rootfs patching: `e2cp`, `e2rm`, `debugfs`
|
||||
- Guest work disk creation/resizing: `mkfs.ext4`, `e2fsck`, `resize2fs`, `mount`, `umount`, `cp`
|
||||
|
|
@ -21,6 +22,7 @@ generated `./runtime/` bundle, while installed binaries use
|
|||
|
||||
The bundle contains:
|
||||
- `firecracker`
|
||||
- `banger-vsock-pingd` for the guest-side SSH reminder responder
|
||||
- `bundle.json` with the bundle's default kernel/initrd/modules/rootfs paths
|
||||
- a kernel, initrd, and modules tree referenced by `bundle.json`
|
||||
- `rootfs-docker.ext4`
|
||||
|
|
@ -64,7 +66,8 @@ URL. `make install` will not fetch artifacts for you.
|
|||
make build
|
||||
```
|
||||
|
||||
Run `make build` after `./runtime/` has been bootstrapped.
|
||||
Run `make build` after `./runtime/` has been bootstrapped. It also rebuilds the
|
||||
bundled `banger-vsock-pingd` guest helper in `./runtime/`.
|
||||
|
||||
Install into `~/.local/bin` by default, with the runtime bundle under
|
||||
`~/.local/lib/banger`:
|
||||
|
|
@ -102,6 +105,9 @@ SSH into a running VM:
|
|||
banger vm ssh calm-otter
|
||||
```
|
||||
|
||||
When the SSH session exits normally, `banger` checks the guest over vsock and
|
||||
reminds you if the VM is still running.
|
||||
|
||||
Stop, restart, kill, or delete it:
|
||||
```bash
|
||||
banger vm stop calm-otter
|
||||
|
|
@ -160,6 +166,7 @@ Useful config keys:
|
|||
- `ssh_key_path`
|
||||
- `namegen_path`
|
||||
- `customize_script` (manual helper compatibility; `banger image build` is Go-native)
|
||||
- `vsock_ping_helper_path`
|
||||
- `default_rootfs`
|
||||
- `default_base_rootfs`
|
||||
- `default_kernel`
|
||||
|
|
@ -197,9 +204,10 @@ banger image build --name docker-dev --docker
|
|||
```
|
||||
|
||||
Rebuilt images install a pinned `mise` at `/usr/local/bin/mise`, activate it
|
||||
for bash login and interactive shells, install `opencode` through `mise`, and
|
||||
for bash login and interactive shells, install `opencode` through `mise`,
|
||||
configure `tmux-resurrect` plus `tmux-continuum` for `root` with periodic
|
||||
autosaves and manual-only restore by default.
|
||||
autosaves and manual-only restore by default, and bake in the
|
||||
`banger-vsock-pingd` systemd service used by the post-SSH reminder path.
|
||||
|
||||
Show or delete images:
|
||||
```bash
|
||||
|
|
|
|||
49
cmd/banger-vsock-pingd/main.go
Normal file
49
cmd/banger-vsock-pingd/main.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
sdkvsock "github.com/firecracker-microvm/firecracker-go-sdk/vsock"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"banger/internal/vsockping"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
logger := logrus.New()
|
||||
logger.SetOutput(io.Discard)
|
||||
listener, err := sdkvsock.Listener(ctx, logrus.NewEntry(logger), vsockping.Port)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "banger-vsock-pingd: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "banger-vsock-pingd: accept: %v\n", err)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
go func(conn net.Conn) {
|
||||
if err := vsockping.ServeConn(conn); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "banger-vsock-pingd: %v\n", err)
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
36
customize.sh
36
customize.sh
|
|
@ -68,6 +68,7 @@ FC_BIN="$RUNTIME_DIR/firecracker"
|
|||
KERNEL="$(bundle_path default_kernel "$RUNTIME_DIR/wtf/root/boot/vmlinux-6.8.0-94-generic")"
|
||||
INITRD="$(bundle_path default_initrd "$RUNTIME_DIR/wtf/root/boot/initrd.img-6.8.0-94-generic")"
|
||||
SSH_KEY="$RUNTIME_DIR/id_ed25519"
|
||||
VSOCK_PING_HELPER="$(bundle_path vsock_ping_helper_path "$RUNTIME_DIR/banger-vsock-pingd")"
|
||||
|
||||
BR_DEV="br-fc"
|
||||
BR_IP="172.16.0.1"
|
||||
|
|
@ -207,6 +208,11 @@ if [[ ! -f "$PACKAGES_FILE" ]]; then
|
|||
log "package manifest not found: $PACKAGES_FILE"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -x "$VSOCK_PING_HELPER" ]]; then
|
||||
log "vsock ping helper not found or not executable: $VSOCK_PING_HELPER"
|
||||
log "run 'make build' or refresh the runtime bundle"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
APT_PACKAGES=()
|
||||
if ! banger_packages_read_array APT_PACKAGES "$PACKAGES_FILE"; then
|
||||
|
|
@ -382,6 +388,10 @@ if [[ "$SSH_READY" -ne 1 ]]; then
|
|||
fi
|
||||
|
||||
log "configuring guest"
|
||||
log "installing vsock ping helper"
|
||||
scp -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
"$VSOCK_PING_HELPER" "root@${GUEST_IP}:/usr/local/bin/banger-vsock-pingd" >/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
|
||||
|
|
@ -421,6 +431,32 @@ if [[ \"$INSTALL_DOCKER\" == \"1\" ]]; then
|
|||
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-pingd
|
||||
mkdir -p /etc/modules-load.d /etc/systemd/system
|
||||
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-pingd.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Banger vsock ping responder
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/banger-vsock-pingd
|
||||
Restart=on-failure
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
chmod 0644 /etc/systemd/system/banger-vsock-pingd.service
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || true
|
||||
systemctl enable --now banger-vsock-pingd.service || true
|
||||
fi
|
||||
git config --system init.defaultBranch main
|
||||
"
|
||||
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -46,6 +46,8 @@ require (
|
|||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mdlayher/socket v0.2.0 // indirect
|
||||
github.com/mdlayher/vsock v1.1.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
|
||||
github.com/muesli/cancelreader v0.2.1 // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -514,7 +514,9 @@ github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vq
|
|||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mdlayher/socket v0.2.0 h1:EY4YQd6hTAg2tcXF84p5DTHazShE50u5HeBzBaNgjkA=
|
||||
github.com/mdlayher/socket v0.2.0/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E=
|
||||
github.com/mdlayher/vsock v1.1.1 h1:8lFuiXQnmICBrCIIA9PMgVSke6Fg6V4+r0v7r55k88I=
|
||||
github.com/mdlayher/vsock v1.1.1/go.mod h1:Y43jzcy7KM3QB+/FK15pfqGxDMCMzUXWegEfIbSM18U=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
|
|
|
|||
|
|
@ -63,6 +63,11 @@ type VMSSHResult struct {
|
|||
GuestIP string `json:"guest_ip"`
|
||||
}
|
||||
|
||||
type VMPingResult struct {
|
||||
Name string `json:"name"`
|
||||
Alive bool `json:"alive"`
|
||||
}
|
||||
|
||||
type ImageBuildParams struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
BaseRootfs string `json:"base_rootfs,omitempty"`
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
|
@ -23,6 +24,7 @@ import (
|
|||
"banger/internal/rpc"
|
||||
"banger/internal/system"
|
||||
"banger/internal/vmdns"
|
||||
"banger/internal/vsockping"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
@ -32,7 +34,17 @@ var (
|
|||
daemonExePath = func(pid int) string {
|
||||
return filepath.Join("/proc", fmt.Sprintf("%d", pid), "exe")
|
||||
}
|
||||
doctorFunc = daemon.Doctor
|
||||
doctorFunc = daemon.Doctor
|
||||
sshExecFunc = func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error {
|
||||
sshCmd := exec.CommandContext(ctx, "ssh", args...)
|
||||
sshCmd.Stdout = stdout
|
||||
sshCmd.Stderr = stderr
|
||||
sshCmd.Stdin = stdin
|
||||
return sshCmd.Run()
|
||||
}
|
||||
vmPingFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMPingResult, error) {
|
||||
return rpc.Call[api.VMPingResult](ctx, socketPath, "vm.ping", api.VMRefParams{IDOrName: idOrName})
|
||||
}
|
||||
)
|
||||
|
||||
func NewBangerCommand() *cobra.Command {
|
||||
|
|
@ -454,11 +466,7 @@ func newVMSSHCommand() *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sshCmd := exec.CommandContext(cmd.Context(), "ssh", sshArgs...)
|
||||
sshCmd.Stdout = cmd.OutOrStdout()
|
||||
sshCmd.Stderr = cmd.ErrOrStderr()
|
||||
sshCmd.Stdin = cmd.InOrStdin()
|
||||
return sshCmd.Run()
|
||||
return runSSHSession(cmd.Context(), layout.SocketPath, result.Name, cmd.InOrStdin(), cmd.OutOrStdout(), cmd.ErrOrStderr(), sshArgs)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -953,6 +961,36 @@ func validatePositiveSetting(label string, value int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func runSSHSession(ctx context.Context, socketPath, vmRef string, stdin io.Reader, stdout, stderr io.Writer, sshArgs []string) error {
|
||||
sshErr := sshExecFunc(ctx, stdin, stdout, stderr, sshArgs)
|
||||
if !shouldCheckSSHReminder(sshErr) || ctx.Err() != nil {
|
||||
return sshErr
|
||||
}
|
||||
pingCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
ping, err := vmPingFunc(pingCtx, socketPath, vmRef)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(stderr, vsockping.WarningMessage(vmRef, err))
|
||||
return sshErr
|
||||
}
|
||||
if ping.Alive {
|
||||
name := ping.Name
|
||||
if strings.TrimSpace(name) == "" {
|
||||
name = vmRef
|
||||
}
|
||||
_, _ = fmt.Fprintln(stderr, vsockping.ReminderMessage(name))
|
||||
}
|
||||
return sshErr
|
||||
}
|
||||
|
||||
func shouldCheckSSHReminder(err error) bool {
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
var exitErr *exec.ExitError
|
||||
return errors.As(err, &exitErr)
|
||||
}
|
||||
|
||||
func sshCommandArgs(cfg model.DaemonConfig, guestIP string, extra []string) ([]string, error) {
|
||||
if guestIP == "" {
|
||||
return nil, errors.New("vm has no guest IP")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
|
@ -209,6 +211,56 @@ func TestVMSetParamsFromFlagsRejectsNonPositiveCPUAndMemory(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRunSSHSessionPrintsReminderWhenPingAlive(t *testing.T) {
|
||||
origSSHExec := sshExecFunc
|
||||
origPing := vmPingFunc
|
||||
t.Cleanup(func() {
|
||||
sshExecFunc = origSSHExec
|
||||
vmPingFunc = origPing
|
||||
})
|
||||
|
||||
sshExecFunc = func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error {
|
||||
return nil
|
||||
}
|
||||
vmPingFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMPingResult, error) {
|
||||
return api.VMPingResult{Name: "devbox", Alive: true}, nil
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
if err := runSSHSession(context.Background(), "/tmp/bangerd.sock", "devbox", strings.NewReader(""), &bytes.Buffer{}, &stderr, []string{"root@127.0.0.1"}); err != nil {
|
||||
t.Fatalf("runSSHSession: %v", err)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "devbox is still running") {
|
||||
t.Fatalf("stderr = %q, want reminder", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSSHSessionPreservesSSHExitStatusOnPingWarning(t *testing.T) {
|
||||
origSSHExec := sshExecFunc
|
||||
origPing := vmPingFunc
|
||||
t.Cleanup(func() {
|
||||
sshExecFunc = origSSHExec
|
||||
vmPingFunc = origPing
|
||||
})
|
||||
|
||||
sshExecFunc = func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error {
|
||||
return &exec.ExitError{}
|
||||
}
|
||||
vmPingFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMPingResult, error) {
|
||||
return api.VMPingResult{}, errors.New("dial failed")
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
err := runSSHSession(context.Background(), "/tmp/bangerd.sock", "devbox", strings.NewReader(""), &bytes.Buffer{}, &stderr, []string{"root@127.0.0.1"})
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("runSSHSession error = %v, want exit error", err)
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "failed to check whether devbox is still running") {
|
||||
t.Fatalf("stderr = %q, want warning", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveVMTargetsDeduplicatesAndReportsErrors(t *testing.T) {
|
||||
vms := []model.VMRecord{
|
||||
testCLIResolvedVM("alpha-id", "alpha"),
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"banger/internal/paths"
|
||||
"banger/internal/rpc"
|
||||
"banger/internal/system"
|
||||
"banger/internal/vsockping"
|
||||
|
||||
"github.com/charmbracelet/bubbles/help"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
|
|
@ -104,6 +105,7 @@ type externalPreparedMsg struct {
|
|||
action actionRequest
|
||||
command *exec.Cmd
|
||||
doneStatus string
|
||||
done func(error) tea.Msg
|
||||
refresh bool
|
||||
err error
|
||||
}
|
||||
|
|
@ -716,10 +718,14 @@ func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
break
|
||||
}
|
||||
cmds = append(cmds, tea.ExecProcess(msg.command, func(err error) tea.Msg {
|
||||
err = normalizeExecError(err)
|
||||
if msg.done != nil {
|
||||
return msg.done(err)
|
||||
}
|
||||
return actionResultMsg{
|
||||
action: msg.action,
|
||||
status: msg.doneStatus,
|
||||
err: normalizeExecError(err),
|
||||
err: err,
|
||||
refresh: msg.refresh,
|
||||
focusID: m.selectedID,
|
||||
}
|
||||
|
|
@ -1439,14 +1445,55 @@ func prepareSSHCmd(layout paths.Layout, cfg model.DaemonConfig, action actionReq
|
|||
return externalPreparedMsg{action: action, err: err}
|
||||
}
|
||||
return externalPreparedMsg{
|
||||
action: action,
|
||||
command: exec.Command("ssh", args...),
|
||||
doneStatus: fmt.Sprintf("ssh session ended for %s", result.Name),
|
||||
refresh: true,
|
||||
action: action,
|
||||
command: exec.Command("ssh", args...),
|
||||
done: func(execErr error) tea.Msg {
|
||||
return sshDoneMsg(layout, action, result.Name, execErr)
|
||||
},
|
||||
refresh: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sshDoneMsg(layout paths.Layout, action actionRequest, name string, execErr error) tea.Msg {
|
||||
if execErr != nil {
|
||||
return actionResultMsg{
|
||||
action: action,
|
||||
err: execErr,
|
||||
refresh: true,
|
||||
focusID: action.id,
|
||||
}
|
||||
}
|
||||
pingCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
ping, err := vmPingFunc(pingCtx, layout.SocketPath, name)
|
||||
if err != nil {
|
||||
return actionResultMsg{
|
||||
action: action,
|
||||
status: vsockping.WarningMessage(name, err),
|
||||
refresh: true,
|
||||
focusID: action.id,
|
||||
}
|
||||
}
|
||||
if ping.Alive {
|
||||
if strings.TrimSpace(ping.Name) != "" {
|
||||
name = ping.Name
|
||||
}
|
||||
return actionResultMsg{
|
||||
action: action,
|
||||
status: vsockping.ReminderMessage(name),
|
||||
refresh: true,
|
||||
focusID: action.id,
|
||||
}
|
||||
}
|
||||
return actionResultMsg{
|
||||
action: action,
|
||||
status: fmt.Sprintf("ssh session ended for %s", name),
|
||||
refresh: true,
|
||||
focusID: action.id,
|
||||
}
|
||||
}
|
||||
|
||||
func prepareLogsCmd(layout paths.Layout, action actionRequest) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
result, err := rpc.Call[api.VMLogsResult](context.Background(), layout.SocketPath, "vm.logs", api.VMRefParams{IDOrName: action.id})
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@ package cli
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"banger/internal/api"
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
|
||||
|
|
@ -236,6 +238,41 @@ func TestTUIStatusIncludesStageDurationsAfterInitialLoad(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSSHDoneMsgShowsReminderWhenPingAlive(t *testing.T) {
|
||||
origPing := vmPingFunc
|
||||
t.Cleanup(func() {
|
||||
vmPingFunc = origPing
|
||||
})
|
||||
vmPingFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMPingResult, error) {
|
||||
return api.VMPingResult{Name: "devbox", Alive: true}, nil
|
||||
}
|
||||
|
||||
msg := sshDoneMsg(paths.Layout{SocketPath: "/tmp/bangerd.sock"}, actionRequest{id: "devbox", name: "devbox"}, "devbox", nil)
|
||||
result, ok := msg.(actionResultMsg)
|
||||
if !ok {
|
||||
t.Fatalf("msg = %T, want actionResultMsg", msg)
|
||||
}
|
||||
if !strings.Contains(result.status, "devbox is still running") {
|
||||
t.Fatalf("status = %q, want reminder", result.status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSHDoneMsgShowsWarningWhenPingFails(t *testing.T) {
|
||||
origPing := vmPingFunc
|
||||
t.Cleanup(func() {
|
||||
vmPingFunc = origPing
|
||||
})
|
||||
vmPingFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMPingResult, error) {
|
||||
return api.VMPingResult{}, errors.New("dial failed")
|
||||
}
|
||||
|
||||
msg := sshDoneMsg(paths.Layout{SocketPath: "/tmp/bangerd.sock"}, actionRequest{id: "devbox", name: "devbox"}, "devbox", nil)
|
||||
result := msg.(actionResultMsg)
|
||||
if !strings.Contains(result.status, "failed to check whether devbox is still running") {
|
||||
t.Fatalf("status = %q, want warning", result.status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateRunningVMResources(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type fileConfig struct {
|
|||
SSHKeyPath string `toml:"ssh_key_path"`
|
||||
NamegenPath string `toml:"namegen_path"`
|
||||
CustomizeScript string `toml:"customize_script"`
|
||||
VSockPingHelper string `toml:"vsock_ping_helper_path"`
|
||||
DefaultImageName string `toml:"default_image_name"`
|
||||
DefaultRootfs string `toml:"default_rootfs"`
|
||||
DefaultBaseRootfs string `toml:"default_base_rootfs"`
|
||||
|
|
@ -87,6 +88,9 @@ func Load(layout paths.Layout) (model.DaemonConfig, error) {
|
|||
if file.CustomizeScript != "" {
|
||||
cfg.CustomizeScript = file.CustomizeScript
|
||||
}
|
||||
if file.VSockPingHelper != "" {
|
||||
cfg.VSockPingHelperPath = file.VSockPingHelper
|
||||
}
|
||||
if file.DefaultImageName != "" {
|
||||
cfg.DefaultImageName = file.DefaultImageName
|
||||
}
|
||||
|
|
@ -180,6 +184,7 @@ func applyBundleMetadataDefaults(cfg *model.DaemonConfig, runtimeDir string, met
|
|||
cfg.SSHKeyPath = defaultRuntimePath(cfg.SSHKeyPath, runtimeDir, meta.SSHKeyPath)
|
||||
cfg.NamegenPath = defaultRuntimePath(cfg.NamegenPath, runtimeDir, meta.NamegenPath)
|
||||
cfg.CustomizeScript = defaultRuntimePath(cfg.CustomizeScript, runtimeDir, meta.CustomizeScript)
|
||||
cfg.VSockPingHelperPath = defaultRuntimePath(cfg.VSockPingHelperPath, runtimeDir, meta.VSockPingHelperPath)
|
||||
cfg.DefaultKernel = defaultRuntimePath(cfg.DefaultKernel, runtimeDir, meta.DefaultKernel)
|
||||
cfg.DefaultInitrd = defaultRuntimePath(cfg.DefaultInitrd, runtimeDir, meta.DefaultInitrd)
|
||||
cfg.DefaultModulesDir = defaultRuntimePath(cfg.DefaultModulesDir, runtimeDir, meta.DefaultModulesDir)
|
||||
|
|
@ -193,6 +198,7 @@ func applyLegacyRuntimeDefaults(cfg *model.DaemonConfig) {
|
|||
cfg.SSHKeyPath = defaultRuntimePath(cfg.SSHKeyPath, cfg.RuntimeDir, "id_ed25519")
|
||||
cfg.NamegenPath = defaultRuntimePath(cfg.NamegenPath, cfg.RuntimeDir, "namegen")
|
||||
cfg.CustomizeScript = defaultRuntimePath(cfg.CustomizeScript, cfg.RuntimeDir, "customize.sh")
|
||||
cfg.VSockPingHelperPath = defaultRuntimePath(cfg.VSockPingHelperPath, cfg.RuntimeDir, "banger-vsock-pingd")
|
||||
cfg.DefaultKernel = defaultRuntimePath(cfg.DefaultKernel, cfg.RuntimeDir, "wtf/root/boot/vmlinux-6.8.0-94-generic")
|
||||
cfg.DefaultInitrd = defaultRuntimePath(cfg.DefaultInitrd, cfg.RuntimeDir, "wtf/root/boot/initrd.img-6.8.0-94-generic")
|
||||
cfg.DefaultModulesDir = defaultRuntimePath(cfg.DefaultModulesDir, cfg.RuntimeDir, "wtf/root/lib/modules/6.8.0-94-generic")
|
||||
|
|
|
|||
|
|
@ -13,21 +13,23 @@ import (
|
|||
func TestLoadDerivesArtifactPathsFromRuntimeDir(t *testing.T) {
|
||||
runtimeDir := t.TempDir()
|
||||
meta := runtimebundle.BundleMetadata{
|
||||
FirecrackerBin: "bin/firecracker",
|
||||
SSHKeyPath: "keys/id_ed25519",
|
||||
NamegenPath: "bin/namegen",
|
||||
CustomizeScript: "scripts/customize.sh",
|
||||
DefaultPackages: "config/packages.apt",
|
||||
DefaultRootfs: "images/rootfs-docker.ext4",
|
||||
DefaultKernel: "kernels/vmlinux",
|
||||
DefaultInitrd: "kernels/initrd.img",
|
||||
DefaultModulesDir: "modules/current",
|
||||
FirecrackerBin: "bin/firecracker",
|
||||
SSHKeyPath: "keys/id_ed25519",
|
||||
NamegenPath: "bin/namegen",
|
||||
CustomizeScript: "scripts/customize.sh",
|
||||
VSockPingHelperPath: "bin/banger-vsock-pingd",
|
||||
DefaultPackages: "config/packages.apt",
|
||||
DefaultRootfs: "images/rootfs-docker.ext4",
|
||||
DefaultKernel: "kernels/vmlinux",
|
||||
DefaultInitrd: "kernels/initrd.img",
|
||||
DefaultModulesDir: "modules/current",
|
||||
}
|
||||
for _, rel := range []string{
|
||||
meta.FirecrackerBin,
|
||||
meta.SSHKeyPath,
|
||||
meta.NamegenPath,
|
||||
meta.CustomizeScript,
|
||||
meta.VSockPingHelperPath,
|
||||
meta.DefaultPackages,
|
||||
meta.DefaultRootfs,
|
||||
meta.DefaultKernel,
|
||||
|
|
@ -71,6 +73,9 @@ func TestLoadDerivesArtifactPathsFromRuntimeDir(t *testing.T) {
|
|||
if cfg.CustomizeScript != filepath.Join(runtimeDir, meta.CustomizeScript) {
|
||||
t.Fatalf("CustomizeScript = %q", cfg.CustomizeScript)
|
||||
}
|
||||
if cfg.VSockPingHelperPath != filepath.Join(runtimeDir, meta.VSockPingHelperPath) {
|
||||
t.Fatalf("VSockPingHelperPath = %q", cfg.VSockPingHelperPath)
|
||||
}
|
||||
if cfg.DefaultRootfs != filepath.Join(runtimeDir, meta.DefaultRootfs) {
|
||||
t.Fatalf("DefaultRootfs = %q", cfg.DefaultRootfs)
|
||||
}
|
||||
|
|
@ -98,6 +103,7 @@ func TestLoadFallsBackToLegacyRuntimeLayoutWithoutBundleMetadata(t *testing.T) {
|
|||
"id_ed25519",
|
||||
"namegen",
|
||||
"customize.sh",
|
||||
"banger-vsock-pingd",
|
||||
"packages.apt",
|
||||
"rootfs-docker.ext4",
|
||||
"wtf/root/boot/vmlinux-6.8.0-94-generic",
|
||||
|
|
@ -122,6 +128,9 @@ func TestLoadFallsBackToLegacyRuntimeLayoutWithoutBundleMetadata(t *testing.T) {
|
|||
if cfg.FirecrackerBin != filepath.Join(runtimeDir, "firecracker") {
|
||||
t.Fatalf("FirecrackerBin = %q", cfg.FirecrackerBin)
|
||||
}
|
||||
if cfg.VSockPingHelperPath != filepath.Join(runtimeDir, "banger-vsock-pingd") {
|
||||
t.Fatalf("VSockPingHelperPath = %q", cfg.VSockPingHelperPath)
|
||||
}
|
||||
if cfg.DefaultKernel != filepath.Join(runtimeDir, "wtf/root/boot/vmlinux-6.8.0-94-generic") {
|
||||
t.Fatalf("DefaultKernel = %q", cfg.DefaultKernel)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -323,6 +323,13 @@ func (d *Daemon) dispatch(ctx context.Context, req rpc.Request) rpc.Response {
|
|||
return rpc.NewError("not_running", fmt.Sprintf("vm %s is not running", vm.Name))
|
||||
}
|
||||
return marshalResultOrError(api.VMSSHResult{Name: vm.Name, GuestIP: vm.Runtime.GuestIP}, nil)
|
||||
case "vm.ping":
|
||||
params, err := rpc.DecodeParams[api.VMRefParams](req)
|
||||
if err != nil {
|
||||
return rpc.NewError("bad_request", err.Error())
|
||||
}
|
||||
result, err := d.PingVM(ctx, params.IDOrName)
|
||||
return marshalResultOrError(result, err)
|
||||
case "image.list":
|
||||
images, err := d.store.ListImages(ctx)
|
||||
return marshalResultOrError(api.ImageListResult{Images: images}, err)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ func (d *Daemon) doctorReport(ctx context.Context) system.Report {
|
|||
|
||||
report.AddPreflight("runtime bundle", d.runtimeBundleChecks(), runtimeBundleStatus(d.config))
|
||||
report.AddPreflight("core vm lifecycle", d.coreVMLifecycleChecks(), "required host tools available")
|
||||
report.AddPreflight("vsock ssh reminder", d.vsockChecks(), "vsock reminder prerequisites available")
|
||||
d.addCapabilityDoctorChecks(ctx, &report)
|
||||
report.AddPreflight("image build", d.imageBuildChecks(ctx), "image build prerequisites available")
|
||||
|
||||
|
|
@ -44,6 +45,7 @@ func (d *Daemon) runtimeBundleChecks() *system.Preflight {
|
|||
hint := paths.RuntimeBundleHint()
|
||||
checks.RequireExecutable(d.config.FirecrackerBin, "firecracker binary", hint)
|
||||
checks.RequireFile(d.config.SSHKeyPath, "ssh private key", `set "ssh_key_path" or refresh the runtime bundle`)
|
||||
checks.RequireExecutable(d.config.VSockPingHelperPath, "vsock ping helper", `run 'make build' or refresh the runtime bundle`)
|
||||
checks.RequireFile(d.config.DefaultRootfs, "default rootfs image", `set "default_rootfs" or refresh the runtime bundle`)
|
||||
checks.RequireFile(d.config.DefaultKernel, "kernel image", `set "default_kernel" or refresh the runtime bundle`)
|
||||
if strings.TrimSpace(d.config.DefaultInitrd) != "" {
|
||||
|
|
@ -75,6 +77,13 @@ func (d *Daemon) imageBuildChecks(ctx context.Context) *system.Preflight {
|
|||
return checks
|
||||
}
|
||||
|
||||
func (d *Daemon) vsockChecks() *system.Preflight {
|
||||
checks := system.NewPreflight()
|
||||
checks.RequireExecutable(d.config.VSockPingHelperPath, "vsock ping helper", `run 'make build' or refresh the runtime bundle`)
|
||||
checks.RequireFile(vsockHostDevicePath, "vsock host device", "load the vhost_vsock kernel module on the host")
|
||||
return checks
|
||||
}
|
||||
|
||||
func runtimeBundleStatus(cfg model.DaemonConfig) string {
|
||||
if strings.TrimSpace(cfg.RuntimeDir) == "" {
|
||||
return "runtime dir not configured"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"banger/internal/hostnat"
|
||||
"banger/internal/model"
|
||||
"banger/internal/system"
|
||||
"banger/internal/vsockping"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -103,6 +104,16 @@ func (d *Daemon) runImageBuildNative(ctx context.Context, spec imageBuildSpec) (
|
|||
}
|
||||
defer client.Close()
|
||||
|
||||
helperBytes, err := os.ReadFile(d.config.VSockPingHelperPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeBuildLog(spec.BuildLog, "installing vsock ping helper"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := client.UploadFile(ctx, vsockping.GuestInstallPath, 0o755, helperBytes, spec.BuildLog); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeBuildLog(spec.BuildLog, "configuring guest"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -207,7 +218,7 @@ func (d *Daemon) startImageBuildVM(ctx context.Context, spec imageBuildSpec) (im
|
|||
return imageBuildVM{}, nil, err
|
||||
}
|
||||
vm.PID = d.resolveFirecrackerPID(firecrackerCtx, machine, vm.APISock)
|
||||
if err := d.ensureSocketAccess(ctx, vm.APISock); err != nil {
|
||||
if err := d.ensureSocketAccess(ctx, vm.APISock, "firecracker api socket"); err != nil {
|
||||
_ = d.killVMProcess(context.Background(), vm.PID)
|
||||
_ = hostnat.Ensure(ctx, d.runner, vm.GuestIP, vm.TapDevice, false)
|
||||
_, _ = d.runner.RunSudo(ctx, "ip", "link", "del", vm.TapDevice)
|
||||
|
|
@ -255,6 +266,7 @@ func buildProvisionScript(vmName, dnsServer string, packages []string, installDo
|
|||
script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y install \"${PACKAGES[@]}\"\n")
|
||||
appendMiseSetup(&script)
|
||||
appendTmuxSetup(&script)
|
||||
appendVSockPingSetup(&script)
|
||||
if installDocker {
|
||||
script.WriteString("DEBIAN_FRONTEND=noninteractive apt-get -y remove containerd || true\n")
|
||||
script.WriteString("if ! DEBIAN_FRONTEND=noninteractive apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then\n")
|
||||
|
|
@ -318,6 +330,19 @@ func appendTmuxSetup(script *bytes.Buffer) {
|
|||
script.WriteString("chmod 0644 \"$TMUX_CONF\"\n")
|
||||
}
|
||||
|
||||
func appendVSockPingSetup(script *bytes.Buffer) {
|
||||
script.WriteString("mkdir -p /etc/modules-load.d /etc/systemd/system\n")
|
||||
script.WriteString("cat > /etc/modules-load.d/banger-vsock.conf <<'EOF'\n")
|
||||
script.WriteString(vsockping.ModulesLoadConfig())
|
||||
script.WriteString("EOF\n")
|
||||
script.WriteString("chmod 0644 /etc/modules-load.d/banger-vsock.conf\n")
|
||||
script.WriteString("cat > /etc/systemd/system/" + vsockping.ServiceName + " <<'EOF'\n")
|
||||
script.WriteString(vsockping.ServiceUnit())
|
||||
script.WriteString("EOF\n")
|
||||
script.WriteString("chmod 0644 /etc/systemd/system/" + vsockping.ServiceName + "\n")
|
||||
script.WriteString("if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload || true; systemctl enable --now " + vsockping.ServiceName + " || true; fi\n")
|
||||
}
|
||||
|
||||
func appendGitRepo(script *bytes.Buffer, dir, repo string) {
|
||||
fmt.Fprintf(script, "if [[ -d \"%s/.git\" ]]; then\n", dir)
|
||||
fmt.Fprintf(script, " git -C \"%s\" fetch --depth 1 origin\n", dir)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@ func TestBuildProvisionScriptInstallsDefaultTools(t *testing.T) {
|
|||
"set -g @continuum-restore 'off'",
|
||||
"set -g @resurrect-dir '/root/.tmux/resurrect'",
|
||||
"run '~/.tmux/plugins/tpm/tpm'",
|
||||
"cat > /etc/modules-load.d/banger-vsock.conf <<'EOF'",
|
||||
"vmw_vsock_virtio_transport",
|
||||
"cat > /etc/systemd/system/banger-vsock-pingd.service <<'EOF'",
|
||||
"ExecStart=/usr/local/bin/banger-vsock-pingd",
|
||||
"systemctl enable --now banger-vsock-pingd.service || true",
|
||||
"rm -f /root/get-docker /root/get-docker.sh /tmp/get-docker /tmp/get-docker.sh",
|
||||
} {
|
||||
if !strings.Contains(script, snippet) {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ func TestNewDaemonLoggerEmitsJSONAtConfiguredLevel(t *testing.T) {
|
|||
|
||||
func TestStartVMLockedLogsBridgeFailure(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
origVsockHostDevicePath := vsockHostDevicePath
|
||||
vsockHostDevicePath = filepath.Join(t.TempDir(), "vhost-vsock")
|
||||
t.Cleanup(func() {
|
||||
vsockHostDevicePath = origVsockHostDevicePath
|
||||
})
|
||||
binDir := t.TempDir()
|
||||
for _, name := range []string{
|
||||
"sudo", "ip", "dmsetup", "losetup", "blockdev", "truncate", "pgrep", "ps",
|
||||
|
|
@ -54,9 +59,16 @@ func TestStartVMLockedLogsBridgeFailure(t *testing.T) {
|
|||
t.Setenv("PATH", binDir)
|
||||
|
||||
firecrackerBin := filepath.Join(t.TempDir(), "firecracker")
|
||||
vsockHelper := filepath.Join(t.TempDir(), "banger-vsock-pingd")
|
||||
if err := os.WriteFile(firecrackerBin, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
t.Fatalf("write firecracker: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(vsockHostDevicePath, []byte{}, 0o644); err != nil {
|
||||
t.Fatalf("write vsock host device: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(vsockHelper, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
t.Fatalf("write vsock helper: %v", err)
|
||||
}
|
||||
rootfsPath := filepath.Join(t.TempDir(), "rootfs.ext4")
|
||||
kernelPath := filepath.Join(t.TempDir(), "vmlinux")
|
||||
for _, path := range []string{rootfsPath, kernelPath} {
|
||||
|
|
@ -93,11 +105,12 @@ func TestStartVMLockedLogsBridgeFailure(t *testing.T) {
|
|||
d := &Daemon{
|
||||
layout: paths.Layout{RuntimeDir: filepath.Join(t.TempDir(), "runtime")},
|
||||
config: model.DaemonConfig{
|
||||
BridgeName: "br-fc",
|
||||
BridgeIP: model.DefaultBridgeIP,
|
||||
DefaultDNS: model.DefaultDNS,
|
||||
FirecrackerBin: firecrackerBin,
|
||||
StatsPollInterval: model.DefaultStatsPollInterval,
|
||||
BridgeName: "br-fc",
|
||||
BridgeIP: model.DefaultBridgeIP,
|
||||
DefaultDNS: model.DefaultDNS,
|
||||
FirecrackerBin: firecrackerBin,
|
||||
VSockPingHelperPath: vsockHelper,
|
||||
StatsPollInterval: model.DefaultStatsPollInterval,
|
||||
},
|
||||
runner: runner,
|
||||
logger: logger,
|
||||
|
|
@ -138,11 +151,15 @@ func TestBuildImagePreservesBuildLogOnFailure(t *testing.T) {
|
|||
packagesPath := filepath.Join(t.TempDir(), "packages.apt")
|
||||
sshKeyPath := filepath.Join(t.TempDir(), "id_ed25519")
|
||||
firecrackerBin := filepath.Join(t.TempDir(), "firecracker")
|
||||
vsockHelper := filepath.Join(t.TempDir(), "banger-vsock-pingd")
|
||||
for _, path := range []string{baseRootfs, kernelPath, packagesPath, sshKeyPath} {
|
||||
if err := os.WriteFile(path, []byte("artifact"), 0o644); err != nil {
|
||||
t.Fatalf("write %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(vsockHelper, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
t.Fatalf("write %s: %v", vsockHelper, err)
|
||||
}
|
||||
if err := os.WriteFile(firecrackerBin, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
t.Fatalf("write %s: %v", firecrackerBin, err)
|
||||
}
|
||||
|
|
@ -169,6 +186,7 @@ func TestBuildImagePreservesBuildLogOnFailure(t *testing.T) {
|
|||
DefaultPackagesFile: packagesPath,
|
||||
SSHKeyPath: sshKeyPath,
|
||||
FirecrackerBin: firecrackerBin,
|
||||
VSockPingHelperPath: vsockHelper,
|
||||
},
|
||||
store: store,
|
||||
runner: runner,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import (
|
|||
"banger/internal/system"
|
||||
)
|
||||
|
||||
var vsockHostDevicePath = "/dev/vhost-vsock"
|
||||
|
||||
func (d *Daemon) validateStartPrereqs(ctx context.Context, vm model.VMRecord, image model.Image) error {
|
||||
checks := system.NewPreflight()
|
||||
d.addBaseStartPrereqs(checks, image)
|
||||
|
|
@ -52,6 +54,8 @@ func (d *Daemon) addBaseStartPrereqs(checks *system.Preflight, image model.Image
|
|||
|
||||
d.addBaseStartCommandPrereqs(checks)
|
||||
checks.RequireExecutable(d.config.FirecrackerBin, "firecracker binary", hint)
|
||||
checks.RequireExecutable(d.config.VSockPingHelperPath, "vsock ping helper", `run 'make build' or refresh the runtime bundle`)
|
||||
checks.RequireFile(vsockHostDevicePath, "vsock host device", "load the vhost_vsock kernel module on the host")
|
||||
checks.RequireFile(image.RootfsPath, "rootfs image", "select a valid image or rebuild the runtime bundle")
|
||||
checks.RequireFile(image.KernelPath, "kernel image", `set "default_kernel" or refresh the runtime bundle`)
|
||||
if strings.TrimSpace(image.InitrdPath) != "" {
|
||||
|
|
@ -73,6 +77,7 @@ func (d *Daemon) addImageBuildPrereqs(ctx context.Context, checks *system.Prefli
|
|||
}
|
||||
checks.RequireExecutable(d.config.FirecrackerBin, "firecracker binary", hint)
|
||||
checks.RequireFile(d.config.SSHKeyPath, "ssh private key", `set "ssh_key_path" or refresh the runtime bundle`)
|
||||
checks.RequireExecutable(d.config.VSockPingHelperPath, "vsock ping helper", `run 'make build' or refresh the runtime bundle`)
|
||||
checks.RequireFile(baseRootfs, "base rootfs image", `pass --base-rootfs or set "default_base_rootfs"`)
|
||||
checks.RequireFile(kernelPath, "kernel image", `pass --kernel or set "default_kernel"`)
|
||||
checks.RequireFile(d.config.DefaultPackagesFile, "package manifest", `set "default_packages_file" or refresh the runtime bundle`)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
|
@ -75,6 +76,10 @@ func (d *Daemon) CreateVM(ctx context.Context, params api.VMCreateParams) (vm mo
|
|||
if err := os.MkdirAll(vmDir, 0o755); err != nil {
|
||||
return model.VMRecord{}, err
|
||||
}
|
||||
vsockCID, err := defaultVSockCID(guestIP)
|
||||
if err != nil {
|
||||
return model.VMRecord{}, err
|
||||
}
|
||||
systemOverlaySize := int64(model.DefaultSystemOverlaySize)
|
||||
if params.SystemOverlaySize != "" {
|
||||
systemOverlaySize, err = model.ParseSize(params.SystemOverlaySize)
|
||||
|
|
@ -111,6 +116,8 @@ func (d *Daemon) CreateVM(ctx context.Context, params api.VMCreateParams) (vm mo
|
|||
GuestIP: guestIP,
|
||||
DNSName: vmdns.RecordName(name),
|
||||
VMDir: vmDir,
|
||||
VSockPath: defaultVSockPath(d.layout.RuntimeDir, id),
|
||||
VSockCID: vsockCID,
|
||||
SystemOverlay: filepath.Join(vmDir, "system.cow"),
|
||||
WorkDiskPath: filepath.Join(vmDir, "root.ext4"),
|
||||
LogPath: filepath.Join(vmDir, "firecracker.log"),
|
||||
|
|
@ -183,9 +190,21 @@ func (d *Daemon) startVMLocked(ctx context.Context, vm model.VMRecord, image mod
|
|||
apiSock := filepath.Join(d.layout.RuntimeDir, "fc-"+shortID+".sock")
|
||||
tap := "tap-fc-" + shortID
|
||||
dmName := "fc-rootfs-" + shortID
|
||||
if strings.TrimSpace(vm.Runtime.VSockPath) == "" {
|
||||
vm.Runtime.VSockPath = defaultVSockPath(d.layout.RuntimeDir, vm.ID)
|
||||
}
|
||||
if vm.Runtime.VSockCID == 0 {
|
||||
vm.Runtime.VSockCID, err = defaultVSockCID(vm.Runtime.GuestIP)
|
||||
if err != nil {
|
||||
return model.VMRecord{}, err
|
||||
}
|
||||
}
|
||||
if err := os.RemoveAll(apiSock); err != nil && !os.IsNotExist(err) {
|
||||
return model.VMRecord{}, err
|
||||
}
|
||||
if err := os.RemoveAll(vm.Runtime.VSockPath); err != nil && !os.IsNotExist(err) {
|
||||
return model.VMRecord{}, err
|
||||
}
|
||||
|
||||
op.stage("system_overlay", "overlay_path", vm.Runtime.SystemOverlay)
|
||||
if err := d.ensureSystemOverlay(ctx, &vm); err != nil {
|
||||
|
|
@ -260,6 +279,8 @@ func (d *Daemon) startVMLocked(ctx context.Context, vm model.VMRecord, image mod
|
|||
IsRoot: true,
|
||||
}},
|
||||
TapDevice: tap,
|
||||
VSockPath: vm.Runtime.VSockPath,
|
||||
VSockCID: vm.Runtime.VSockCID,
|
||||
VCPUCount: vm.Spec.VCPUCount,
|
||||
MemoryMiB: vm.Spec.MemoryMiB,
|
||||
Logger: d.logger,
|
||||
|
|
@ -276,7 +297,11 @@ func (d *Daemon) startVMLocked(ctx context.Context, vm model.VMRecord, image mod
|
|||
vm.Runtime.PID = d.resolveFirecrackerPID(firecrackerCtx, machine, apiSock)
|
||||
op.debugStage("firecracker_started", "pid", vm.Runtime.PID)
|
||||
op.stage("socket_access", "api_socket", apiSock)
|
||||
if err := d.ensureSocketAccess(ctx, apiSock); err != nil {
|
||||
if err := d.ensureSocketAccess(ctx, apiSock, "firecracker api socket"); err != nil {
|
||||
return cleanupOnErr(err)
|
||||
}
|
||||
op.stage("vsock_access", "vsock_path", vm.Runtime.VSockPath, "vsock_cid", vm.Runtime.VSockCID)
|
||||
if err := d.ensureSocketAccess(ctx, vm.Runtime.VSockPath, "firecracker vsock socket"); err != nil {
|
||||
return cleanupOnErr(err)
|
||||
}
|
||||
op.stage("post_start_features")
|
||||
|
|
@ -556,6 +581,33 @@ func (d *Daemon) GetVMStats(ctx context.Context, idOrName string) (model.VMRecor
|
|||
return vm, vm.Stats, nil
|
||||
}
|
||||
|
||||
func (d *Daemon) PingVM(ctx context.Context, idOrName string) (result api.VMPingResult, err error) {
|
||||
_, err = d.withVMLockByRef(ctx, idOrName, func(vm model.VMRecord) (model.VMRecord, error) {
|
||||
result.Name = vm.Name
|
||||
if vm.State != model.VMStateRunning || !system.ProcessRunning(vm.Runtime.PID, vm.Runtime.APISockPath) {
|
||||
result.Alive = false
|
||||
return vm, nil
|
||||
}
|
||||
if strings.TrimSpace(vm.Runtime.VSockPath) == "" {
|
||||
return model.VMRecord{}, errors.New("vm has no vsock path")
|
||||
}
|
||||
if vm.Runtime.VSockCID == 0 {
|
||||
return model.VMRecord{}, errors.New("vm has no vsock cid")
|
||||
}
|
||||
if err := d.ensureSocketAccess(ctx, vm.Runtime.VSockPath, "firecracker vsock socket"); err != nil {
|
||||
return model.VMRecord{}, err
|
||||
}
|
||||
pingCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
if err := firecracker.PingVSock(pingCtx, d.logger, vm.Runtime.VSockPath); err != nil {
|
||||
return model.VMRecord{}, err
|
||||
}
|
||||
result.Alive = true
|
||||
return vm, nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d *Daemon) getVMStatsLocked(ctx context.Context, vm model.VMRecord) (model.VMRecord, error) {
|
||||
stats, err := d.collectStats(ctx, vm)
|
||||
if err == nil {
|
||||
|
|
@ -812,11 +864,14 @@ func (d *Daemon) firecrackerBinary() (string, error) {
|
|||
return path, nil
|
||||
}
|
||||
|
||||
func (d *Daemon) ensureSocketAccess(ctx context.Context, apiSock string) error {
|
||||
if _, err := d.runner.RunSudo(ctx, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), apiSock); err != nil {
|
||||
func (d *Daemon) ensureSocketAccess(ctx context.Context, socketPath, label string) error {
|
||||
if err := waitForPath(ctx, socketPath, 5*time.Second, label); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := d.runner.RunSudo(ctx, "chmod", "600", apiSock)
|
||||
if _, err := d.runner.RunSudo(ctx, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), socketPath); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := d.runner.RunSudo(ctx, "chmod", "600", socketPath)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -841,7 +896,7 @@ func (d *Daemon) resolveFirecrackerPID(ctx context.Context, machine *firecracker
|
|||
}
|
||||
|
||||
func (d *Daemon) sendCtrlAltDel(ctx context.Context, vm model.VMRecord) error {
|
||||
if err := d.ensureSocketAccess(ctx, vm.Runtime.APISockPath); err != nil {
|
||||
if err := d.ensureSocketAccess(ctx, vm.Runtime.APISockPath, "firecracker api socket"); err != nil {
|
||||
return err
|
||||
}
|
||||
client := firecracker.New(vm.Runtime.APISockPath, d.logger)
|
||||
|
|
@ -887,6 +942,9 @@ func (d *Daemon) cleanupRuntime(ctx context.Context, vm model.VMRecord, preserve
|
|||
if vm.Runtime.APISockPath != "" {
|
||||
_ = os.Remove(vm.Runtime.APISockPath)
|
||||
}
|
||||
if vm.Runtime.VSockPath != "" {
|
||||
_ = os.Remove(vm.Runtime.VSockPath)
|
||||
}
|
||||
snapshotErr := d.cleanupDMSnapshot(ctx, dmSnapshotHandles{
|
||||
BaseLoop: vm.Runtime.BaseLoop,
|
||||
COWLoop: vm.Runtime.COWLoop,
|
||||
|
|
@ -910,6 +968,37 @@ func clearRuntimeHandles(vm *model.VMRecord) {
|
|||
vm.Runtime.DMDev = ""
|
||||
}
|
||||
|
||||
func defaultVSockPath(runtimeDir, vmID string) string {
|
||||
return filepath.Join(runtimeDir, "fc-"+system.ShortID(vmID)+".vsock")
|
||||
}
|
||||
|
||||
func defaultVSockCID(guestIP string) (uint32, error) {
|
||||
ip := net.ParseIP(strings.TrimSpace(guestIP)).To4()
|
||||
if ip == nil {
|
||||
return 0, fmt.Errorf("guest IP is not IPv4: %q", guestIP)
|
||||
}
|
||||
return 10000 + uint32(ip[3]), nil
|
||||
}
|
||||
|
||||
func waitForPath(ctx context.Context, path string, timeout time.Duration, label string) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil
|
||||
} else if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("%s not ready: %s: %w", label, path, context.DeadlineExceeded)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Daemon) setDNS(ctx context.Context, vmName, guestIP string) error {
|
||||
if d.vmDNS == nil {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -261,6 +261,114 @@ func TestSetVMRejectsStoppedOnlyChangesForRunningVM(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPingVMReturnsAliveForRunningGuest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
db := openDaemonStore(t)
|
||||
apiSock := filepath.Join(t.TempDir(), "fc.sock")
|
||||
fake := startFakeFirecrackerProcess(t, apiSock)
|
||||
t.Cleanup(func() {
|
||||
_ = fake.Process.Kill()
|
||||
_ = fake.Wait()
|
||||
})
|
||||
|
||||
vsockSock := filepath.Join(t.TempDir(), "fc.vsock")
|
||||
listener, err := net.Listen("unix", vsockSock)
|
||||
if err != nil {
|
||||
t.Fatalf("listen vsock: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = listener.Close()
|
||||
_ = os.Remove(vsockSock)
|
||||
})
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
serverDone <- err
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
buf := make([]byte, 128)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
serverDone <- err
|
||||
return
|
||||
}
|
||||
if got := string(buf[:n]); got != "CONNECT 42070\n" {
|
||||
serverDone <- fmt.Errorf("unexpected connect message %q", got)
|
||||
return
|
||||
}
|
||||
if _, err := conn.Write([]byte("OK 1\n")); err != nil {
|
||||
serverDone <- err
|
||||
return
|
||||
}
|
||||
n, err = conn.Read(buf)
|
||||
if err != nil {
|
||||
serverDone <- err
|
||||
return
|
||||
}
|
||||
if got := string(buf[:n]); got != "PING\n" {
|
||||
serverDone <- fmt.Errorf("unexpected ping payload %q", got)
|
||||
return
|
||||
}
|
||||
_, err = conn.Write([]byte("PONG\n"))
|
||||
serverDone <- err
|
||||
}()
|
||||
|
||||
vm := testVM("alive", "image-alive", "172.16.0.41")
|
||||
vm.State = model.VMStateRunning
|
||||
vm.Runtime.State = model.VMStateRunning
|
||||
vm.Runtime.PID = fake.Process.Pid
|
||||
vm.Runtime.APISockPath = apiSock
|
||||
vm.Runtime.VSockPath = vsockSock
|
||||
vm.Runtime.VSockCID = 10041
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM: %v", err)
|
||||
}
|
||||
|
||||
runner := &scriptedRunner{
|
||||
t: t,
|
||||
steps: []runnerStep{
|
||||
sudoStep("", nil, "chown", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), vsockSock),
|
||||
sudoStep("", nil, "chmod", "600", vsockSock),
|
||||
},
|
||||
}
|
||||
d := &Daemon{store: db, runner: runner}
|
||||
result, err := d.PingVM(ctx, vm.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("PingVM: %v", err)
|
||||
}
|
||||
if !result.Alive || result.Name != vm.Name {
|
||||
t.Fatalf("PingVM result = %+v, want alive %s", result, vm.Name)
|
||||
}
|
||||
runner.assertExhausted()
|
||||
if err := <-serverDone; err != nil {
|
||||
t.Fatalf("server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPingVMReturnsFalseForStoppedVM(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
db := openDaemonStore(t)
|
||||
vm := testVM("stopped-ping", "image-stopped", "172.16.0.42")
|
||||
if err := db.UpsertVM(ctx, vm); err != nil {
|
||||
t.Fatalf("UpsertVM: %v", err)
|
||||
}
|
||||
|
||||
d := &Daemon{store: db}
|
||||
result, err := d.PingVM(ctx, vm.Name)
|
||||
if err != nil {
|
||||
t.Fatalf("PingVM: %v", err)
|
||||
}
|
||||
if result.Alive {
|
||||
t.Fatalf("PingVM result = %+v, want not alive", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetVMDiskResizeFailsPreflightWhenToolsMissing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := openDaemonStore(t)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,23 @@
|
|||
package firecracker
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
sdk "github.com/firecracker-microvm/firecracker-go-sdk"
|
||||
models "github.com/firecracker-microvm/firecracker-go-sdk/client/models"
|
||||
sdkvsock "github.com/firecracker-microvm/firecracker-go-sdk/vsock"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"banger/internal/vsockping"
|
||||
)
|
||||
|
||||
type MachineConfig struct {
|
||||
|
|
@ -25,6 +31,8 @@ type MachineConfig struct {
|
|||
KernelArgs string
|
||||
Drives []DriveConfig
|
||||
TapDevice string
|
||||
VSockPath string
|
||||
VSockCID uint32
|
||||
VCPUCount int
|
||||
MemoryMiB int
|
||||
Logger *slog.Logger
|
||||
|
|
@ -132,6 +140,7 @@ func buildConfig(cfg MachineConfig) sdk.Config {
|
|||
HostDevName: cfg.TapDevice,
|
||||
},
|
||||
}},
|
||||
VsockDevices: buildVsockDevices(cfg),
|
||||
MachineCfg: models.MachineConfiguration{
|
||||
VcpuCount: sdk.Int64(int64(cfg.VCPUCount)),
|
||||
MemSizeMib: sdk.Int64(int64(cfg.MemoryMiB)),
|
||||
|
|
@ -141,6 +150,17 @@ func buildConfig(cfg MachineConfig) sdk.Config {
|
|||
}
|
||||
}
|
||||
|
||||
func buildVsockDevices(cfg MachineConfig) []sdk.VsockDevice {
|
||||
if strings.TrimSpace(cfg.VSockPath) == "" || cfg.VSockCID == 0 {
|
||||
return nil
|
||||
}
|
||||
return []sdk.VsockDevice{{
|
||||
ID: "vsock",
|
||||
Path: cfg.VSockPath,
|
||||
CID: cfg.VSockCID,
|
||||
}}
|
||||
}
|
||||
|
||||
func splitDrives(drives []DriveConfig) (DriveConfig, []DriveConfig) {
|
||||
root := DriveConfig{ID: "rootfs"}
|
||||
var extras []DriveConfig
|
||||
|
|
@ -192,6 +212,39 @@ func newLogger(base *slog.Logger) *logrus.Entry {
|
|||
return logrus.NewEntry(logger)
|
||||
}
|
||||
|
||||
func PingVSock(ctx context.Context, logger *slog.Logger, socketPath string) error {
|
||||
conn, err := sdkvsock.DialContext(
|
||||
ctx,
|
||||
socketPath,
|
||||
vsockping.Port,
|
||||
sdkvsock.WithRetryTimeout(3*time.Second),
|
||||
sdkvsock.WithRetryInterval(100*time.Millisecond),
|
||||
sdkvsock.WithLogger(newLogger(logger)),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = conn.SetDeadline(deadline)
|
||||
} else {
|
||||
_ = conn.SetDeadline(time.Now().Add(3 * time.Second))
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(conn, vsockping.RequestLine); err != nil {
|
||||
return err
|
||||
}
|
||||
line, err := bufio.NewReader(conn).ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(line) != strings.TrimSpace(vsockping.ResponseLine) {
|
||||
return fmt.Errorf("unexpected vsock response %q", strings.TrimSpace(line))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type slogHook struct {
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@ package firecracker
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log/slog"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBuildConfig(t *testing.T) {
|
||||
|
|
@ -21,6 +25,8 @@ func TestBuildConfig(t *testing.T) {
|
|||
{ID: "work", Path: "/var/lib/banger/root.ext4"},
|
||||
},
|
||||
TapDevice: "tap-fc-1",
|
||||
VSockPath: "/tmp/fc.vsock",
|
||||
VSockCID: 10042,
|
||||
VCPUCount: 4,
|
||||
MemoryMiB: 2048,
|
||||
})
|
||||
|
|
@ -46,6 +52,12 @@ func TestBuildConfig(t *testing.T) {
|
|||
if len(cfg.NetworkInterfaces) != 1 {
|
||||
t.Fatalf("interface count = %d, want 1", len(cfg.NetworkInterfaces))
|
||||
}
|
||||
if len(cfg.VsockDevices) != 1 {
|
||||
t.Fatalf("vsock count = %d, want 1", len(cfg.VsockDevices))
|
||||
}
|
||||
if cfg.VsockDevices[0].Path != "/tmp/fc.vsock" || cfg.VsockDevices[0].CID != 10042 {
|
||||
t.Fatalf("unexpected vsock config: %+v", cfg.VsockDevices[0])
|
||||
}
|
||||
if got := cfg.NetworkInterfaces[0].StaticConfiguration.HostDevName; got != "tap-fc-1" {
|
||||
t.Fatalf("host dev name = %q", got)
|
||||
}
|
||||
|
|
@ -115,3 +127,81 @@ func TestSDKLoggerBridgeSuppressesDebugAtInfoLevel(t *testing.T) {
|
|||
t.Fatalf("expected info-level logger to suppress sdk debug chatter, got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPingVSock(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
socketPath := filepath.Join(dir, "fc.vsock")
|
||||
listener, err := net.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Listen: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
buf := make([]byte, 0, 64)
|
||||
tmp := make([]byte, 64)
|
||||
for {
|
||||
n, err := conn.Read(tmp)
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
buf = append(buf, tmp[:n]...)
|
||||
if strings.Contains(string(buf), "\n") {
|
||||
break
|
||||
}
|
||||
}
|
||||
if got := string(buf); got != "CONNECT 42070\n" {
|
||||
done <- errUnexpectedString(got)
|
||||
return
|
||||
}
|
||||
if _, err := conn.Write([]byte("OK 55\n")); err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
buf = buf[:0]
|
||||
for {
|
||||
n, err := conn.Read(tmp)
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
buf = append(buf, tmp[:n]...)
|
||||
if strings.Contains(string(buf), "\n") {
|
||||
break
|
||||
}
|
||||
}
|
||||
if got := string(buf); got != "PING\n" {
|
||||
done <- errUnexpectedString(got)
|
||||
return
|
||||
}
|
||||
_, err = conn.Write([]byte("PONG\n"))
|
||||
done <- err
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
if err := PingVSock(ctx, nil, socketPath); err != nil {
|
||||
t.Fatalf("PingVSock: %v", err)
|
||||
}
|
||||
if err := <-done; err != nil {
|
||||
t.Fatalf("server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type unexpectedStringError string
|
||||
|
||||
func (e unexpectedStringError) Error() string {
|
||||
return "unexpected string: " + string(e)
|
||||
}
|
||||
|
||||
func errUnexpectedString(value string) error {
|
||||
return unexpectedStringError(value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package guest
|
|||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -73,6 +74,11 @@ func (c *Client) RunScript(ctx context.Context, script string, logWriter io.Writ
|
|||
return c.runSession(ctx, "bash -se", strings.NewReader(script), logWriter)
|
||||
}
|
||||
|
||||
func (c *Client) UploadFile(ctx context.Context, remotePath string, mode os.FileMode, data []byte, logWriter io.Writer) error {
|
||||
command := fmt.Sprintf("install -D -m %04o /dev/stdin %s", mode.Perm(), shellQuote(remotePath))
|
||||
return c.runSession(ctx, command, bytes.NewReader(data), logWriter)
|
||||
}
|
||||
|
||||
func (c *Client) StreamTar(ctx context.Context, sourceDir, remoteCommand string, logWriter io.Writer) error {
|
||||
reader, writer := io.Pipe()
|
||||
writeErr := make(chan error, 1)
|
||||
|
|
@ -123,6 +129,10 @@ func privateKeySigner(path string) (ssh.Signer, error) {
|
|||
return ssh.ParsePrivateKey(data)
|
||||
}
|
||||
|
||||
func shellQuote(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
|
||||
}
|
||||
|
||||
func writeTarArchive(dst io.Writer, sourceDir string) error {
|
||||
tw := tar.NewWriter(dst)
|
||||
defer tw.Close()
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ type DaemonConfig struct {
|
|||
SSHKeyPath string
|
||||
NamegenPath string
|
||||
CustomizeScript string
|
||||
VSockPingHelperPath string
|
||||
AutoStopStaleAfter time.Duration
|
||||
StatsPollInterval time.Duration
|
||||
MetricsPollInterval time.Duration
|
||||
|
|
@ -87,6 +88,8 @@ type VMRuntime struct {
|
|||
GuestIP string `json:"guest_ip"`
|
||||
TapDevice string `json:"tap_device,omitempty"`
|
||||
APISockPath string `json:"api_sock_path,omitempty"`
|
||||
VSockPath string `json:"vsock_path,omitempty"`
|
||||
VSockCID uint32 `json:"vsock_cid,omitempty"`
|
||||
LogPath string `json:"log_path,omitempty"`
|
||||
MetricsPath string `json:"metrics_path,omitempty"`
|
||||
DNSName string `json:"dns_name,omitempty"`
|
||||
|
|
|
|||
|
|
@ -56,19 +56,21 @@ func TestResolveRuntimeDirUsesSourceCheckoutRuntimeSubdir(t *testing.T) {
|
|||
func createRuntimeBundle(t *testing.T, runtimeDir string) {
|
||||
t.Helper()
|
||||
metadata := runtimebundle.BundleMetadata{
|
||||
FirecrackerBin: "bin/firecracker",
|
||||
SSHKeyPath: "keys/id_ed25519",
|
||||
NamegenPath: "bin/namegen",
|
||||
CustomizeScript: "scripts/customize.sh",
|
||||
DefaultPackages: "config/packages.apt",
|
||||
DefaultRootfs: "images/rootfs-docker.ext4",
|
||||
DefaultKernel: "kernels/vmlinux",
|
||||
FirecrackerBin: "bin/firecracker",
|
||||
SSHKeyPath: "keys/id_ed25519",
|
||||
NamegenPath: "bin/namegen",
|
||||
CustomizeScript: "scripts/customize.sh",
|
||||
VSockPingHelperPath: "bin/banger-vsock-pingd",
|
||||
DefaultPackages: "config/packages.apt",
|
||||
DefaultRootfs: "images/rootfs-docker.ext4",
|
||||
DefaultKernel: "kernels/vmlinux",
|
||||
}
|
||||
for _, rel := range []string{
|
||||
metadata.FirecrackerBin,
|
||||
metadata.SSHKeyPath,
|
||||
metadata.NamegenPath,
|
||||
metadata.CustomizeScript,
|
||||
metadata.VSockPingHelperPath,
|
||||
metadata.DefaultPackages,
|
||||
metadata.DefaultRootfs,
|
||||
metadata.DefaultKernel,
|
||||
|
|
|
|||
|
|
@ -30,16 +30,17 @@ type Manifest struct {
|
|||
}
|
||||
|
||||
type BundleMetadata struct {
|
||||
FirecrackerBin string `json:"firecracker_bin" toml:"firecracker_bin"`
|
||||
SSHKeyPath string `json:"ssh_key_path" toml:"ssh_key_path"`
|
||||
NamegenPath string `json:"namegen_path" toml:"namegen_path"`
|
||||
CustomizeScript string `json:"customize_script" toml:"customize_script"`
|
||||
DefaultPackages string `json:"default_packages_file" toml:"default_packages_file"`
|
||||
DefaultRootfs string `json:"default_rootfs" toml:"default_rootfs"`
|
||||
DefaultBaseRootfs string `json:"default_base_rootfs,omitempty" toml:"default_base_rootfs"`
|
||||
DefaultKernel string `json:"default_kernel" toml:"default_kernel"`
|
||||
DefaultInitrd string `json:"default_initrd,omitempty" toml:"default_initrd"`
|
||||
DefaultModulesDir string `json:"default_modules_dir,omitempty" toml:"default_modules_dir"`
|
||||
FirecrackerBin string `json:"firecracker_bin" toml:"firecracker_bin"`
|
||||
SSHKeyPath string `json:"ssh_key_path" toml:"ssh_key_path"`
|
||||
NamegenPath string `json:"namegen_path" toml:"namegen_path"`
|
||||
CustomizeScript string `json:"customize_script" toml:"customize_script"`
|
||||
VSockPingHelperPath string `json:"vsock_ping_helper_path" toml:"vsock_ping_helper_path"`
|
||||
DefaultPackages string `json:"default_packages_file" toml:"default_packages_file"`
|
||||
DefaultRootfs string `json:"default_rootfs" toml:"default_rootfs"`
|
||||
DefaultBaseRootfs string `json:"default_base_rootfs,omitempty" toml:"default_base_rootfs"`
|
||||
DefaultKernel string `json:"default_kernel" toml:"default_kernel"`
|
||||
DefaultInitrd string `json:"default_initrd,omitempty" toml:"default_initrd"`
|
||||
DefaultModulesDir string `json:"default_modules_dir,omitempty" toml:"default_modules_dir"`
|
||||
}
|
||||
|
||||
const BundleMetadataFile = "bundle.json"
|
||||
|
|
@ -209,6 +210,7 @@ func validateBundleMetadata(runtimeDir string, meta BundleMetadata) error {
|
|||
{meta.SSHKeyPath, "ssh_key_path"},
|
||||
{meta.NamegenPath, "namegen_path"},
|
||||
{meta.CustomizeScript, "customize_script"},
|
||||
{meta.VSockPingHelperPath, "vsock_ping_helper_path"},
|
||||
{meta.DefaultPackages, "default_packages_file"},
|
||||
{meta.DefaultRootfs, "default_rootfs"},
|
||||
{meta.DefaultKernel, "default_kernel"},
|
||||
|
|
@ -227,6 +229,7 @@ func validateBundleMetadata(runtimeDir string, meta BundleMetadata) error {
|
|||
{meta.SSHKeyPath, "ssh_key_path", true},
|
||||
{meta.NamegenPath, "namegen_path", true},
|
||||
{meta.CustomizeScript, "customize_script", true},
|
||||
{meta.VSockPingHelperPath, "vsock_ping_helper_path", true},
|
||||
{meta.DefaultPackages, "default_packages_file", true},
|
||||
{meta.DefaultRootfs, "default_rootfs", true},
|
||||
{meta.DefaultBaseRootfs, "default_base_rootfs", false},
|
||||
|
|
@ -264,6 +267,7 @@ func metadataArchiveBytes(runtimeDir string, meta BundleMetadata) ([]byte, error
|
|||
strings.TrimSpace(meta.SSHKeyPath) == "" &&
|
||||
strings.TrimSpace(meta.NamegenPath) == "" &&
|
||||
strings.TrimSpace(meta.CustomizeScript) == "" &&
|
||||
strings.TrimSpace(meta.VSockPingHelperPath) == "" &&
|
||||
strings.TrimSpace(meta.DefaultPackages) == "" &&
|
||||
strings.TrimSpace(meta.DefaultRootfs) == "" &&
|
||||
strings.TrimSpace(meta.DefaultBaseRootfs) == "" &&
|
||||
|
|
@ -283,6 +287,7 @@ func normalizeBundleMetadata(meta BundleMetadata) BundleMetadata {
|
|||
meta.SSHKeyPath = strings.TrimSpace(meta.SSHKeyPath)
|
||||
meta.NamegenPath = strings.TrimSpace(meta.NamegenPath)
|
||||
meta.CustomizeScript = strings.TrimSpace(meta.CustomizeScript)
|
||||
meta.VSockPingHelperPath = strings.TrimSpace(meta.VSockPingHelperPath)
|
||||
meta.DefaultPackages = strings.TrimSpace(meta.DefaultPackages)
|
||||
meta.DefaultRootfs = strings.TrimSpace(meta.DefaultRootfs)
|
||||
meta.DefaultBaseRootfs = strings.TrimSpace(meta.DefaultBaseRootfs)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ func TestBootstrapExtractsBundleAndValidatesChecksum(t *testing.T) {
|
|||
"runtime/firecracker": "fc",
|
||||
"runtime/id_ed25519": "key",
|
||||
"runtime/namegen": "namegen",
|
||||
"runtime/banger-vsock-pingd": "pingd",
|
||||
"runtime/customize.sh": "#!/bin/bash\n",
|
||||
"runtime/packages.sh": "#!/bin/bash\n",
|
||||
"runtime/packages.apt": "vim\n",
|
||||
|
|
@ -27,7 +28,7 @@ func TestBootstrapExtractsBundleAndValidatesChecksum(t *testing.T) {
|
|||
"runtime/wtf/root/boot/vmlinux-6.8.0-94-generic": "kernel",
|
||||
"runtime/wtf/root/boot/initrd.img-6.8.0-94-generic": "initrd",
|
||||
"runtime/wtf/root/lib/modules/6.8.0-94-generic/modules.dep": "dep",
|
||||
"runtime/bundle.json": mustJSON(t, BundleMetadata{FirecrackerBin: "firecracker", SSHKeyPath: "id_ed25519", NamegenPath: "namegen", CustomizeScript: "customize.sh", DefaultPackages: "packages.apt", DefaultRootfs: "rootfs-docker.ext4", DefaultKernel: "wtf/root/boot/vmlinux-6.8.0-94-generic", DefaultInitrd: "wtf/root/boot/initrd.img-6.8.0-94-generic", DefaultModulesDir: "wtf/root/lib/modules/6.8.0-94-generic"}),
|
||||
"runtime/bundle.json": mustJSON(t, BundleMetadata{FirecrackerBin: "firecracker", SSHKeyPath: "id_ed25519", NamegenPath: "namegen", CustomizeScript: "customize.sh", VSockPingHelperPath: "banger-vsock-pingd", DefaultPackages: "packages.apt", DefaultRootfs: "rootfs-docker.ext4", DefaultKernel: "wtf/root/boot/vmlinux-6.8.0-94-generic", DefaultInitrd: "wtf/root/boot/initrd.img-6.8.0-94-generic", DefaultModulesDir: "wtf/root/lib/modules/6.8.0-94-generic"}),
|
||||
})
|
||||
archivePath := filepath.Join(manifestDir, "bundle.tar.gz")
|
||||
if err := os.WriteFile(archivePath, bundleData, 0o644); err != nil {
|
||||
|
|
@ -38,7 +39,7 @@ func TestBootstrapExtractsBundleAndValidatesChecksum(t *testing.T) {
|
|||
URL: "./bundle.tar.gz",
|
||||
SHA256: sha256Hex(bundleData),
|
||||
BundleRoot: "runtime",
|
||||
RequiredPaths: []string{"firecracker", "customize.sh", "packages.apt", "rootfs-docker.ext4", "wtf/root/boot/vmlinux-6.8.0-94-generic", "wtf/root/lib/modules/6.8.0-94-generic"},
|
||||
RequiredPaths: []string{"firecracker", "banger-vsock-pingd", "customize.sh", "packages.apt", "rootfs-docker.ext4", "wtf/root/boot/vmlinux-6.8.0-94-generic", "wtf/root/lib/modules/6.8.0-94-generic"},
|
||||
}
|
||||
outDir := filepath.Join(t.TempDir(), "runtime")
|
||||
if err := Bootstrap(context.Background(), manifest, filepath.Join(manifestDir, "runtime-bundle.toml"), outDir); err != nil {
|
||||
|
|
@ -99,6 +100,7 @@ func TestPackageWritesArchive(t *testing.T) {
|
|||
"firecracker",
|
||||
"id_ed25519",
|
||||
"namegen",
|
||||
"banger-vsock-pingd",
|
||||
"customize.sh",
|
||||
"packages.apt",
|
||||
"rootfs-docker.ext4",
|
||||
|
|
@ -126,20 +128,22 @@ func TestPackageWritesArchive(t *testing.T) {
|
|||
manifest := Manifest{
|
||||
BundleRoot: "runtime",
|
||||
BundleMeta: BundleMetadata{
|
||||
FirecrackerBin: "firecracker",
|
||||
SSHKeyPath: "id_ed25519",
|
||||
NamegenPath: "namegen",
|
||||
CustomizeScript: "customize.sh",
|
||||
DefaultPackages: "packages.apt",
|
||||
DefaultRootfs: "rootfs-docker.ext4",
|
||||
DefaultKernel: "wtf/root/boot/vmlinux-6.8.0-94-generic",
|
||||
DefaultInitrd: "wtf/root/boot/initrd.img-6.8.0-94-generic",
|
||||
DefaultModulesDir: "wtf/root/lib/modules/6.8.0-94-generic",
|
||||
FirecrackerBin: "firecracker",
|
||||
SSHKeyPath: "id_ed25519",
|
||||
NamegenPath: "namegen",
|
||||
CustomizeScript: "customize.sh",
|
||||
VSockPingHelperPath: "banger-vsock-pingd",
|
||||
DefaultPackages: "packages.apt",
|
||||
DefaultRootfs: "rootfs-docker.ext4",
|
||||
DefaultKernel: "wtf/root/boot/vmlinux-6.8.0-94-generic",
|
||||
DefaultInitrd: "wtf/root/boot/initrd.img-6.8.0-94-generic",
|
||||
DefaultModulesDir: "wtf/root/lib/modules/6.8.0-94-generic",
|
||||
},
|
||||
RequiredPaths: []string{
|
||||
"firecracker",
|
||||
"id_ed25519",
|
||||
"namegen",
|
||||
"banger-vsock-pingd",
|
||||
"customize.sh",
|
||||
"packages.apt",
|
||||
"rootfs-docker.ext4",
|
||||
|
|
@ -182,7 +186,7 @@ func TestPackageWritesArchive(t *testing.T) {
|
|||
|
||||
func TestLoadBundleMetadataRejectsMissingRequiredPath(t *testing.T) {
|
||||
runtimeDir := t.TempDir()
|
||||
for _, rel := range []string{"firecracker", "id_ed25519", "namegen", "customize.sh", "packages.apt", "rootfs-docker.ext4"} {
|
||||
for _, rel := range []string{"firecracker", "id_ed25519", "namegen", "banger-vsock-pingd", "customize.sh", "packages.apt", "rootfs-docker.ext4"} {
|
||||
path := filepath.Join(runtimeDir, rel)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll: %v", err)
|
||||
|
|
@ -192,13 +196,14 @@ func TestLoadBundleMetadataRejectsMissingRequiredPath(t *testing.T) {
|
|||
}
|
||||
}
|
||||
data := mustJSON(t, BundleMetadata{
|
||||
FirecrackerBin: "firecracker",
|
||||
SSHKeyPath: "id_ed25519",
|
||||
NamegenPath: "namegen",
|
||||
CustomizeScript: "customize.sh",
|
||||
DefaultPackages: "packages.apt",
|
||||
DefaultRootfs: "rootfs-docker.ext4",
|
||||
DefaultKernel: "missing-kernel",
|
||||
FirecrackerBin: "firecracker",
|
||||
SSHKeyPath: "id_ed25519",
|
||||
NamegenPath: "namegen",
|
||||
CustomizeScript: "customize.sh",
|
||||
VSockPingHelperPath: "banger-vsock-pingd",
|
||||
DefaultPackages: "packages.apt",
|
||||
DefaultRootfs: "rootfs-docker.ext4",
|
||||
DefaultKernel: "missing-kernel",
|
||||
})
|
||||
if err := os.WriteFile(filepath.Join(runtimeDir, BundleMetadataFile), []byte(data), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
|
|
|
|||
105
internal/vsockping/vsockping.go
Normal file
105
internal/vsockping/vsockping.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package vsockping
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdkvsock "github.com/firecracker-microvm/firecracker-go-sdk/vsock"
|
||||
)
|
||||
|
||||
const (
|
||||
Port uint32 = 42070
|
||||
RequestLine = "PING\n"
|
||||
ResponseLine = "PONG\n"
|
||||
GuestBinaryName = "banger-vsock-pingd"
|
||||
GuestInstallPath = "/usr/local/bin/" + GuestBinaryName
|
||||
ServiceName = "banger-vsock-pingd.service"
|
||||
serviceUnit = `[Unit]
|
||||
Description=Banger vsock ping responder
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/banger-vsock-pingd
|
||||
Restart=on-failure
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
modulesLoadConfig = "vsock\nvmw_vsock_virtio_transport\n"
|
||||
)
|
||||
|
||||
func Ping(ctx context.Context, logger *slog.Logger, socketPath string) error {
|
||||
conn, err := sdkvsock.DialContext(
|
||||
ctx,
|
||||
socketPath,
|
||||
Port,
|
||||
sdkvsock.WithRetryTimeout(3*time.Second),
|
||||
sdkvsock.WithRetryInterval(100*time.Millisecond),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = conn.SetDeadline(deadline)
|
||||
} else {
|
||||
_ = conn.SetDeadline(time.Now().Add(3 * time.Second))
|
||||
}
|
||||
|
||||
if _, err := io.WriteString(conn, RequestLine); err != nil {
|
||||
return err
|
||||
}
|
||||
line, err := bufio.NewReader(conn).ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(line) != strings.TrimSpace(ResponseLine) {
|
||||
return fmt.Errorf("unexpected vsock ping response %q", strings.TrimSpace(line))
|
||||
}
|
||||
if logger != nil {
|
||||
logger.Debug("vsock ping ok", "vsock_path", socketPath, "vsock_port", Port)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ServiceUnit() string {
|
||||
return serviceUnit
|
||||
}
|
||||
|
||||
func ModulesLoadConfig() string {
|
||||
return modulesLoadConfig
|
||||
}
|
||||
|
||||
func ReminderMessage(name string) string {
|
||||
return fmt.Sprintf("session ended; %s is still running (stop it with 'banger vm stop %s')", name, name)
|
||||
}
|
||||
|
||||
func WarningMessage(name string, err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("warning: failed to check whether %s is still running: %v", name, err)
|
||||
}
|
||||
|
||||
func ServeConn(conn net.Conn) error {
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
line, err := bufio.NewReader(conn).ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(line) != strings.TrimSpace(RequestLine) {
|
||||
return fmt.Errorf("unexpected request %q", strings.TrimSpace(line))
|
||||
}
|
||||
_, err = io.WriteString(conn, ResponseLine)
|
||||
return err
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ required_paths = [
|
|||
"customize.sh",
|
||||
"packages.sh",
|
||||
"namegen",
|
||||
"banger-vsock-pingd",
|
||||
"packages.apt",
|
||||
"id_ed25519",
|
||||
"rootfs-docker.ext4",
|
||||
|
|
@ -23,6 +24,7 @@ firecracker_bin = "firecracker"
|
|||
ssh_key_path = "id_ed25519"
|
||||
namegen_path = "namegen"
|
||||
customize_script = "customize.sh"
|
||||
vsock_ping_helper_path = "banger-vsock-pingd"
|
||||
default_packages_file = "packages.apt"
|
||||
default_rootfs = "rootfs-docker.ext4"
|
||||
default_kernel = "wtf/root/boot/vmlinux-6.8.0-94-generic"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue