Add vsock-backed VM port inspection
Let the host ask the guest vsock agent to run ss so open ports can be surfaced without SSHing in manually. Add a narrow /ports agent endpoint, a daemon vm.ports RPC that enriches listeners with <hostname>.vm endpoints and best-effort HTTP links, and a concurrent 'banger vm ports' CLI table for one or more VMs. Update the guest package contract to include ss for rebuilt Debian images, allow the guest agent package in the shell-out policy, and cover the new parsing/RPC/CLI flow in tests. Verified with GOCACHE=/tmp/banger-gocache go test ./... outside the sandbox, make build, bash -n customize.sh make-rootfs-void.sh verify.sh, and ./banger vm ports --help.
This commit is contained in:
parent
3ed78fdcfc
commit
c298ed2fc1
11 changed files with 1029 additions and 23 deletions
|
|
@ -9,6 +9,7 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
|
@ -45,6 +46,9 @@ var (
|
|||
vmHealthFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMHealthResult, error) {
|
||||
return rpc.Call[api.VMHealthResult](ctx, socketPath, "vm.health", api.VMRefParams{IDOrName: idOrName})
|
||||
}
|
||||
vmPortsFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMPortsResult, error) {
|
||||
return rpc.Call[api.VMPortsResult](ctx, socketPath, "vm.ports", api.VMRefParams{IDOrName: idOrName})
|
||||
}
|
||||
)
|
||||
|
||||
func NewBangerCommand() *cobra.Command {
|
||||
|
|
@ -243,6 +247,7 @@ func newVMCommand() *cobra.Command {
|
|||
newVMSSHCommand(),
|
||||
newVMLogsCommand(),
|
||||
newVMStatsCommand(),
|
||||
newVMPortsCommand(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -542,6 +547,50 @@ func newVMStatsCommand() *cobra.Command {
|
|||
}
|
||||
}
|
||||
|
||||
func newVMPortsCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "ports <id-or-name>...",
|
||||
Short: "Show host-reachable listening guest ports",
|
||||
Args: minArgsUsage(1, "usage: banger vm ports <id-or-name>..."),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
layout, _, err := ensureDaemon(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listResult, err := rpc.Call[api.VMListResult](cmd.Context(), layout.SocketPath, "vm.list", api.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targets, resolutionErrs := resolveVMTargets(listResult.VMs, args)
|
||||
results := executeVMPortsBatch(cmd.Context(), layout.SocketPath, targets)
|
||||
|
||||
failed := false
|
||||
for _, resolutionErr := range resolutionErrs {
|
||||
if _, err := fmt.Fprintf(cmd.ErrOrStderr(), "%s: %v\n", resolutionErr.Ref, resolutionErr.Err); err != nil {
|
||||
return err
|
||||
}
|
||||
failed = true
|
||||
}
|
||||
for _, result := range results {
|
||||
if result.Err == nil {
|
||||
continue
|
||||
}
|
||||
if _, err := fmt.Fprintf(cmd.ErrOrStderr(), "%s: %v\n", result.Target.Ref, result.Err); err != nil {
|
||||
return err
|
||||
}
|
||||
failed = true
|
||||
}
|
||||
if err := printVMPortsTable(cmd.OutOrStdout(), results); err != nil {
|
||||
return err
|
||||
}
|
||||
if failed {
|
||||
return errors.New("one or more VM operations failed")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newImageCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "image",
|
||||
|
|
@ -744,6 +793,12 @@ type vmBatchActionResult struct {
|
|||
Err error
|
||||
}
|
||||
|
||||
type vmPortsBatchResult struct {
|
||||
Target resolvedVMTarget
|
||||
Result api.VMPortsResult
|
||||
Err error
|
||||
}
|
||||
|
||||
func runVMBatchAction(cmd *cobra.Command, socketPath string, refs []string, action func(context.Context, string) (model.VMRecord, error)) error {
|
||||
listResult, err := rpc.Call[api.VMListResult](cmd.Context(), socketPath, "vm.list", api.Empty{})
|
||||
if err != nil {
|
||||
|
|
@ -852,6 +907,27 @@ func executeVMActionBatch(ctx context.Context, targets []resolvedVMTarget, actio
|
|||
return results
|
||||
}
|
||||
|
||||
func executeVMPortsBatch(ctx context.Context, socketPath string, targets []resolvedVMTarget) []vmPortsBatchResult {
|
||||
results := make([]vmPortsBatchResult, len(targets))
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(targets))
|
||||
for index, target := range targets {
|
||||
index := index
|
||||
target := target
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := vmPortsFunc(ctx, socketPath, target.VM.ID)
|
||||
results[index] = vmPortsBatchResult{
|
||||
Target: target,
|
||||
Result: result,
|
||||
Err: err,
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
|
||||
func ensureDaemon(ctx context.Context) (paths.Layout, model.DaemonConfig, error) {
|
||||
layout, err := paths.Resolve()
|
||||
if err != nil {
|
||||
|
|
@ -1147,6 +1223,77 @@ func printImageSummary(out anyWriter, image model.Image) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func printVMPortsTable(out anyWriter, results []vmPortsBatchResult) error {
|
||||
type portRow struct {
|
||||
VM string
|
||||
Proto string
|
||||
Endpoint string
|
||||
Process string
|
||||
Command string
|
||||
WebURL string
|
||||
Port int
|
||||
}
|
||||
rows := make([]portRow, 0)
|
||||
for _, result := range results {
|
||||
if result.Err != nil {
|
||||
continue
|
||||
}
|
||||
vmName := strings.TrimSpace(result.Result.Name)
|
||||
if vmName == "" {
|
||||
vmName = result.Target.VM.Name
|
||||
}
|
||||
for _, port := range result.Result.Ports {
|
||||
rows = append(rows, portRow{
|
||||
VM: vmName,
|
||||
Proto: port.Proto,
|
||||
Endpoint: port.Endpoint,
|
||||
Process: port.Process,
|
||||
Command: port.Command,
|
||||
WebURL: emptyDash(port.WebURL),
|
||||
Port: port.Port,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
if rows[i].VM != rows[j].VM {
|
||||
return rows[i].VM < rows[j].VM
|
||||
}
|
||||
if rows[i].Proto != rows[j].Proto {
|
||||
return rows[i].Proto < rows[j].Proto
|
||||
}
|
||||
if rows[i].Port != rows[j].Port {
|
||||
return rows[i].Port < rows[j].Port
|
||||
}
|
||||
if rows[i].Process != rows[j].Process {
|
||||
return rows[i].Process < rows[j].Process
|
||||
}
|
||||
return rows[i].Command < rows[j].Command
|
||||
})
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
|
||||
if _, err := fmt.Fprintln(w, "VM\tPROTO\tENDPOINT\tPROCESS\tCOMMAND\tWEB"); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
if _, err := fmt.Fprintf(
|
||||
w,
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||
row.VM,
|
||||
row.Proto,
|
||||
emptyDash(row.Endpoint),
|
||||
emptyDash(row.Process),
|
||||
emptyDash(row.Command),
|
||||
row.WebURL,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
func printDoctorReport(out anyWriter, report system.Report) error {
|
||||
for _, check := range report.Checks {
|
||||
status := strings.ToUpper(string(check.Status))
|
||||
|
|
@ -1162,6 +1309,14 @@ func printDoctorReport(out anyWriter, report system.Report) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func emptyDash(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return "-"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
type anyWriter interface {
|
||||
Write(p []byte) (n int, err error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,6 +151,17 @@ func TestVMKillFlagsExist(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestVMPortsCommandExists(t *testing.T) {
|
||||
root := NewBangerCommand()
|
||||
vm, _, err := root.Find([]string{"vm"})
|
||||
if err != nil {
|
||||
t.Fatalf("find vm: %v", err)
|
||||
}
|
||||
if _, _, err := vm.Find([]string{"ports"}); err != nil {
|
||||
t.Fatalf("find ports: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMSetParamsFromFlags(t *testing.T) {
|
||||
params, err := vmSetParamsFromFlags("devbox", 4, 2048, "16G", true, false)
|
||||
if err != nil {
|
||||
|
|
@ -268,6 +279,59 @@ func TestAbsolutizeImageRegisterPaths(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPrintVMPortsTableSortsAndRendersURLs(t *testing.T) {
|
||||
results := []vmPortsBatchResult{
|
||||
{
|
||||
Target: resolvedVMTarget{Ref: "beta"},
|
||||
Result: api.VMPortsResult{
|
||||
Name: "beta",
|
||||
Ports: []api.VMPort{{
|
||||
Proto: "tcp",
|
||||
Port: 8080,
|
||||
Endpoint: "beta.vm:8080",
|
||||
Process: "python3",
|
||||
Command: "python3 -m http.server 8080",
|
||||
WebURL: "http://beta.vm:8080/",
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
Target: resolvedVMTarget{Ref: "alpha"},
|
||||
Result: api.VMPortsResult{
|
||||
Name: "alpha",
|
||||
Ports: []api.VMPort{{
|
||||
Proto: "udp",
|
||||
Port: 53,
|
||||
Endpoint: "alpha.vm:53",
|
||||
Process: "dnsd",
|
||||
Command: "dnsd --foreground",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := printVMPortsTable(&out, results); err != nil {
|
||||
t.Fatalf("printVMPortsTable: %v", err)
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
|
||||
if len(lines) != 3 {
|
||||
t.Fatalf("lines = %q, want header + 2 rows", lines)
|
||||
}
|
||||
if !strings.Contains(lines[0], "VM") || !strings.Contains(lines[0], "WEB") {
|
||||
t.Fatalf("header = %q, want VM/WEB columns", lines[0])
|
||||
}
|
||||
if !strings.Contains(lines[1], "alpha") || !strings.Contains(lines[1], "alpha.vm:53") || !strings.Contains(lines[1], "\t-\n") {
|
||||
// tabwriter output is space-expanded, so just require the dash placeholder.
|
||||
if !strings.Contains(lines[1], "alpha") || !strings.Contains(lines[1], "alpha.vm:53") || !strings.HasSuffix(strings.TrimSpace(lines[1]), "-") {
|
||||
t.Fatalf("first row = %q, want alpha row with dash web column", lines[1])
|
||||
}
|
||||
}
|
||||
if !strings.Contains(lines[2], "beta") || !strings.Contains(lines[2], "http://beta.vm:8080/") {
|
||||
t.Fatalf("second row = %q, want beta web url", lines[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSSHSessionPrintsReminderWhenHealthCheckPasses(t *testing.T) {
|
||||
origSSHExec := sshExecFunc
|
||||
origHealth := vmHealthFunc
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue