Add WatchEvents, enable/disable/weight RPCs, and config check

gRPC / proto
- Rename WatchBackendEvents → WatchEvents; return a stream of Event
  oneof (LogEvent, BackendEvent, FrontendEvent) with optional filter
  flags (log, log_level, backend, frontend)
- Add EnableBackend, DisableBackend, SetFrontendPoolBackendWeight RPCs
- Rename PauseResumeRequest → BackendRequest
- Add CheckConfig RPC returning ok/parse_error/semantic_error

maglevd
- Route slog through a LogBroadcaster (slog.Handler) so WatchEvents
  subscribers can receive structured log records independently of the
  daemon's own --log-level
- Add --reflection flag (default true) to toggle gRPC server reflection
- Add --check flag: validates config file and exits 0/1/2
- SIGHUP: use config.Check before applying reload; log parse vs semantic
  error separately; refuse reload on any error
- Rename default config path /etc/maglev → /etc/vpp-maglev

maglevc
- Add 'watch events [num <n>] [log [level <level>]] [backend] [frontend]'
  command; prints compact protojson, stops on any keypress or Ctrl-C;
  uses cbreak mode (not raw) so output post-processing is preserved
- Add 'set backend <name> enable|disable'
- Add 'set frontend <name> pool <pool> backend <name> weight <0-100>'
- Add 'config check' command

Debian packaging
- Rename service unit to vpp-maglevd.service
- Rename conffiles to /etc/default/vpp-maglev and /etc/vpp-maglev/
- Create maglevd system user/group in postinst; add to vpp group if present
- Add postrm; add adduser to Depends
This commit is contained in:
2026-04-11 16:42:11 +02:00
parent d612086a5f
commit 58391f5463
26 changed files with 1969 additions and 400 deletions

View File

