// Copyright (c) 2026, Pim van Pelt package main import ( "context" "fmt" "os" "strings" "text/tabwriter" "time" "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 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{ 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 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, "name\t%s\n", info.Name) fmt.Fprintf(w, "address\t%s\n", info.Address) fmt.Fprintf(w, "protocol\t%s\n", info.Protocol) fmt.Fprintf(w, "port\t%d\n", info.Port) for i, b := range info.BackendNames { if i == 0 { fmt.Fprintf(w, "backends\t%s\n", b) } else { fmt.Fprintf(w, "\t%s\n", b) } } if info.Description != "" { fmt.Fprintf(w, "description\t%s\n", info.Description) } return w.Flush() } 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, "name\t%s\n", info.Name) fmt.Fprintf(w, "address\t%s\n", 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, "state\t%s%s\n", info.State, stateDur) fmt.Fprintf(w, "enabled\t%v\n", info.Enabled) fmt.Fprintf(w, "weight\t%d\n", info.Weight) fmt.Fprintf(w, "healthcheck\t%s\n", info.Healthcheck) for i, t := range info.Transitions { ts := time.Unix(0, t.AtUnixNs) label := "" if i == 0 { label = "transitions" } fmt.Fprintf(w, "%s\t%s → %s\t%s\t%s\n", label, 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, "name\t%s\n", info.Name) fmt.Fprintf(w, "type\t%s\n", info.Type) if info.Port > 0 { fmt.Fprintf(w, "port\t%d\n", info.Port) } fmt.Fprintf(w, "interval\t%s\n", time.Duration(info.IntervalNs)) if info.FastIntervalNs > 0 { fmt.Fprintf(w, "fast-interval\t%s\n", time.Duration(info.FastIntervalNs)) } if info.DownIntervalNs > 0 { fmt.Fprintf(w, "down-interval\t%s\n", time.Duration(info.DownIntervalNs)) } fmt.Fprintf(w, "timeout\t%s\n", time.Duration(info.TimeoutNs)) fmt.Fprintf(w, "rise\t%d\n", info.Rise) fmt.Fprintf(w, "fall\t%d\n", info.Fall) if info.ProbeIpv4Src != "" { fmt.Fprintf(w, "probe-ipv4-src\t%s\n", info.ProbeIpv4Src) } if info.ProbeIpv6Src != "" { fmt.Fprintf(w, "probe-ipv6-src\t%s\n", info.ProbeIpv6Src) } if h := info.Http; h != nil { fmt.Fprintf(w, "http.path\t%s\n", h.Path) if h.Host != "" { fmt.Fprintf(w, "http.host\t%s\n", h.Host) } fmt.Fprintf(w, "http.response-code\t%d-%d\n", h.ResponseCodeMin, h.ResponseCodeMax) if h.ResponseRegexp != "" { fmt.Fprintf(w, "http.response-regexp\t%s\n", h.ResponseRegexp) } } if t := info.Tcp; t != nil { fmt.Fprintf(w, "tcp.ssl\t%v\n", t.Ssl) if t.ServerName != "" { fmt.Fprintf(w, "tcp.server-name\t%s\n", 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" }