refactor(maglevc): build the CLI on the golang-cli library
Replace maglevc's hand-rolled command-tree CLI with git.ipng.ch/ipng/golang-cli v1.3.0, mirroring the evpnc refactor. The tree (commands.go) and the gRPC-status error unwrap (color.go) stay app-specific; the generic parts — parse tree, completion, '?'-help, the readline shell, one-shot dispatch, color helpers, and the watch keypress handler — now come from the library. - main.go: a single cli.App[grpcapi.MaglevClient] with a Connect callback; drops the flag/color-default/dispatch boilerplate. - commands.go: `type node = cli.Node[grpcapi.MaglevClient]`; label() -> cli.Label(); dyn* gain the captured-args parameter the library's Dynamic signature carries; runQuit returns cli.ErrQuit. - watch.go: keypress.WaitForKey replaces the inline cbreak helper. - color.go: only formatError remains, reading cli.ColorEnabled(). - delete tree.go, complete.go, shell.go. - tests use the library API; add TestTreeValid (cli.Validate). Behavior is unchanged except labels/errors now use the library's bright ANSI palette (was dark); escape lengths are identical so tabwriter alignment is unaffected. maglevc additionally gains the OpenBSD readline fix and BSD-correct watch keypress it previously lacked. Builds on linux and openbsd. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+141
-135
@@ -11,82 +11,88 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
cli "git.ipng.ch/ipng/golang-cli"
|
||||
buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd"
|
||||
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
||||
)
|
||||
|
||||
const callTimeout = 10 * time.Second
|
||||
|
||||
// buildTree constructs the full command tree.
|
||||
func buildTree() *Node {
|
||||
root := &Node{Word: "", Help: ""}
|
||||
// node is the maglevc command tree node: a cli.Node specialized to the maglevd
|
||||
// gRPC client. The alias keeps the tree literals below concise (&node{...}
|
||||
// instead of &cli.Node[grpcapi.MaglevClient]{...}).
|
||||
type node = cli.Node[grpcapi.MaglevClient]
|
||||
|
||||
show := &Node{Word: "show", Help: "show information"}
|
||||
set := &Node{Word: "set", Help: "modify configuration"}
|
||||
quit := &Node{Word: "quit", Help: "exit the shell", Run: runQuit}
|
||||
exit := &Node{Word: "exit", Help: "exit the shell", Run: runQuit}
|
||||
// buildTree constructs the full command tree.
|
||||
func buildTree() *node {
|
||||
root := &node{Word: "", Help: ""}
|
||||
|
||||
show := &node{Word: "show", Help: "show information"}
|
||||
set := &node{Word: "set", Help: "modify configuration"}
|
||||
quit := &node{Word: "quit", Help: "exit the shell", Run: runQuit}
|
||||
exit := &node{Word: "exit", Help: "exit the shell", Run: runQuit}
|
||||
|
||||
// show version
|
||||
showVersion := &Node{Word: "version", Help: "Show build version", Run: runShowVersion}
|
||||
showVersion := &node{Word: "version", Help: "Show build version", Run: runShowVersion}
|
||||
|
||||
// show frontends [<name>] — without name: list all, with name: show details
|
||||
showFrontendName := &Node{
|
||||
showFrontendName := &node{
|
||||
Word: "<name>",
|
||||
Help: "Show details for a single frontend",
|
||||
Dynamic: dynFrontends,
|
||||
Run: runShowFrontend,
|
||||
}
|
||||
showFrontends := &Node{
|
||||
showFrontends := &node{
|
||||
Word: "frontends",
|
||||
Help: "List all frontends",
|
||||
Run: runShowFrontends,
|
||||
Children: []*Node{showFrontendName},
|
||||
Children: []*node{showFrontendName},
|
||||
}
|
||||
|
||||
// show backends [<name>] — without name: list all, with name: show details
|
||||
showBackendName := &Node{
|
||||
showBackendName := &node{
|
||||
Word: "<name>",
|
||||
Help: "Show details for a single backend",
|
||||
Dynamic: dynBackends,
|
||||
Run: runShowBackend,
|
||||
}
|
||||
showBackends := &Node{
|
||||
showBackends := &node{
|
||||
Word: "backends",
|
||||
Help: "List all backends",
|
||||
Run: runShowBackends,
|
||||
Children: []*Node{showBackendName},
|
||||
Children: []*node{showBackendName},
|
||||
}
|
||||
|
||||
// show healthchecks [<name>] — without name: list all, with name: show details
|
||||
showHealthCheckName := &Node{
|
||||
showHealthCheckName := &node{
|
||||
Word: "<name>",
|
||||
Help: "Show details for a single health check",
|
||||
Dynamic: dynHealthChecks,
|
||||
Run: runShowHealthCheck,
|
||||
}
|
||||
showHealthChecks := &Node{
|
||||
showHealthChecks := &node{
|
||||
Word: "healthchecks",
|
||||
Help: "List all health checks",
|
||||
Run: runShowHealthChecks,
|
||||
Children: []*Node{showHealthCheckName},
|
||||
Children: []*node{showHealthCheckName},
|
||||
}
|
||||
|
||||
// show vpp info / lb state / lb counters
|
||||
showVPPInfo := &Node{Word: "info", Help: "Show VPP version, uptime, and connection status", Run: runShowVPPInfo}
|
||||
showVPPLBState := &Node{Word: "state", Help: "Show VPP load-balancer state (VIPs and application servers)", Run: runShowVPPLBState}
|
||||
showVPPLBCounters := &Node{Word: "counters", Help: "Show VPP per-VIP and per-backend packet/byte counters (refreshed every ~5s server-side)", Run: runShowVPPLBCounters}
|
||||
showVPPLB := &Node{
|
||||
showVPPInfo := &node{Word: "info", Help: "Show VPP version, uptime, and connection status", Run: runShowVPPInfo}
|
||||
showVPPLBState := &node{Word: "state", Help: "Show VPP load-balancer state (VIPs and application servers)", Run: runShowVPPLBState}
|
||||
showVPPLBCounters := &node{Word: "counters", Help: "Show VPP per-VIP and per-backend packet/byte counters (refreshed every ~5s server-side)", Run: runShowVPPLBCounters}
|
||||
showVPPLB := &node{
|
||||
Word: "lb",
|
||||
Help: "VPP load-balancer information",
|
||||
Children: []*Node{showVPPLBState, showVPPLBCounters},
|
||||
Children: []*node{showVPPLBState, showVPPLBCounters},
|
||||
}
|
||||
showVPP := &Node{
|
||||
showVPP := &node{
|
||||
Word: "vpp",
|
||||
Help: "VPP dataplane information",
|
||||
Children: []*Node{showVPPInfo, showVPPLB},
|
||||
Children: []*node{showVPPInfo, showVPPLB},
|
||||
}
|
||||
|
||||
show.Children = []*Node{
|
||||
show.Children = []*node{
|
||||
showVersion,
|
||||
showFrontends,
|
||||
showBackends,
|
||||
@@ -95,20 +101,20 @@ func buildTree() *Node {
|
||||
}
|
||||
|
||||
// set backend <name> pause|resume|disabled|enabled
|
||||
setPause := &Node{Word: "pause", Help: "pause health checking", Run: runPauseBackend}
|
||||
setResume := &Node{Word: "resume", Help: "resume health checking", Run: runResumeBackend}
|
||||
setDisabled := &Node{Word: "disable", Help: "disable backend (stop probing, remove from rotation)", Run: runDisableBackend}
|
||||
setEnabled := &Node{Word: "enable", Help: "enable backend (resume probing)", Run: runEnableBackend}
|
||||
setBackendName := &Node{
|
||||
setPause := &node{Word: "pause", Help: "pause health checking", Run: runPauseBackend}
|
||||
setResume := &node{Word: "resume", Help: "resume health checking", Run: runResumeBackend}
|
||||
setDisabled := &node{Word: "disable", Help: "disable backend (stop probing, remove from rotation)", Run: runDisableBackend}
|
||||
setEnabled := &node{Word: "enable", Help: "enable backend (resume probing)", Run: runEnableBackend}
|
||||
setBackendName := &node{
|
||||
Word: "<name>",
|
||||
Help: "backend name",
|
||||
Dynamic: dynBackends,
|
||||
Children: []*Node{setPause, setResume, setDisabled, setEnabled},
|
||||
Children: []*node{setPause, setResume, setDisabled, setEnabled},
|
||||
}
|
||||
setBackend := &Node{
|
||||
setBackend := &node{
|
||||
Word: "backend",
|
||||
Help: "modify a backend",
|
||||
Children: []*Node{setBackendName},
|
||||
Children: []*node{setBackendName},
|
||||
}
|
||||
// set frontend <name> pool <pool> backend <name> weight <0-100> [flush]
|
||||
//
|
||||
@@ -116,120 +122,120 @@ func buildTree() *Node {
|
||||
// args, so the literal "flush" keyword isn't visible in the arg
|
||||
// list. We use two distinct Run functions to distinguish the two
|
||||
// leaf paths instead — both share the same underlying helper.
|
||||
setWeightFlush := &Node{
|
||||
setWeightFlush := &node{
|
||||
Word: "flush",
|
||||
Help: "also drop VPP's flow table for this backend (otherwise only the new-buckets map is updated)",
|
||||
Run: runSetFrontendPoolBackendWeightFlush,
|
||||
}
|
||||
setWeightValue := &Node{
|
||||
setWeightValue := &node{
|
||||
Word: "<weight>",
|
||||
Help: "Set weight of a backend in a pool (0-100)",
|
||||
Dynamic: dynNone, // accepts any integer; no tab-completion candidates
|
||||
Run: runSetFrontendPoolBackendWeight,
|
||||
Children: []*Node{setWeightFlush},
|
||||
Children: []*node{setWeightFlush},
|
||||
}
|
||||
setFrontendPoolBackendWeight := &Node{Word: "weight", Help: "set backend weight in pool", Children: []*Node{setWeightValue}}
|
||||
setFrontendPoolBackendName := &Node{
|
||||
setFrontendPoolBackendWeight := &node{Word: "weight", Help: "set backend weight in pool", Children: []*node{setWeightValue}}
|
||||
setFrontendPoolBackendName := &node{
|
||||
Word: "<backend>",
|
||||
Help: "backend name",
|
||||
Dynamic: dynBackends,
|
||||
Children: []*Node{setFrontendPoolBackendWeight},
|
||||
Children: []*node{setFrontendPoolBackendWeight},
|
||||
}
|
||||
setFrontendPoolBackend := &Node{Word: "backend", Help: "select a backend", Children: []*Node{setFrontendPoolBackendName}}
|
||||
setFrontendPoolName := &Node{
|
||||
setFrontendPoolBackend := &node{Word: "backend", Help: "select a backend", Children: []*node{setFrontendPoolBackendName}}
|
||||
setFrontendPoolName := &node{
|
||||
Word: "<pool>",
|
||||
Help: "pool name",
|
||||
Dynamic: dynNone, // pool names aren't listed via gRPC; accepts any input
|
||||
Children: []*Node{setFrontendPoolBackend},
|
||||
Children: []*node{setFrontendPoolBackend},
|
||||
}
|
||||
setFrontendPool := &Node{Word: "pool", Help: "select a pool", Children: []*Node{setFrontendPoolName}}
|
||||
setFrontendName := &Node{
|
||||
setFrontendPool := &node{Word: "pool", Help: "select a pool", Children: []*node{setFrontendPoolName}}
|
||||
setFrontendName := &node{
|
||||
Word: "<name>",
|
||||
Help: "frontend name",
|
||||
Dynamic: dynFrontends,
|
||||
Children: []*Node{setFrontendPool},
|
||||
Children: []*node{setFrontendPool},
|
||||
}
|
||||
setFrontend := &Node{
|
||||
setFrontend := &node{
|
||||
Word: "frontend",
|
||||
Help: "modify a frontend",
|
||||
Children: []*Node{setFrontendName},
|
||||
Children: []*node{setFrontendName},
|
||||
}
|
||||
|
||||
set.Children = []*Node{setBackend, setFrontend}
|
||||
set.Children = []*node{setBackend, setFrontend}
|
||||
|
||||
// watch events [num <n>] [log [level <level>]] [backend] [frontend]
|
||||
//
|
||||
// All tokens after 'events' are captured as args via a self-referencing slot
|
||||
// node. This lets runWatchEvents parse the optional flags manually while still
|
||||
// providing tab-completion through the dynamic enumerator.
|
||||
watchEventsOptSlot := &Node{
|
||||
watchEventsOptSlot := &node{
|
||||
Word: "<opt>",
|
||||
Help: "Stream events with options",
|
||||
Dynamic: dynWatchEventOpts,
|
||||
Run: runWatchEvents,
|
||||
}
|
||||
watchEventsOptSlot.Children = []*Node{watchEventsOptSlot}
|
||||
watchEventsOptSlot.Children = []*node{watchEventsOptSlot}
|
||||
|
||||
watchEvents := &Node{
|
||||
watchEvents := &node{
|
||||
Word: "events",
|
||||
Help: "stream events (press any key or Ctrl-C to stop)",
|
||||
Run: runWatchEvents,
|
||||
Children: []*Node{watchEventsOptSlot},
|
||||
Children: []*node{watchEventsOptSlot},
|
||||
}
|
||||
watch := &Node{
|
||||
watch := &node{
|
||||
Word: "watch",
|
||||
Help: "watch live event streams",
|
||||
Children: []*Node{watchEvents},
|
||||
Children: []*node{watchEvents},
|
||||
}
|
||||
|
||||
// config check / reload
|
||||
configCheck := &Node{Word: "check", Help: "Check configuration file", Run: runConfigCheck}
|
||||
configReload := &Node{Word: "reload", Help: "Check and reload configuration", Run: runConfigReload}
|
||||
configNode := &Node{
|
||||
configCheck := &node{Word: "check", Help: "Check configuration file", Run: runConfigCheck}
|
||||
configReload := &node{Word: "reload", Help: "Check and reload configuration", Run: runConfigReload}
|
||||
configNode := &node{
|
||||
Word: "config",
|
||||
Help: "configuration commands",
|
||||
Children: []*Node{configCheck, configReload},
|
||||
Children: []*node{configCheck, configReload},
|
||||
}
|
||||
|
||||
// sync vpp lb state [<name>]
|
||||
//
|
||||
// Without a name: run SyncLBStateAll (may remove stale VIPs).
|
||||
// With a name: run SyncLBStateVIP(name) for just that frontend (no removals).
|
||||
syncVPPLBStateName := &Node{
|
||||
syncVPPLBStateName := &node{
|
||||
Word: "<name>",
|
||||
Help: "Sync a single frontend's VIP to VPP",
|
||||
Dynamic: dynFrontends,
|
||||
Run: runSyncVPPLBState,
|
||||
}
|
||||
syncVPPLBState := &Node{
|
||||
syncVPPLBState := &node{
|
||||
Word: "state",
|
||||
Help: "Sync the VPP load-balancer dataplane from the running config",
|
||||
Run: runSyncVPPLBState,
|
||||
Children: []*Node{syncVPPLBStateName},
|
||||
Children: []*node{syncVPPLBStateName},
|
||||
}
|
||||
syncVPPLB := &Node{
|
||||
syncVPPLB := &node{
|
||||
Word: "lb",
|
||||
Help: "VPP load-balancer sync commands",
|
||||
Children: []*Node{syncVPPLBState},
|
||||
Children: []*node{syncVPPLBState},
|
||||
}
|
||||
syncVPP := &Node{
|
||||
syncVPP := &node{
|
||||
Word: "vpp",
|
||||
Help: "VPP dataplane sync commands",
|
||||
Children: []*Node{syncVPPLB},
|
||||
Children: []*node{syncVPPLB},
|
||||
}
|
||||
syncNode := &Node{
|
||||
syncNode := &node{
|
||||
Word: "sync",
|
||||
Help: "Reconcile dataplane state from the running config",
|
||||
Children: []*Node{syncVPP},
|
||||
Children: []*node{syncVPP},
|
||||
}
|
||||
|
||||
root.Children = []*Node{show, set, watch, configNode, syncNode, quit, exit}
|
||||
root.Children = []*node{show, set, watch, configNode, syncNode, quit, exit}
|
||||
return root
|
||||
}
|
||||
|
||||
// ---- dynamic enumerators ---------------------------------------------------
|
||||
|
||||
func dynFrontends(ctx context.Context, client grpcapi.MaglevClient) []string {
|
||||
func dynFrontends(ctx context.Context, client grpcapi.MaglevClient, _ []string) []string {
|
||||
resp, err := client.ListFrontends(ctx, &grpcapi.ListFrontendsRequest{})
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -237,7 +243,7 @@ func dynFrontends(ctx context.Context, client grpcapi.MaglevClient) []string {
|
||||
return resp.FrontendNames
|
||||
}
|
||||
|
||||
func dynBackends(ctx context.Context, client grpcapi.MaglevClient) []string {
|
||||
func dynBackends(ctx context.Context, client grpcapi.MaglevClient, _ []string) []string {
|
||||
resp, err := client.ListBackends(ctx, &grpcapi.ListBackendsRequest{})
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -245,7 +251,7 @@ func dynBackends(ctx context.Context, client grpcapi.MaglevClient) []string {
|
||||
return resp.BackendNames
|
||||
}
|
||||
|
||||
func dynHealthChecks(ctx context.Context, client grpcapi.MaglevClient) []string {
|
||||
func dynHealthChecks(ctx context.Context, client grpcapi.MaglevClient, _ []string) []string {
|
||||
resp, err := client.ListHealthChecks(ctx, &grpcapi.ListHealthChecksRequest{})
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -255,7 +261,7 @@ func dynHealthChecks(ctx context.Context, client grpcapi.MaglevClient) []string
|
||||
|
||||
// dynNone marks a slot node that accepts any input but provides no
|
||||
// tab-completion candidates (e.g. a pool name or numeric weight value).
|
||||
func dynNone(_ context.Context, _ grpcapi.MaglevClient) []string { return nil }
|
||||
func dynNone(_ context.Context, _ grpcapi.MaglevClient, _ []string) []string { return nil }
|
||||
|
||||
// ---- run functions ---------------------------------------------------------
|
||||
|
||||
@@ -267,18 +273,18 @@ func runShowVPPInfo(ctx context.Context, client grpcapi.MaglevClient, _ []string
|
||||
return err
|
||||
}
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("version"), info.Version)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("build-date"), info.BuildDate)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("build-dir"), info.BuildDirectory)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%d\n", label("vpp-pid"), info.Pid)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("version"), info.Version)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("build-date"), info.BuildDate)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("build-dir"), info.BuildDirectory)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%d\n", cli.Label("vpp-pid"), info.Pid)
|
||||
if info.BoottimeNs > 0 {
|
||||
bootTime := time.Unix(0, info.BoottimeNs)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s (%s)\n", label("vpp-boottime"),
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s (%s)\n", cli.Label("vpp-boottime"),
|
||||
bootTime.Format("2006-01-02 15:04:05"),
|
||||
formatDuration(time.Since(bootTime)))
|
||||
}
|
||||
connTime := time.Unix(0, info.ConnecttimeNs)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s (%s)\n", label("connected"),
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s (%s)\n", cli.Label("connected"),
|
||||
connTime.Format("2006-01-02 15:04:05"),
|
||||
formatDuration(time.Since(connTime)))
|
||||
return w.Flush()
|
||||
@@ -294,21 +300,21 @@ func runShowVPPLBState(ctx context.Context, client grpcapi.MaglevClient, _ []str
|
||||
|
||||
// ---- global config ----
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "%s\n", label("global"))
|
||||
_, _ = fmt.Fprintf(w, "%s\n", cli.Label("global"))
|
||||
if state.Conf.Ip4SrcAddress != "" {
|
||||
_, _ = fmt.Fprintf(w, " %s\t%s\n", label("ip4-src"), state.Conf.Ip4SrcAddress)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%s\n", cli.Label("ip4-src"), state.Conf.Ip4SrcAddress)
|
||||
}
|
||||
if state.Conf.Ip6SrcAddress != "" {
|
||||
_, _ = fmt.Fprintf(w, " %s\t%s\n", label("ip6-src"), state.Conf.Ip6SrcAddress)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%s\n", cli.Label("ip6-src"), state.Conf.Ip6SrcAddress)
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, " %s\t%d\n", label("sticky-buckets-per-core"), state.Conf.StickyBucketsPerCore)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%ds\n", label("flow-timeout"), state.Conf.FlowTimeout)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%d\n", cli.Label("sticky-buckets-per-core"), state.Conf.StickyBucketsPerCore)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%ds\n", cli.Label("flow-timeout"), state.Conf.FlowTimeout)
|
||||
if err := w.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(state.Vips) == 0 {
|
||||
fmt.Println(label("vips") + " (none)")
|
||||
fmt.Println(cli.Label("vips") + " (none)")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -316,21 +322,21 @@ func runShowVPPLBState(ctx context.Context, client grpcapi.MaglevClient, _ []str
|
||||
for _, v := range state.Vips {
|
||||
fmt.Println()
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("vip"), stripHostMask(v.Prefix))
|
||||
_, _ = fmt.Fprintf(w, " %s\t%s\n", label("protocol"), protoString(v.Protocol))
|
||||
_, _ = fmt.Fprintf(w, " %s\t%d\n", label("port"), v.Port)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%s\n", label("encap"), v.Encap)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%t\n", label("src-ip-sticky"), v.SrcIpSticky)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%d\n", label("flow-table-length"), v.FlowTableLength)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%d\n", label("application-servers"), len(v.ApplicationServers))
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("vip"), stripHostMask(v.Prefix))
|
||||
_, _ = fmt.Fprintf(w, " %s\t%s\n", cli.Label("protocol"), protoString(v.Protocol))
|
||||
_, _ = fmt.Fprintf(w, " %s\t%d\n", cli.Label("port"), v.Port)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%s\n", cli.Label("encap"), v.Encap)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%t\n", cli.Label("src-ip-sticky"), v.SrcIpSticky)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%d\n", cli.Label("flow-table-length"), v.FlowTableLength)
|
||||
_, _ = fmt.Fprintf(w, " %s\t%d\n", cli.Label("application-servers"), len(v.ApplicationServers))
|
||||
if err := w.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, a := range v.ApplicationServers {
|
||||
fmt.Printf(" %s %s %s %d %s %d\n",
|
||||
label("address"), a.Address,
|
||||
label("weight"), a.Weight,
|
||||
label("flow-table-buckets"), a.NumBuckets)
|
||||
cli.Label("address"), a.Address,
|
||||
cli.Label("weight"), a.Weight,
|
||||
cli.Label("flow-table-buckets"), a.NumBuckets)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -356,15 +362,15 @@ func runShowVPPLBCounters(ctx context.Context, client grpcapi.MaglevClient, _ []
|
||||
|
||||
// ---- frontend-counters ----
|
||||
//
|
||||
// Column headers are plain strings, not label()-wrapped. tabwriter
|
||||
// Column headers are plain strings, not cli.Label()-wrapped. tabwriter
|
||||
// counts bytes (not rendered width), so wrapping a header cell in
|
||||
// ANSI escape codes inflates its apparent width by ~11 bytes and
|
||||
// the data rows below — which are plain numeric strings — end up
|
||||
// over-padded. The label() convention only works when every cell
|
||||
// over-padded. The cli.Label() convention only works when every cell
|
||||
// in a column shares the same wrapping, which the key-value show
|
||||
// commands do but this table can't (we're not about to colourise
|
||||
// every packet count).
|
||||
fmt.Println(label("frontend-counters"))
|
||||
fmt.Println(cli.Label("frontend-counters"))
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, " vip\tproto\tport\tfirst\tnext\tuntracked\tno-server\tfib-packets\tfib-bytes\n")
|
||||
for _, v := range resp.Vips {
|
||||
@@ -429,7 +435,7 @@ func runShowVersion(_ context.Context, _ grpcapi.MaglevClient, _ []string) error
|
||||
}
|
||||
|
||||
func runQuit(_ context.Context, _ grpcapi.MaglevClient, _ []string) error {
|
||||
return errQuit
|
||||
return cli.ErrQuit
|
||||
}
|
||||
|
||||
func runShowFrontends(ctx context.Context, client grpcapi.MaglevClient, _ []string) error {
|
||||
@@ -457,17 +463,17 @@ func runShowFrontend(ctx context.Context, client grpcapi.MaglevClient, args []st
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("name"), info.Name)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("address"), info.Address)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("protocol"), info.Protocol)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%d\n", label("port"), info.Port)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%t\n", label("src-ip-sticky"), info.SrcIpSticky)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%t\n", label("flush-on-down"), info.FlushOnDown)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("name"), info.Name)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("address"), info.Address)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("protocol"), info.Protocol)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%d\n", cli.Label("port"), info.Port)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%t\n", cli.Label("src-ip-sticky"), info.SrcIpSticky)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%t\n", cli.Label("flush-on-down"), info.FlushOnDown)
|
||||
if info.Description != "" {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("description"), info.Description)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("description"), info.Description)
|
||||
}
|
||||
if len(info.Pools) > 0 {
|
||||
_, _ = fmt.Fprintf(w, "%s\n", label("pools"))
|
||||
_, _ = fmt.Fprintf(w, "%s\n", cli.Label("pools"))
|
||||
}
|
||||
if err := w.Flush(); err != nil {
|
||||
return err
|
||||
@@ -484,7 +490,7 @@ func runShowFrontend(ctx context.Context, client grpcapi.MaglevClient, args []st
|
||||
|
||||
for _, pool := range info.Pools {
|
||||
namePad := strings.Repeat(" ", poolLblWidth-len("name"))
|
||||
fmt.Printf("%s%s%s%s%s\n", poolIndent, label("name"), namePad, poolSep, pool.Name)
|
||||
fmt.Printf("%s%s%s%s%s\n", poolIndent, cli.Label("name"), namePad, poolSep, pool.Name)
|
||||
for i, pb := range pool.Backends {
|
||||
beInfo, beErr := client.GetBackend(ctx, &grpcapi.GetBackendRequest{Name: pb.Name})
|
||||
suffix := ""
|
||||
@@ -496,11 +502,11 @@ func runShowFrontend(ctx context.Context, client grpcapi.MaglevClient, args []st
|
||||
// after pool-failover logic). Format matches the VPP-style
|
||||
// key-value line so robot tests can parse it with a regex.
|
||||
metaStr := fmt.Sprintf(" %s %d %s %d",
|
||||
label("weight"), pb.Weight,
|
||||
label("effective"), pb.EffectiveWeight)
|
||||
cli.Label("weight"), pb.Weight,
|
||||
cli.Label("effective"), pb.EffectiveWeight)
|
||||
if i == 0 {
|
||||
bePad := strings.Repeat(" ", poolLblWidth-len("backends"))
|
||||
fmt.Printf("%s%s%s%s%s%s%s\n", poolIndent, label("backends"), bePad, poolSep, pb.Name, metaStr, suffix)
|
||||
fmt.Printf("%s%s%s%s%s%s%s\n", poolIndent, cli.Label("backends"), bePad, poolSep, pb.Name, metaStr, suffix)
|
||||
} else {
|
||||
fmt.Printf("%s%s%s%s\n", contIndent, pb.Name, metaStr, suffix)
|
||||
}
|
||||
@@ -533,26 +539,26 @@ func runShowBackend(ctx context.Context, client grpcapi.MaglevClient, args []str
|
||||
return err
|
||||
}
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("name"), info.Name)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("address"), info.Address)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("name"), info.Name)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("address"), info.Address)
|
||||
stateDur := ""
|
||||
if len(info.Transitions) > 0 {
|
||||
since := time.Since(time.Unix(0, info.Transitions[0].AtUnixNs))
|
||||
stateDur = " for " + formatDuration(since)
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s%s\n", label("state"), info.State, stateDur)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%v\n", label("enabled"), info.Enabled)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("healthcheck"), info.Healthcheck)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s%s\n", cli.Label("state"), info.State, stateDur)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%v\n", cli.Label("enabled"), info.Enabled)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("healthcheck"), info.Healthcheck)
|
||||
for i, t := range info.Transitions {
|
||||
ts := time.Unix(0, t.AtUnixNs)
|
||||
var lbl string
|
||||
if i == 0 {
|
||||
lbl = label("transitions")
|
||||
lbl = cli.Label("transitions")
|
||||
} else {
|
||||
// Pad to same visible width as "transitions" and wrap through
|
||||
// label() so tabwriter sees the same byte count (ANSI overhead
|
||||
// cli.Label() so tabwriter sees the same byte count (ANSI overhead
|
||||
// is identical on every row, keeping columns aligned).
|
||||
lbl = label(" ")
|
||||
lbl = cli.Label(" ")
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s → %s\t%s\t%s\n",
|
||||
lbl,
|
||||
@@ -588,41 +594,41 @@ func runShowHealthCheck(ctx context.Context, client grpcapi.MaglevClient, args [
|
||||
return err
|
||||
}
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("name"), info.Name)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("type"), info.Type)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("name"), info.Name)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("type"), info.Type)
|
||||
if info.Port > 0 {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%d\n", label("port"), info.Port)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%d\n", cli.Label("port"), info.Port)
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("interval"), time.Duration(info.IntervalNs))
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("interval"), time.Duration(info.IntervalNs))
|
||||
if info.FastIntervalNs > 0 {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("fast-interval"), time.Duration(info.FastIntervalNs))
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("fast-interval"), time.Duration(info.FastIntervalNs))
|
||||
}
|
||||
if info.DownIntervalNs > 0 {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("down-interval"), time.Duration(info.DownIntervalNs))
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("down-interval"), time.Duration(info.DownIntervalNs))
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("timeout"), time.Duration(info.TimeoutNs))
|
||||
_, _ = fmt.Fprintf(w, "%s\t%d\n", label("rise"), info.Rise)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%d\n", label("fall"), info.Fall)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("timeout"), time.Duration(info.TimeoutNs))
|
||||
_, _ = fmt.Fprintf(w, "%s\t%d\n", cli.Label("rise"), info.Rise)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%d\n", cli.Label("fall"), info.Fall)
|
||||
if info.ProbeIpv4Src != "" {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("probe-ipv4-src"), info.ProbeIpv4Src)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("probe-ipv4-src"), info.ProbeIpv4Src)
|
||||
}
|
||||
if info.ProbeIpv6Src != "" {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("probe-ipv6-src"), info.ProbeIpv6Src)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("probe-ipv6-src"), info.ProbeIpv6Src)
|
||||
}
|
||||
if h := info.Http; h != nil {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("http.path"), h.Path)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("http.path"), h.Path)
|
||||
if h.Host != "" {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("http.host"), h.Host)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("http.host"), h.Host)
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%d-%d\n", label("http.response-code"), h.ResponseCodeMin, h.ResponseCodeMax)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%d-%d\n", cli.Label("http.response-code"), h.ResponseCodeMin, h.ResponseCodeMax)
|
||||
if h.ResponseRegexp != "" {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("http.response-regexp"), h.ResponseRegexp)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("http.response-regexp"), h.ResponseRegexp)
|
||||
}
|
||||
}
|
||||
if t := info.Tcp; t != nil {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%v\n", label("tcp.ssl"), t.Ssl)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%v\n", cli.Label("tcp.ssl"), t.Ssl)
|
||||
if t.ServerName != "" {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", label("tcp.server-name"), t.ServerName)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", cli.Label("tcp.server-name"), t.ServerName)
|
||||
}
|
||||
}
|
||||
return w.Flush()
|
||||
|
||||
Reference in New Issue
Block a user