@@ -6,6 +6,7 @@ import (
"context"
"fmt"
"os"
"strconv"
"strings"
"text/tabwriter"
"time"
@@ -83,8 +84,8 @@ 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: "disabled", Help: "disable backend (not implemented)", Run: runNotImplemented}
setEnabled := &Node{Word: "enabled", Help: "enable backend (not implemented)", Run: runNotImplemented}
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",
@@ -96,9 +97,75 @@ func buildTree() *Node {
Help: "modify a backend",
Children: []*Node{setBackendName},
}
set.Children = []*Node{setBackend}
// set frontend <name> pool <pool> backend <name> weight <0-100>
setWeightValue := &Node{
Word: "<weight>",
Help: "weight 0-100",
Run: runSetFrontendPoolBackendWeight,
}
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},
}
setFrontendPoolBackend := &Node{Word: "backend", Help: "select a backend", Children: []*Node{setFrontendPoolBackendName}}
setFrontendPoolName := &Node{
Word: "<pool>",
Help: "pool name",
Children: []*Node{setFrontendPoolBackend},
}
setFrontendPool := &Node{Word: "pool", Help: "select a pool", Children: []*Node{setFrontendPoolName}}
setFrontendName := &Node{
Word: "<name>",
Help: "frontend name",
Dynamic: dynFrontends,
Children: []*Node{setFrontendPool},
}
setFrontend := &Node{
Word: "frontend",
Help: "modify a frontend",
Children: []*Node{setFrontendName},
}
root.Children = []*Node{show, set, quit, exit}
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.
var watchEventsOptSlot *Node
watchEventsOptSlot = &Node{
Word: "<opt>",
Help: "watch option",
Dynamic: dynWatchEventOpts,
Run: runWatchEvents,
}
watchEventsOptSlot.Children = []*Node{watchEventsOptSlot}
watchEvents := &Node{
Word: "events",
Help: "stream events (press any key or Ctrl-C to stop)",
Run: runWatchEvents,
Children: []*Node{watchEventsOptSlot},
}
watch := &Node{
Word: "watch",
Help: "watch live event streams",
Children: []*Node{watchEvents},
}
// config check
configCheck := &Node{Word: "check", Help: "check configuration file", Run: runConfigCheck}
configNode := &Node{
Word: "config",
Help: "configuration commands",
Children: []*Node{configCheck},
}
root.Children = []*Node{show, set, watch, configNode, quit, exit}
return root
}
@@ -332,7 +399,7 @@ func runPauseBackend(ctx context.Context, client grpcapi.MaglevClient, args []st
}
ctx, cancel := context.WithTimeout(ctx, callTimeout)
defer cancel()
info, err := client.PauseBackend(ctx, &grpcapi.PauseResumeRequest{Name: args[0]})
info, err := client.PauseBackend(ctx, &grpcapi.BackendRequest{Name: args[0]})
if err != nil {
return err
}
@@ -346,7 +413,7 @@ func runResumeBackend(ctx context.Context, client grpcapi.MaglevClient, args []s
}
ctx, cancel := context.WithTimeout(ctx, callTimeout)
defer cancel()
info, err := client.ResumeBackend(ctx, &grpcapi.PauseResumeRequest{Name: args[0]})
info, err := client.ResumeBackend(ctx, &grpcapi.BackendRequest{Name: args[0]})
if err != nil {
return err
}
@@ -354,11 +421,86 @@ func runResumeBackend(ctx context.Context, client grpcapi.MaglevClient, args []s
return nil
}
func runNotImplemented(_ context.Context, _ grpcapi.MaglevClient, _ []string) error {
fmt.Println("not implemented yet")
func runSetFrontendPoolBackendWeight(ctx context.Context, client grpcapi.MaglevClient, args []string) error {
if len(args) != 4 {
return fmt.Errorf("usage: set frontend <name> pool <pool> backend <name> weight <0-100>")
}
frontendName, poolName, backendName, weightStr := args[0], args[1], args[2], args[3]
weight, err := strconv.Atoi(weightStr)
if err != nil || weight < 0 || weight > 100 {
return fmt.Errorf("weight: expected integer 0-100, got %q", weightStr)
}
ctx, cancel := context.WithTimeout(ctx, callTimeout)
defer cancel()
info, err := client.SetFrontendPoolBackendWeight(ctx, &grpcapi.SetWeightRequest{
Frontend: frontendName,
Pool: poolName,
Backend: backendName,
Weight: int32(weight),
})
if err != nil {
return err
}
// Print the updated pool so the user can confirm the new weight.
for _, pool := range info.Pools {
if pool.Name != poolName {
continue
}
for _, pb := range pool.Backends {
if pb.Name == backendName {
fmt.Printf("%s pool %s backend %s: weight set to %d\n", info.Name, pool.Name, pb.Name, pb.Weight)
return nil
}
}
}
return nil
}
func runEnableBackend(ctx context.Context, client grpcapi.MaglevClient, args []string) error {
if len(args) == 0 {
return fmt.Errorf("usage: set backend <name> enable")
}
ctx, cancel := context.WithTimeout(ctx, callTimeout)
defer cancel()
info, err := client.EnableBackend(ctx, &grpcapi.BackendRequest{Name: args[0]})
if err != nil {
return err
}
fmt.Printf("%s: enabled, state is '%s'\n", info.Name, info.State)
return nil
}
func runDisableBackend(ctx context.Context, client grpcapi.MaglevClient, args []string) error {
if len(args) == 0 {
return fmt.Errorf("usage: set backend <name> disable")
}
ctx, cancel := context.WithTimeout(ctx, callTimeout)
defer cancel()
info, err := client.DisableBackend(ctx, &grpcapi.BackendRequest{Name: args[0]})
if err != nil {
return err
}
fmt.Printf("%s: disabled, state is '%s'\n", info.Name, info.State)
return nil
}
func runConfigCheck(ctx context.Context, client grpcapi.MaglevClient, _ []string) error {
ctx, cancel := context.WithTimeout(ctx, callTimeout)
defer cancel()
resp, err := client.CheckConfig(ctx, &grpcapi.CheckConfigRequest{})
if err != nil {
return err
}
if resp.Ok {
fmt.Println("config ok")
return nil
}
if resp.ParseError != "" {
return fmt.Errorf("parse error: %s", resp.ParseError)
}
return fmt.Errorf("semantic error: %s", resp.SemanticError)
}
// formatDuration formats a duration as Xd Xh Xm Xs without milliseconds.
func formatDuration(d time.Duration) string {
if d < 0 {