// Copyright (c) 2026, Pim van Pelt package main import ( "context" "fmt" "os" "strings" "text/tabwriter" "time" 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: ""} 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} // show frontends showFrontends := &Node{Word: "frontends", Help: "list all frontends", Run: runShowFrontends} // show frontend showFrontendName := &Node{ Word: "", Help: "frontend name", Dynamic: dynFrontends, Run: runShowFrontend, } showFrontend := &Node{ Word: "frontend", Help: "show a single frontend", Children: []*Node{showFrontendName}, } // show backends showBackends := &Node{Word: "backends", Help: "list all backends", Run: runShowBackends} // show backend showBackendName := &Node{ Word: "", Help: "backend name", Dynamic: dynBackends, Run: runShowBackend, } showBackend := &Node{ Word: "backend", Help: "show a single backend", Children: []*Node{showBackendName}, } // show healthchecks showHealthChecks := &Node{Word: "healthchecks", Help: "list all health checks", Run: runShowHealthChecks} // show healthcheck showHealthCheckName := &Node{ Word: "", Help: "health check name", Dynamic: dynHealthChecks, Run: runShowHealthCheck, } showHealthCheck := &Node{ Word: "healthcheck", Help: "show a single health check", Children: []*Node{showHealthCheckName}, } show.Children = []*Node{ showVersion, showFrontends, showFrontend, showBackends, showBackend, showHealthChecks, showHealthCheck, } // set backend 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: "disabled", Help: "disable backend (not implemented)", Run: runNotImplemented} setEnabled := &Node{Word: "enabled", Help: "enable backend (not implemented)", Run: runNotImplemented} setBackendName := &Node{ Word: "", Help: "backend name", Dynamic: dynBackends, Children: []*Node{setPause, setResume, setDisabled, setEnabled}, } setBackend := &Node{ Word: "backend", Help: "modify a backend", Children: []*Node{setBackendName}, } set.Children = []*Node{setBackend} root.Children = []*Node{show, set, quit, exit} return root } // ---- dynamic enumerators --------------------------------------------------- func dynFrontends(ctx context.Context, client grpcapi.MaglevClient) []string { resp, err := client.ListFrontends(ctx, &grpcapi.ListFrontendsRequest{}) if err != nil { return nil } return resp.FrontendNames } func dynBackends(ctx context.Context, client grpcapi.MaglevClient) []string { resp, err := client.ListBackends(ctx, &grpcapi.ListBackendsRequest{}) if err != nil { return nil } return resp.BackendNames } func dynHealthChecks(ctx context.Context, client grpcapi.MaglevClient) []string { resp, err := client.ListHealthChecks(ctx, &grpcapi.ListHealthChecksRequest{}) if err != nil { return nil } return resp.Names } // ---- run functions --------------------------------------------------------- func runShowVersion(_ context.Context, _ grpcapi.MaglevClient, _ []string) error { fmt.Printf("maglevc %s (commit %s, built %s)\n", buildinfo.Version(), buildinfo.Commit(), buildinfo.Date()) return nil } func runQuit(_ context.Context, _ grpcapi.MaglevClient, _ []string) error { return errQuit } func runShowFrontends(ctx context.Context, client grpcapi.MaglevClient, _ []string) error { ctx, cancel := context.WithTimeout(ctx, callTimeout) defer cancel() resp, err := client.ListFrontends(ctx, &grpcapi.ListFrontendsRequest{}) if err != nil { return err } for _, name := range resp.FrontendNames { fmt.Println(name) } return nil } func runShowFrontend(ctx context.Context, client grpcapi.MaglevClient, args []string) error { if len(args) == 0 { return fmt.Errorf("usage: show frontend ") } ctx, cancel := context.WithTimeout(ctx, callTimeout) defer cancel() info, err := client.GetFrontend(ctx, &grpcapi.GetFrontendRequest{Name: args[0]}) if err != nil { 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", label("protocol"), info.Protocol) fmt.Fprintf(w, "%s\t%d\n", label("port"), info.Port) if info.Description != "" { fmt.Fprintf(w, "%s\t%s\n", label("description"), info.Description) } if len(info.Pools) > 0 { fmt.Fprintf(w, "%s\n", label("pools")) } if err := w.Flush(); err != nil { return err } // Pool section uses direct Printf with fixed-width padding so that ANSI // escape codes in labels don't confuse tabwriter's byte-based alignment. // "backends" is always the widest pool label (8 chars); all pool labels // are right-padded to that width, giving a 2+8+2 = 12-char visual indent. const poolLblWidth = len("backends") const poolIndent = " " const poolSep = " " contIndent := strings.Repeat(" ", len(poolIndent)+poolLblWidth+len(poolSep)) 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) for i, pb := range pool.Backends { beInfo, beErr := client.GetBackend(ctx, &grpcapi.GetBackendRequest{Name: pb.Name}) suffix := "" if beErr == nil && !beInfo.Enabled { suffix = " [disabled]" } weightStr := "" if pb.Weight != 100 { weightStr = fmt.Sprintf(" %s %d", label("weight"), pb.Weight) } 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, weightStr, suffix) } else { fmt.Printf("%s%s%s%s\n", contIndent, pb.Name, weightStr, suffix) } } } return nil } func runShowBackends(ctx context.Context, client grpcapi.MaglevClient, _ []string) error { ctx, cancel := context.WithTimeout(ctx, callTimeout) defer cancel() resp, err := client.ListBackends(ctx, &grpcapi.ListBackendsRequest{}) if err != nil { return err } for _, name := range resp.BackendNames { fmt.Println(name) } return nil } func runShowBackend(ctx context.Context, client grpcapi.MaglevClient, args []string) error { if len(args) == 0 { return fmt.Errorf("usage: show backend ") } ctx, cancel := context.WithTimeout(ctx, callTimeout) defer cancel() info, err := client.GetBackend(ctx, &grpcapi.GetBackendRequest{Name: args[0]}) if err != nil { 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) 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) for i, t := range info.Transitions { ts := time.Unix(0, t.AtUnixNs) lbl := "" if i == 0 { lbl = label("transitions") } fmt.Fprintf(w, "%s\t%s → %s\t%s\t%s\n", lbl, t.From, t.To, ts.Format("2006-01-02 15:04:05.000"), formatAgo(time.Since(ts)), ) } return w.Flush() } func runShowHealthChecks(ctx context.Context, client grpcapi.MaglevClient, _ []string) error { ctx, cancel := context.WithTimeout(ctx, callTimeout) defer cancel() resp, err := client.ListHealthChecks(ctx, &grpcapi.ListHealthChecksRequest{}) if err != nil { return err } for _, name := range resp.Names { fmt.Println(name) } return nil } func runShowHealthCheck(ctx context.Context, client grpcapi.MaglevClient, args []string) error { if len(args) == 0 { return fmt.Errorf("usage: show healthcheck ") } ctx, cancel := context.WithTimeout(ctx, callTimeout) defer cancel() info, err := client.GetHealthCheck(ctx, &grpcapi.GetHealthCheckRequest{Name: args[0]}) if err != nil { 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) if info.Port > 0 { fmt.Fprintf(w, "%s\t%d\n", label("port"), info.Port) } fmt.Fprintf(w, "%s\t%s\n", label("interval"), time.Duration(info.IntervalNs)) if info.FastIntervalNs > 0 { fmt.Fprintf(w, "%s\t%s\n", 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", 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) if info.ProbeIpv4Src != "" { fmt.Fprintf(w, "%s\t%s\n", label("probe-ipv4-src"), info.ProbeIpv4Src) } if info.ProbeIpv6Src != "" { fmt.Fprintf(w, "%s\t%s\n", label("probe-ipv6-src"), info.ProbeIpv6Src) } if h := info.Http; h != nil { fmt.Fprintf(w, "%s\t%s\n", 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%d-%d\n", label("http.response-code"), h.ResponseCodeMin, h.ResponseCodeMax) if h.ResponseRegexp != "" { fmt.Fprintf(w, "%s\t%s\n", label("http.response-regexp"), h.ResponseRegexp) } } if t := info.Tcp; t != nil { fmt.Fprintf(w, "%s\t%v\n", label("tcp.ssl"), t.Ssl) if t.ServerName != "" { fmt.Fprintf(w, "%s\t%s\n", label("tcp.server-name"), t.ServerName) } } return w.Flush() } func runPauseBackend(ctx context.Context, client grpcapi.MaglevClient, args []string) error { if len(args) == 0 { return fmt.Errorf("usage: set backend pause") } ctx, cancel := context.WithTimeout(ctx, callTimeout) defer cancel() info, err := client.PauseBackend(ctx, &grpcapi.PauseResumeRequest{Name: args[0]}) if err != nil { return err } fmt.Printf("%s: setting state to '%s'\n", info.Name, info.State) return nil } func runResumeBackend(ctx context.Context, client grpcapi.MaglevClient, args []string) error { if len(args) == 0 { return fmt.Errorf("usage: set backend resume") } ctx, cancel := context.WithTimeout(ctx, callTimeout) defer cancel() info, err := client.ResumeBackend(ctx, &grpcapi.PauseResumeRequest{Name: args[0]}) if err != nil { return err } fmt.Printf("%s: setting state to '%s'\n", info.Name, info.State) return nil } func runNotImplemented(_ context.Context, _ grpcapi.MaglevClient, _ []string) error { fmt.Println("not implemented yet") return nil } // formatDuration formats a duration as Xd Xh Xm Xs without milliseconds. func formatDuration(d time.Duration) string { if d < 0 { d = 0 } d = d.Truncate(time.Second) days := int(d.Hours()) / 24 d -= time.Duration(days) * 24 * time.Hour hours := int(d.Hours()) d -= time.Duration(hours) * time.Hour minutes := int(d.Minutes()) d -= time.Duration(minutes) * time.Minute seconds := int(d.Seconds()) var b strings.Builder if days > 0 { fmt.Fprintf(&b, "%dd", days) } if hours > 0 { fmt.Fprintf(&b, "%dh", hours) } if minutes > 0 { fmt.Fprintf(&b, "%dm", minutes) } if seconds > 0 || b.Len() == 0 { fmt.Fprintf(&b, "%ds", seconds) } return b.String() } func formatAgo(d time.Duration) string { return formatDuration(d) + " ago" }