diff --git a/Makefile b/Makefile index d00af87..0772a08 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,9 @@ LDFLAGS := -X '$(MODULE)/cmd.version=$(VERSION)' \ TEST ?= tests/ -.PHONY: all build build-amd64 build-arm64 test proto lint fixstyle pkg-deb robot-test clean +VPP_API_DIR ?= $(HOME)/src/vpp/build-root/install-vpp_debug-native/vpp/share/vpp/api + +.PHONY: all build build-amd64 build-arm64 test proto vpp-binapi lint fixstyle pkg-deb robot-test clean all: build @@ -48,6 +50,27 @@ $(GEN_FILES): $(PROTO_FILE) --go-grpc_out=. --go-grpc_opt=module=$(MODULE) \ $(PROTO_FILE) +# vpp-binapi regenerates the Go bindings for VPP API files used by maglevd +# from a local VPP build. The LB plugin ships with upstream VPP; any newer +# messages (e.g. lb_conf_get, lb_as_v2_dump) require a VPP build that has +# them. Override VPP_API_DIR on the command line to point at another tree: +# make vpp-binapi VPP_API_DIR=/path/to/share/vpp/api +vpp-binapi: + @command -v binapi-generator >/dev/null 2>&1 || { \ + echo "installing binapi-generator..."; \ + go install go.fd.io/govpp/cmd/binapi-generator@v0.12.0; \ + } + rm -rf internal/vpp/binapi + mkdir -p internal/vpp/binapi + binapi-generator \ + --input=$(VPP_API_DIR) \ + --output-dir=internal/vpp/binapi \ + --import-prefix=$(MODULE)/internal/vpp/binapi \ + --no-source-path-info \ + --no-version-info \ + lb lb_types + rm -f internal/vpp/binapi/lb/lb_rpc.ba.go + fixstyle: gofmt -w . diff --git a/cmd/maglevc/commands.go b/cmd/maglevc/commands.go index d892803..e39d45c 100644 --- a/cmd/maglevc/commands.go +++ b/cmd/maglevc/commands.go @@ -71,12 +71,13 @@ func buildTree() *Node { Children: []*Node{showHealthCheckName}, } - // show vpp info + // show vpp info / lbstate showVPPInfo := &Node{Word: "info", Help: "Show VPP version, uptime, and connection status", Run: runShowVPPInfo} + showVPPLBState := &Node{Word: "lbstate", Help: "Show VPP load-balancer state (VIPs and application servers)", Run: runShowVPPLBState} showVPP := &Node{ Word: "vpp", Help: "VPP dataplane information", - Children: []*Node{showVPPInfo}, + Children: []*Node{showVPPInfo, showVPPLBState}, } show.Children = []*Node{ @@ -174,7 +175,34 @@ func buildTree() *Node { Children: []*Node{configCheck, configReload}, } - root.Children = []*Node{show, set, watch, configNode, quit, exit} + // sync vpp lbstate [] + // + // Without a name: run SyncLBStateAll (may remove stale VIPs). + // With a name: run SyncLBStateVIP(name) for just that frontend (no removals). + syncVPPLBStateName := &Node{ + Word: "", + Help: "Sync a single frontend's VIP to VPP", + Dynamic: dynFrontends, + Run: runSyncVPPLBState, + } + syncVPPLBState := &Node{ + Word: "lbstate", + Help: "Sync the VPP load-balancer dataplane from the running config", + Run: runSyncVPPLBState, + Children: []*Node{syncVPPLBStateName}, + } + syncVPP := &Node{ + Word: "vpp", + Help: "VPP dataplane sync commands", + Children: []*Node{syncVPPLBState}, + } + syncNode := &Node{ + Word: "sync", + Help: "Reconcile dataplane state from the running config", + Children: []*Node{syncVPP}, + } + + root.Children = []*Node{show, set, watch, configNode, syncNode, quit, exit} return root } @@ -235,6 +263,89 @@ func runShowVPPInfo(ctx context.Context, client grpcapi.MaglevClient, _ []string return w.Flush() } +func runShowVPPLBState(ctx context.Context, client grpcapi.MaglevClient, _ []string) error { + ctx, cancel := context.WithTimeout(ctx, callTimeout) + defer cancel() + state, err := client.GetVPPLBState(ctx, &grpcapi.GetVPPLBStateRequest{}) + if err != nil { + return err + } + + // ---- global config ---- + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintf(w, "%s\n", label("global")) + if state.Conf.Ip4SrcAddress != "" { + fmt.Fprintf(w, " %s\t%s\n", 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%d\n", label("sticky-buckets-per-core"), state.Conf.StickyBucketsPerCore) + fmt.Fprintf(w, " %s\t%ds\n", label("flow-timeout"), state.Conf.FlowTimeout) + if err := w.Flush(); err != nil { + return err + } + + if len(state.Vips) == 0 { + fmt.Println(label("vips") + " (none)") + return nil + } + + // ---- per-VIP details ---- + 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"), 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%d\n", label("flow-table-length"), v.FlowTableLength) + fmt.Fprintf(w, " %s\t%d\n", 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) + } + } + return nil +} + +// protoString renders an IP protocol number as a name (tcp, udp, any, or numeric). +func protoString(p uint32) string { + switch p { + case 6: + return "tcp" + case 17: + return "udp" + case 255: + return "any" + } + return fmt.Sprintf("%d", p) +} + +func runSyncVPPLBState(ctx context.Context, client grpcapi.MaglevClient, args []string) error { + ctx, cancel := context.WithTimeout(ctx, callTimeout) + defer cancel() + req := &grpcapi.SyncVPPLBStateRequest{} + if len(args) > 0 && args[0] != "" { + name := args[0] + req.FrontendName = &name + } + if _, err := client.SyncVPPLBState(ctx, req); err != nil { + return err + } + if req.FrontendName != nil { + fmt.Printf("synced frontend %q to VPP\n", *req.FrontendName) + } else { + fmt.Println("synced full LB state to VPP") + } + return nil +} + func runShowVersion(_ context.Context, _ grpcapi.MaglevClient, _ []string) error { fmt.Printf("maglevc %s (commit %s, built %s)\n", buildinfo.Version(), buildinfo.Commit(), buildinfo.Date()) diff --git a/cmd/maglevc/tree_test.go b/cmd/maglevc/tree_test.go index 86431ea..2cf5a08 100644 --- a/cmd/maglevc/tree_test.go +++ b/cmd/maglevc/tree_test.go @@ -29,7 +29,10 @@ func TestExpandPathsRoot(t *testing.T) { "watch events ", "config check", "show vpp info", + "show vpp lbstate", "config reload", + "sync vpp lbstate", + "sync vpp lbstate ", "quit", "exit", } @@ -60,9 +63,9 @@ func TestExpandPathsShow(t *testing.T) { } } // version, frontends, frontends , backends, backends , - // healthchecks, healthchecks , vpp info = 8 lines - if len(lines) != 8 { - t.Errorf("expected exactly 8 show subcommands, got %d", len(lines)) + // healthchecks, healthchecks , vpp info, vpp lb = 9 lines + if len(lines) != 9 { + t.Errorf("expected exactly 9 show subcommands, got %d", len(lines)) } } diff --git a/cmd/maglevd/main.go b/cmd/maglevd/main.go index 474c3f8..0badf9a 100644 --- a/cmd/maglevd/main.go +++ b/cmd/maglevd/main.go @@ -99,6 +99,7 @@ func run() error { var vppClient *vpp.Client if *vppAPIAddr != "" { vppClient = vpp.New(*vppAPIAddr, *vppStatsAddr) + vppClient.SetConfigSource(chkr) go vppClient.Run(ctx) } diff --git a/debian/maglev.yaml b/debian/maglev.yaml index e9e15ac..fac7fa9 100644 --- a/debian/maglev.yaml +++ b/debian/maglev.yaml @@ -3,6 +3,13 @@ maglev: transition-history: 5 # netns: dataplane # run probes inside a named network namespace + vpp: + lb: + ipv4-src-address: 192.0.2.254 # source for GRE4 encap to application servers + ipv6-src-address: 2001:db8::254 # source for GRE6 encap to application servers + # sticky-buckets-per-core: 65536 # power of 2, default 65536 + # flow-timeout: 40s # 1s-120s, default 40s + healthchecks: http-check: type: http diff --git a/docs/config-guide.md b/docs/config-guide.md index 3caf84a..559e3f3 100644 --- a/docs/config-guide.md +++ b/docs/config-guide.md @@ -11,7 +11,7 @@ in two stages: ensuring that every backend referenced by a frontend exists, that address families are consistent within a frontend, and that IP source addresses are the correct family. -If you want to get started quickly, take a look at the [[example config](../debian/mavleg.yaml)]. +If you want to get started quickly, take a look at the [example config](../debian/maglev.yaml). ## Basic structure @@ -22,6 +22,10 @@ maglev: healthchecker: [ Global health checker settings ] + vpp: + lb: + [ VPP load-balancer integration settings ] + healthchecks: my-check: [ Health check definition ] @@ -35,9 +39,11 @@ maglev: [ Frontend (VIP) definition ] ``` -All four sections live under the top-level `maglev:` key. The `healthchecks`, `backends`, +All five sections live under the top-level `maglev:` key. The `healthchecks`, `backends`, and `frontends` sections are maps keyed by an arbitrary name of your choosing. Names must be -unique within their section and are case-sensitive. +unique within their section and are case-sensitive. The `vpp` section is required when +`maglevd` has a working VPP connection — its `lb.ipv4-src-address` and `lb.ipv6-src-address` +fields are mandatory and `maglevd` will refuse to start without them. --- @@ -61,6 +67,50 @@ maglev: --- +## vpp + +Settings controlling the integration with a locally running VPP instance. The +`vpp` section is a map with a single sub-section, `lb`. Both `lb.ipv4-src-address` +and `lb.ipv6-src-address` are **required** — `maglevd --check` exits with a +semantic error and the daemon refuses to start when either is missing, because +VPP's GRE encap needs a source address and every VIP `maglevd` programs uses GRE. + +* ***lb.ipv4-src-address***: Required. The IPv4 source address VPP uses when + encapsulating IPv4 traffic into GRE4 tunnels to application servers. Must + be a valid IPv4 address. No default. +* ***lb.ipv6-src-address***: Required. The IPv6 source address VPP uses when + encapsulating IPv6 traffic into GRE6 tunnels. Must be a valid IPv6 address. + No default. +* ***lb.sync-interval***: A positive Go duration (e.g. `30s`, `1m`) controlling + how often `maglevd` reconciles the VPP load-balancer dataplane against its + running configuration. On startup, an immediate full sync runs; subsequent + syncs fire at this interval as long as the VPP connection is up. Defaults + to `30s`. The purpose is to catch drift — for example, a VIP added to VPP + by hand — and bring VPP back in line with the maglev config. +* ***lb.sticky-buckets-per-core***: The number of buckets per worker thread in + the established-flow table. Must be a power of 2. Defaults to `65536` (64k). +* ***lb.flow-timeout***: Idle time after which an established flow is removed + from the table. Must be a whole number of seconds between `1s` and `120s` + inclusive. Defaults to `40s`. + +These four values are pushed to VPP via `lb_conf` when `maglevd` connects to +VPP and again after every config reload (whenever they change). A log line +`vpp-lb-conf-set` records the effective values. + +Example: +```yaml +maglev: + vpp: + lb: + sync-interval: 60s + ipv4-src-address: 10.0.0.1 + ipv6-src-address: 2001:db8::1 + sticky-buckets-per-core: 65536 + flow-timeout: 40s +``` + +--- + ## healthchecks A named map of health check definitions. Each health check describes *how* to probe a backend. @@ -278,6 +328,6 @@ frontends: --- For a detailed description of the health state machine, probe intervals, and all transition events, -see [[healthchecks.md](healthchecks.md)]. For a user guide on how to use the maglev daemon and client, -see the [[user-guide.md](user-guide.md)]. +see [healthchecks.md](healthchecks.md). For a user guide on how to use the maglev daemon and client, +see the [user-guide.md](user-guide.md). diff --git a/docs/user-guide.md b/docs/user-guide.md index 9335569..3f4c93b 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -88,10 +88,26 @@ show healthchecks [] Without name: list all health-check names. show vpp info Show VPP version, build date, PID, uptime, and when maglevd connected. Returns an error if VPP is not connected. +show vpp lbstate Show the VPP load-balancer plugin state: global + configuration, configured VIPs, and their attached + application servers (address, weight, bucket count). + Returns an error if VPP is not connected. -set backend pause Suspend health checking for a backend, freezing its state. -set backend resume Resume health checking; backend re-enters unknown state - and is probed immediately. +sync vpp lbstate [] Reconcile the VPP load-balancer dataplane from the + running config. Without a name: runs a full sync — + creates missing VIPs, removes stale VIPs, and adjusts + application-server membership and weights across all + frontends. With a name: only the named frontend's VIP + is reconciled, and no VIPs are removed. A full sync + also runs automatically every + maglev.vpp.lb.sync-interval (default 30s) to catch + drift, and once on startup. + +set backend pause Stop health checking for a backend. Cancels the probe + goroutine so no further traffic is sent, and freezes + the state at whatever it was when paused. +set backend resume Resume health checking. A fresh probe goroutine is + started and the backend re-enters unknown state. set backend disable Stop probing entirely and remove the backend from rotation. The backend remains visible (state: disabled) and can be re-enabled without reloading configuration. diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 9e4298b..e61e39e 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -152,6 +152,15 @@ func (c *Checker) Subscribe() (<-chan Event, func()) { } } +// Config returns the live config pointer held by the checker. Callers must +// treat the returned value as read-only. The pointer is swapped on Reload, +// so callers that cache it across reloads may see stale data. +func (c *Checker) Config() *config.Config { + c.mu.RLock() + defer c.mu.RUnlock() + return c.cfg +} + // ListFrontends returns the names of all configured frontends. func (c *Checker) ListFrontends() []string { c.mu.RLock() diff --git a/internal/config/config.go b/internal/config/config.go index dfcf4dd..4fd0a4f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -17,6 +17,7 @@ import ( // Config is the top-level parsed and validated configuration. type Config struct { HealthChecker HealthCheckerConfig + VPP VPPConfig HealthChecks map[string]HealthCheck Backends map[string]Backend Frontends map[string]Frontend @@ -28,6 +29,37 @@ type HealthCheckerConfig struct { Netns string // network namespace for probes; "" = current netns } +// VPPConfig holds VPP-related configuration. +type VPPConfig struct { + LB VPPLBConfig +} + +// VPPLBConfig holds load-balancer integration settings. +type VPPLBConfig struct { + // SyncInterval is how often the full dataplane reconciliation runs, + // catching drift (e.g. manual changes to VPP). Defaults to 30s. + SyncInterval time.Duration + + // IPv4SrcAddress is the source address VPP uses when encapsulating + // IPv4 traffic into GRE4 tunnels to application servers. Required + // when any frontend uses an IPv4 VIP; VPP GRE encap will fail if unset. + IPv4SrcAddress net.IP + + // IPv6SrcAddress is the source address VPP uses when encapsulating + // IPv6 traffic into GRE6 tunnels. Required when any frontend uses an + // IPv6 VIP; VPP GRE encap will fail if unset. + IPv6SrcAddress net.IP + + // StickyBucketsPerCore is the number of buckets (per worker thread) in + // the established-flow table. Must be a power of 2. Defaults to 65536. + StickyBucketsPerCore uint32 + + // FlowTimeout is the idle time after which an established flow is + // removed from the table. Must be between 1 and 120 seconds inclusive. + // Defaults to 40s. + FlowTimeout time.Duration +} + // HealthCheck describes how to probe a backend. type HealthCheck struct { Type string @@ -97,6 +129,7 @@ type rawConfig struct { type rawMaglev struct { HealthChecker rawHealthCheckerCfg `yaml:"healthchecker"` + VPP rawVPPCfg `yaml:"vpp"` HealthChecks map[string]rawHealthCheck `yaml:"healthchecks"` Backends map[string]rawBackend `yaml:"backends"` Frontends map[string]rawFrontend `yaml:"frontends"` @@ -107,6 +140,18 @@ type rawHealthCheckerCfg struct { Netns string `yaml:"netns"` } +type rawVPPCfg struct { + LB rawVPPLBCfg `yaml:"lb"` +} + +type rawVPPLBCfg struct { + SyncInterval string `yaml:"sync-interval"` // Go duration; default 30s + IPv4SrcAddress string `yaml:"ipv4-src-address"` + IPv6SrcAddress string `yaml:"ipv6-src-address"` + StickyBucketsPerCore *uint32 `yaml:"sticky-buckets-per-core"` // default 65536 + FlowTimeout string `yaml:"flow-timeout"` // Go duration; default 40s, [1-120]s +} + type rawHealthCheck struct { Type string `yaml:"type"` Port uint16 `yaml:"port"` @@ -255,9 +300,93 @@ func convert(r *rawMaglev) (*Config, error) { cfg.Frontends[name] = fe } + // ---- vpp ------------------------------------------------------------------ + // Runs last so structural errors in healthchecks/backends/frontends are + // reported first; operators fix those, then we tell them about the VPP + // src-address requirements. + if err := convertVPP(&r.VPP, &cfg.VPP); err != nil { + return nil, err + } + return cfg, nil } +// convertVPP parses and validates the maglev.vpp section. Missing src-address +// fields are tolerated but logged at ERROR level so operators notice that VPP +// GRE encap will fail without them. +func convertVPP(r *rawVPPCfg, cfg *VPPConfig) error { + // sync-interval: default 30s, must be > 0. + if s := r.LB.SyncInterval; s != "" { + d, err := time.ParseDuration(s) + if err != nil { + return fmt.Errorf("vpp.lb.sync-interval: %w", err) + } + if d <= 0 { + return fmt.Errorf("vpp.lb.sync-interval must be > 0") + } + cfg.LB.SyncInterval = d + } else { + cfg.LB.SyncInterval = 30 * time.Second + } + + // ipv4-src-address: optional here, but warned below if missing. + if s := r.LB.IPv4SrcAddress; s != "" { + ip := net.ParseIP(s) + if ip == nil || ip.To4() == nil { + return fmt.Errorf("vpp.lb.ipv4-src-address: %q is not a valid IPv4 address", s) + } + cfg.LB.IPv4SrcAddress = ip.To4() + } + + // ipv6-src-address: optional here, but warned below if missing. + if s := r.LB.IPv6SrcAddress; s != "" { + ip := net.ParseIP(s) + if ip == nil || ip.To4() != nil { + return fmt.Errorf("vpp.lb.ipv6-src-address: %q is not a valid IPv6 address", s) + } + cfg.LB.IPv6SrcAddress = ip.To16() + } + + // sticky-buckets-per-core: default 65536, must be power of 2. + if p := r.LB.StickyBucketsPerCore; p != nil { + n := *p + if n == 0 || n&(n-1) != 0 { + return fmt.Errorf("vpp.lb.sticky-buckets-per-core: %d must be a power of 2", n) + } + cfg.LB.StickyBucketsPerCore = n + } else { + cfg.LB.StickyBucketsPerCore = 65536 + } + + // flow-timeout: default 40s, must be 1-120s inclusive and a whole number of seconds. + if s := r.LB.FlowTimeout; s != "" { + d, err := time.ParseDuration(s) + if err != nil { + return fmt.Errorf("vpp.lb.flow-timeout: %w", err) + } + if d%time.Second != 0 { + return fmt.Errorf("vpp.lb.flow-timeout: %s must be a whole number of seconds", d) + } + if d < time.Second || d > 120*time.Second { + return fmt.Errorf("vpp.lb.flow-timeout: %s out of range [1s, 120s]", d) + } + cfg.LB.FlowTimeout = d + } else { + cfg.LB.FlowTimeout = 40 * time.Second + } + + // A missing src address is a hard error: VPP's GRE encap needs a source, + // and every VIP we program uses GRE. Fail the config check so the + // operator cannot start maglevd with a broken setup. + if cfg.LB.IPv4SrcAddress == nil { + return fmt.Errorf("vpp.lb.ipv4-src-address must be set; VPP GRE4 encap will fail for IPv4 VIPs") + } + if cfg.LB.IPv6SrcAddress == nil { + return fmt.Errorf("vpp.lb.ipv6-src-address must be set; VPP GRE6 encap will fail for IPv6 VIPs") + } + return nil +} + func convertHealthCheck(r *rawHealthCheck) (HealthCheck, error) { h := HealthCheck{Type: r.Type, Port: r.Port} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 35d70e3..bdf878f 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -12,6 +12,10 @@ maglev: healthchecker: transition-history: 5 netns: dataplane + vpp: + lb: + ipv4-src-address: 10.0.0.1 + ipv6-src-address: 2001:db8::1 healthchecks: http-check: type: http @@ -150,6 +154,10 @@ func TestValidConfig(t *testing.T) { func TestDefaults(t *testing.T) { raw := ` maglev: + vpp: + lb: + ipv4-src-address: 10.0.0.1 + ipv6-src-address: 2001:db8::1 healthchecks: icmp: type: icmp @@ -196,6 +204,10 @@ func TestBackendNoHealthcheck(t *testing.T) { // A backend with no healthcheck reference is valid; probe is skipped. raw := ` maglev: + vpp: + lb: + ipv4-src-address: 10.0.0.1 + ipv6-src-address: 2001:db8::1 healthchecks: {} backends: be: @@ -220,6 +232,10 @@ maglev: func TestOptionalIntervals(t *testing.T) { raw := ` maglev: + vpp: + lb: + ipv4-src-address: 10.0.0.1 + ipv6-src-address: 2001:db8::1 healthchecks: icmp: type: icmp @@ -259,6 +275,10 @@ func TestValidationErrors(t *testing.T) { base := func(hcExtra, beExtra, feExtra string) string { return ` maglev: + vpp: + lb: + ipv4-src-address: 10.0.0.1 + ipv6-src-address: 2001:db8::1 healthchecks: c: type: icmp @@ -294,6 +314,10 @@ maglev: name: "mixed backend address families in pool", yaml: ` maglev: + vpp: + lb: + ipv4-src-address: 10.0.0.1 + ipv6-src-address: 2001:db8::1 healthchecks: c: type: icmp diff --git a/internal/grpcapi/maglev.pb.go b/internal/grpcapi/maglev.pb.go index c6da847..5c432ea 100644 --- a/internal/grpcapi/maglev.pb.go +++ b/internal/grpcapi/maglev.pb.go @@ -625,6 +625,412 @@ func (x *VPPInfo) GetConnecttimeNs() int64 { return 0 } +type GetVPPLBStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetVPPLBStateRequest) Reset() { + *x = GetVPPLBStateRequest{} + mi := &file_proto_maglev_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetVPPLBStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetVPPLBStateRequest) ProtoMessage() {} + +func (x *GetVPPLBStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_maglev_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetVPPLBStateRequest.ProtoReflect.Descriptor instead. +func (*GetVPPLBStateRequest) Descriptor() ([]byte, []int) { + return file_proto_maglev_proto_rawDescGZIP(), []int{13} +} + +// VPPLBConf mirrors VPP's lb_conf_get_reply: global LB plugin settings. +type VPPLBConf struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ip4SrcAddress string `protobuf:"bytes,1,opt,name=ip4_src_address,json=ip4SrcAddress,proto3" json:"ip4_src_address,omitempty"` + Ip6SrcAddress string `protobuf:"bytes,2,opt,name=ip6_src_address,json=ip6SrcAddress,proto3" json:"ip6_src_address,omitempty"` + StickyBucketsPerCore uint32 `protobuf:"varint,3,opt,name=sticky_buckets_per_core,json=stickyBucketsPerCore,proto3" json:"sticky_buckets_per_core,omitempty"` + FlowTimeout uint32 `protobuf:"varint,4,opt,name=flow_timeout,json=flowTimeout,proto3" json:"flow_timeout,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VPPLBConf) Reset() { + *x = VPPLBConf{} + mi := &file_proto_maglev_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VPPLBConf) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VPPLBConf) ProtoMessage() {} + +func (x *VPPLBConf) ProtoReflect() protoreflect.Message { + mi := &file_proto_maglev_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VPPLBConf.ProtoReflect.Descriptor instead. +func (*VPPLBConf) Descriptor() ([]byte, []int) { + return file_proto_maglev_proto_rawDescGZIP(), []int{14} +} + +func (x *VPPLBConf) GetIp4SrcAddress() string { + if x != nil { + return x.Ip4SrcAddress + } + return "" +} + +func (x *VPPLBConf) GetIp6SrcAddress() string { + if x != nil { + return x.Ip6SrcAddress + } + return "" +} + +func (x *VPPLBConf) GetStickyBucketsPerCore() uint32 { + if x != nil { + return x.StickyBucketsPerCore + } + return 0 +} + +func (x *VPPLBConf) GetFlowTimeout() uint32 { + if x != nil { + return x.FlowTimeout + } + return 0 +} + +// VPPLBAS is one application server attached to a VIP. +type VPPLBAS struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Weight uint32 `protobuf:"varint,2,opt,name=weight,proto3" json:"weight,omitempty"` // 0-100 + Flags uint32 `protobuf:"varint,3,opt,name=flags,proto3" json:"flags,omitempty"` // VPP AS flags (bit 0 = used, bit 1 = flushed) + NumBuckets uint32 `protobuf:"varint,4,opt,name=num_buckets,json=numBuckets,proto3" json:"num_buckets,omitempty"` + InUseSinceNs int64 `protobuf:"varint,5,opt,name=in_use_since_ns,json=inUseSinceNs,proto3" json:"in_use_since_ns,omitempty"` // unix timestamp (ns), 0 if never used + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VPPLBAS) Reset() { + *x = VPPLBAS{} + mi := &file_proto_maglev_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VPPLBAS) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VPPLBAS) ProtoMessage() {} + +func (x *VPPLBAS) ProtoReflect() protoreflect.Message { + mi := &file_proto_maglev_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VPPLBAS.ProtoReflect.Descriptor instead. +func (*VPPLBAS) Descriptor() ([]byte, []int) { + return file_proto_maglev_proto_rawDescGZIP(), []int{15} +} + +func (x *VPPLBAS) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *VPPLBAS) GetWeight() uint32 { + if x != nil { + return x.Weight + } + return 0 +} + +func (x *VPPLBAS) GetFlags() uint32 { + if x != nil { + return x.Flags + } + return 0 +} + +func (x *VPPLBAS) GetNumBuckets() uint32 { + if x != nil { + return x.NumBuckets + } + return 0 +} + +func (x *VPPLBAS) GetInUseSinceNs() int64 { + if x != nil { + return x.InUseSinceNs + } + return 0 +} + +// VPPLBVIP mirrors VPP's lb_vip_details plus the attached application servers. +// Note: srv_type, dscp, and target_port are intentionally omitted — maglevd +// only supports GRE encap, so NAT/L3DSR-specific fields don't apply. +type VPPLBVIP struct { + state protoimpl.MessageState `protogen:"open.v1"` + Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` // CIDR, e.g. 192.0.2.1/32 + Protocol uint32 `protobuf:"varint,2,opt,name=protocol,proto3" json:"protocol,omitempty"` // 6=TCP, 17=UDP, 255=any + Port uint32 `protobuf:"varint,3,opt,name=port,proto3" json:"port,omitempty"` // 0 = all-port VIP + Encap string `protobuf:"bytes,4,opt,name=encap,proto3" json:"encap,omitempty"` // gre4|gre6|l3dsr|nat4|nat6 + FlowTableLength uint32 `protobuf:"varint,5,opt,name=flow_table_length,json=flowTableLength,proto3" json:"flow_table_length,omitempty"` + ApplicationServers []*VPPLBAS `protobuf:"bytes,6,rep,name=application_servers,json=applicationServers,proto3" json:"application_servers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VPPLBVIP) Reset() { + *x = VPPLBVIP{} + mi := &file_proto_maglev_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VPPLBVIP) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VPPLBVIP) ProtoMessage() {} + +func (x *VPPLBVIP) ProtoReflect() protoreflect.Message { + mi := &file_proto_maglev_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VPPLBVIP.ProtoReflect.Descriptor instead. +func (*VPPLBVIP) Descriptor() ([]byte, []int) { + return file_proto_maglev_proto_rawDescGZIP(), []int{16} +} + +func (x *VPPLBVIP) GetPrefix() string { + if x != nil { + return x.Prefix + } + return "" +} + +func (x *VPPLBVIP) GetProtocol() uint32 { + if x != nil { + return x.Protocol + } + return 0 +} + +func (x *VPPLBVIP) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +func (x *VPPLBVIP) GetEncap() string { + if x != nil { + return x.Encap + } + return "" +} + +func (x *VPPLBVIP) GetFlowTableLength() uint32 { + if x != nil { + return x.FlowTableLength + } + return 0 +} + +func (x *VPPLBVIP) GetApplicationServers() []*VPPLBAS { + if x != nil { + return x.ApplicationServers + } + return nil +} + +type VPPLBState struct { + state protoimpl.MessageState `protogen:"open.v1"` + Conf *VPPLBConf `protobuf:"bytes,1,opt,name=conf,proto3" json:"conf,omitempty"` + Vips []*VPPLBVIP `protobuf:"bytes,2,rep,name=vips,proto3" json:"vips,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VPPLBState) Reset() { + *x = VPPLBState{} + mi := &file_proto_maglev_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VPPLBState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VPPLBState) ProtoMessage() {} + +func (x *VPPLBState) ProtoReflect() protoreflect.Message { + mi := &file_proto_maglev_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VPPLBState.ProtoReflect.Descriptor instead. +func (*VPPLBState) Descriptor() ([]byte, []int) { + return file_proto_maglev_proto_rawDescGZIP(), []int{17} +} + +func (x *VPPLBState) GetConf() *VPPLBConf { + if x != nil { + return x.Conf + } + return nil +} + +func (x *VPPLBState) GetVips() []*VPPLBVIP { + if x != nil { + return x.Vips + } + return nil +} + +// SyncVPPLBStateRequest triggers a reconciliation between the maglev config +// and the VPP load-balancer dataplane. When frontend_name is set, only that +// frontend's VIP is synced (SyncLBStateVIP) and no VIPs are removed. When +// unset, a full reconciliation runs (SyncLBStateAll), which will also remove +// stale VIPs from VPP. +type SyncVPPLBStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + FrontendName *string `protobuf:"bytes,1,opt,name=frontend_name,json=frontendName,proto3,oneof" json:"frontend_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncVPPLBStateRequest) Reset() { + *x = SyncVPPLBStateRequest{} + mi := &file_proto_maglev_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncVPPLBStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncVPPLBStateRequest) ProtoMessage() {} + +func (x *SyncVPPLBStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_maglev_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncVPPLBStateRequest.ProtoReflect.Descriptor instead. +func (*SyncVPPLBStateRequest) Descriptor() ([]byte, []int) { + return file_proto_maglev_proto_rawDescGZIP(), []int{18} +} + +func (x *SyncVPPLBStateRequest) GetFrontendName() string { + if x != nil && x.FrontendName != nil { + return *x.FrontendName + } + return "" +} + +type SyncVPPLBStateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncVPPLBStateResponse) Reset() { + *x = SyncVPPLBStateResponse{} + mi := &file_proto_maglev_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncVPPLBStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncVPPLBStateResponse) ProtoMessage() {} + +func (x *SyncVPPLBStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_maglev_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncVPPLBStateResponse.ProtoReflect.Descriptor instead. +func (*SyncVPPLBStateResponse) Descriptor() ([]byte, []int) { + return file_proto_maglev_proto_rawDescGZIP(), []int{19} +} + type SetWeightRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Frontend string `protobuf:"bytes,1,opt,name=frontend,proto3" json:"frontend,omitempty"` @@ -637,7 +1043,7 @@ type SetWeightRequest struct { func (x *SetWeightRequest) Reset() { *x = SetWeightRequest{} - mi := &file_proto_maglev_proto_msgTypes[13] + mi := &file_proto_maglev_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -649,7 +1055,7 @@ func (x *SetWeightRequest) String() string { func (*SetWeightRequest) ProtoMessage() {} func (x *SetWeightRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[13] + mi := &file_proto_maglev_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -662,7 +1068,7 @@ func (x *SetWeightRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetWeightRequest.ProtoReflect.Descriptor instead. func (*SetWeightRequest) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{13} + return file_proto_maglev_proto_rawDescGZIP(), []int{20} } func (x *SetWeightRequest) GetFrontend() string { @@ -707,7 +1113,7 @@ type WatchRequest struct { func (x *WatchRequest) Reset() { *x = WatchRequest{} - mi := &file_proto_maglev_proto_msgTypes[14] + mi := &file_proto_maglev_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -719,7 +1125,7 @@ func (x *WatchRequest) String() string { func (*WatchRequest) ProtoMessage() {} func (x *WatchRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[14] + mi := &file_proto_maglev_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -732,7 +1138,7 @@ func (x *WatchRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WatchRequest.ProtoReflect.Descriptor instead. func (*WatchRequest) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{14} + return file_proto_maglev_proto_rawDescGZIP(), []int{21} } func (x *WatchRequest) GetLog() bool { @@ -772,7 +1178,7 @@ type ListFrontendsResponse struct { func (x *ListFrontendsResponse) Reset() { *x = ListFrontendsResponse{} - mi := &file_proto_maglev_proto_msgTypes[15] + mi := &file_proto_maglev_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -784,7 +1190,7 @@ func (x *ListFrontendsResponse) String() string { func (*ListFrontendsResponse) ProtoMessage() {} func (x *ListFrontendsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[15] + mi := &file_proto_maglev_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -797,7 +1203,7 @@ func (x *ListFrontendsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListFrontendsResponse.ProtoReflect.Descriptor instead. func (*ListFrontendsResponse) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{15} + return file_proto_maglev_proto_rawDescGZIP(), []int{22} } func (x *ListFrontendsResponse) GetFrontendNames() []string { @@ -817,7 +1223,7 @@ type PoolBackendInfo struct { func (x *PoolBackendInfo) Reset() { *x = PoolBackendInfo{} - mi := &file_proto_maglev_proto_msgTypes[16] + mi := &file_proto_maglev_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -829,7 +1235,7 @@ func (x *PoolBackendInfo) String() string { func (*PoolBackendInfo) ProtoMessage() {} func (x *PoolBackendInfo) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[16] + mi := &file_proto_maglev_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -842,7 +1248,7 @@ func (x *PoolBackendInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use PoolBackendInfo.ProtoReflect.Descriptor instead. func (*PoolBackendInfo) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{16} + return file_proto_maglev_proto_rawDescGZIP(), []int{23} } func (x *PoolBackendInfo) GetName() string { @@ -869,7 +1275,7 @@ type PoolInfo struct { func (x *PoolInfo) Reset() { *x = PoolInfo{} - mi := &file_proto_maglev_proto_msgTypes[17] + mi := &file_proto_maglev_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -881,7 +1287,7 @@ func (x *PoolInfo) String() string { func (*PoolInfo) ProtoMessage() {} func (x *PoolInfo) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[17] + mi := &file_proto_maglev_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -894,7 +1300,7 @@ func (x *PoolInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use PoolInfo.ProtoReflect.Descriptor instead. func (*PoolInfo) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{17} + return file_proto_maglev_proto_rawDescGZIP(), []int{24} } func (x *PoolInfo) GetName() string { @@ -925,7 +1331,7 @@ type FrontendInfo struct { func (x *FrontendInfo) Reset() { *x = FrontendInfo{} - mi := &file_proto_maglev_proto_msgTypes[18] + mi := &file_proto_maglev_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -937,7 +1343,7 @@ func (x *FrontendInfo) String() string { func (*FrontendInfo) ProtoMessage() {} func (x *FrontendInfo) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[18] + mi := &file_proto_maglev_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -950,7 +1356,7 @@ func (x *FrontendInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use FrontendInfo.ProtoReflect.Descriptor instead. func (*FrontendInfo) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{18} + return file_proto_maglev_proto_rawDescGZIP(), []int{25} } func (x *FrontendInfo) GetName() string { @@ -1004,7 +1410,7 @@ type ListBackendsResponse struct { func (x *ListBackendsResponse) Reset() { *x = ListBackendsResponse{} - mi := &file_proto_maglev_proto_msgTypes[19] + mi := &file_proto_maglev_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1016,7 +1422,7 @@ func (x *ListBackendsResponse) String() string { func (*ListBackendsResponse) ProtoMessage() {} func (x *ListBackendsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[19] + mi := &file_proto_maglev_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1029,7 +1435,7 @@ func (x *ListBackendsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListBackendsResponse.ProtoReflect.Descriptor instead. func (*ListBackendsResponse) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{19} + return file_proto_maglev_proto_rawDescGZIP(), []int{26} } func (x *ListBackendsResponse) GetBackendNames() []string { @@ -1048,7 +1454,7 @@ type ListHealthChecksResponse struct { func (x *ListHealthChecksResponse) Reset() { *x = ListHealthChecksResponse{} - mi := &file_proto_maglev_proto_msgTypes[20] + mi := &file_proto_maglev_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1060,7 +1466,7 @@ func (x *ListHealthChecksResponse) String() string { func (*ListHealthChecksResponse) ProtoMessage() {} func (x *ListHealthChecksResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[20] + mi := &file_proto_maglev_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1073,7 +1479,7 @@ func (x *ListHealthChecksResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListHealthChecksResponse.ProtoReflect.Descriptor instead. func (*ListHealthChecksResponse) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{20} + return file_proto_maglev_proto_rawDescGZIP(), []int{27} } func (x *ListHealthChecksResponse) GetNames() []string { @@ -1098,7 +1504,7 @@ type HTTPCheckParams struct { func (x *HTTPCheckParams) Reset() { *x = HTTPCheckParams{} - mi := &file_proto_maglev_proto_msgTypes[21] + mi := &file_proto_maglev_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1110,7 +1516,7 @@ func (x *HTTPCheckParams) String() string { func (*HTTPCheckParams) ProtoMessage() {} func (x *HTTPCheckParams) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[21] + mi := &file_proto_maglev_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1123,7 +1529,7 @@ func (x *HTTPCheckParams) ProtoReflect() protoreflect.Message { // Deprecated: Use HTTPCheckParams.ProtoReflect.Descriptor instead. func (*HTTPCheckParams) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{21} + return file_proto_maglev_proto_rawDescGZIP(), []int{28} } func (x *HTTPCheckParams) GetPath() string { @@ -1186,7 +1592,7 @@ type TCPCheckParams struct { func (x *TCPCheckParams) Reset() { *x = TCPCheckParams{} - mi := &file_proto_maglev_proto_msgTypes[22] + mi := &file_proto_maglev_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1198,7 +1604,7 @@ func (x *TCPCheckParams) String() string { func (*TCPCheckParams) ProtoMessage() {} func (x *TCPCheckParams) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[22] + mi := &file_proto_maglev_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1211,7 +1617,7 @@ func (x *TCPCheckParams) ProtoReflect() protoreflect.Message { // Deprecated: Use TCPCheckParams.ProtoReflect.Descriptor instead. func (*TCPCheckParams) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{22} + return file_proto_maglev_proto_rawDescGZIP(), []int{29} } func (x *TCPCheckParams) GetSsl() bool { @@ -1256,7 +1662,7 @@ type HealthCheckInfo struct { func (x *HealthCheckInfo) Reset() { *x = HealthCheckInfo{} - mi := &file_proto_maglev_proto_msgTypes[23] + mi := &file_proto_maglev_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1268,7 +1674,7 @@ func (x *HealthCheckInfo) String() string { func (*HealthCheckInfo) ProtoMessage() {} func (x *HealthCheckInfo) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[23] + mi := &file_proto_maglev_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1281,7 +1687,7 @@ func (x *HealthCheckInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use HealthCheckInfo.ProtoReflect.Descriptor instead. func (*HealthCheckInfo) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{23} + return file_proto_maglev_proto_rawDescGZIP(), []int{30} } func (x *HealthCheckInfo) GetName() string { @@ -1389,7 +1795,7 @@ type BackendInfo struct { func (x *BackendInfo) Reset() { *x = BackendInfo{} - mi := &file_proto_maglev_proto_msgTypes[24] + mi := &file_proto_maglev_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1401,7 +1807,7 @@ func (x *BackendInfo) String() string { func (*BackendInfo) ProtoMessage() {} func (x *BackendInfo) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[24] + mi := &file_proto_maglev_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1414,7 +1820,7 @@ func (x *BackendInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use BackendInfo.ProtoReflect.Descriptor instead. func (*BackendInfo) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{24} + return file_proto_maglev_proto_rawDescGZIP(), []int{31} } func (x *BackendInfo) GetName() string { @@ -1470,7 +1876,7 @@ type TransitionRecord struct { func (x *TransitionRecord) Reset() { *x = TransitionRecord{} - mi := &file_proto_maglev_proto_msgTypes[25] + mi := &file_proto_maglev_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1482,7 +1888,7 @@ func (x *TransitionRecord) String() string { func (*TransitionRecord) ProtoMessage() {} func (x *TransitionRecord) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[25] + mi := &file_proto_maglev_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1495,7 +1901,7 @@ func (x *TransitionRecord) ProtoReflect() protoreflect.Message { // Deprecated: Use TransitionRecord.ProtoReflect.Descriptor instead. func (*TransitionRecord) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{25} + return file_proto_maglev_proto_rawDescGZIP(), []int{32} } func (x *TransitionRecord) GetFrom() string { @@ -1530,7 +1936,7 @@ type LogAttr struct { func (x *LogAttr) Reset() { *x = LogAttr{} - mi := &file_proto_maglev_proto_msgTypes[26] + mi := &file_proto_maglev_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1542,7 +1948,7 @@ func (x *LogAttr) String() string { func (*LogAttr) ProtoMessage() {} func (x *LogAttr) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[26] + mi := &file_proto_maglev_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1555,7 +1961,7 @@ func (x *LogAttr) ProtoReflect() protoreflect.Message { // Deprecated: Use LogAttr.ProtoReflect.Descriptor instead. func (*LogAttr) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{26} + return file_proto_maglev_proto_rawDescGZIP(), []int{33} } func (x *LogAttr) GetKey() string { @@ -1585,7 +1991,7 @@ type LogEvent struct { func (x *LogEvent) Reset() { *x = LogEvent{} - mi := &file_proto_maglev_proto_msgTypes[27] + mi := &file_proto_maglev_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1597,7 +2003,7 @@ func (x *LogEvent) String() string { func (*LogEvent) ProtoMessage() {} func (x *LogEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[27] + mi := &file_proto_maglev_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1610,7 +2016,7 @@ func (x *LogEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use LogEvent.ProtoReflect.Descriptor instead. func (*LogEvent) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{27} + return file_proto_maglev_proto_rawDescGZIP(), []int{34} } func (x *LogEvent) GetAtUnixNs() int64 { @@ -1652,7 +2058,7 @@ type BackendEvent struct { func (x *BackendEvent) Reset() { *x = BackendEvent{} - mi := &file_proto_maglev_proto_msgTypes[28] + mi := &file_proto_maglev_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1664,7 +2070,7 @@ func (x *BackendEvent) String() string { func (*BackendEvent) ProtoMessage() {} func (x *BackendEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[28] + mi := &file_proto_maglev_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1677,7 +2083,7 @@ func (x *BackendEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use BackendEvent.ProtoReflect.Descriptor instead. func (*BackendEvent) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{28} + return file_proto_maglev_proto_rawDescGZIP(), []int{35} } func (x *BackendEvent) GetBackendName() string { @@ -1703,7 +2109,7 @@ type FrontendEvent struct { func (x *FrontendEvent) Reset() { *x = FrontendEvent{} - mi := &file_proto_maglev_proto_msgTypes[29] + mi := &file_proto_maglev_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1715,7 +2121,7 @@ func (x *FrontendEvent) String() string { func (*FrontendEvent) ProtoMessage() {} func (x *FrontendEvent) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[29] + mi := &file_proto_maglev_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1728,7 +2134,7 @@ func (x *FrontendEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use FrontendEvent.ProtoReflect.Descriptor instead. func (*FrontendEvent) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{29} + return file_proto_maglev_proto_rawDescGZIP(), []int{36} } // Event is the envelope returned by WatchEvents. @@ -1746,7 +2152,7 @@ type Event struct { func (x *Event) Reset() { *x = Event{} - mi := &file_proto_maglev_proto_msgTypes[30] + mi := &file_proto_maglev_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1758,7 +2164,7 @@ func (x *Event) String() string { func (*Event) ProtoMessage() {} func (x *Event) ProtoReflect() protoreflect.Message { - mi := &file_proto_maglev_proto_msgTypes[30] + mi := &file_proto_maglev_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1771,7 +2177,7 @@ func (x *Event) ProtoReflect() protoreflect.Message { // Deprecated: Use Event.ProtoReflect.Descriptor instead. func (*Event) Descriptor() ([]byte, []int) { - return file_proto_maglev_proto_rawDescGZIP(), []int{30} + return file_proto_maglev_proto_rawDescGZIP(), []int{37} } func (x *Event) GetEvent() isEvent_Event { @@ -1868,7 +2274,35 @@ const file_proto_maglev_proto_rawDesc = "" + "\x03pid\x18\x04 \x01(\rR\x03pid\x12\x1f\n" + "\vboottime_ns\x18\x05 \x01(\x03R\n" + "boottimeNs\x12%\n" + - "\x0econnecttime_ns\x18\x06 \x01(\x03R\rconnecttimeNs\"t\n" + + "\x0econnecttime_ns\x18\x06 \x01(\x03R\rconnecttimeNs\"\x16\n" + + "\x14GetVPPLBStateRequest\"\xb5\x01\n" + + "\tVPPLBConf\x12&\n" + + "\x0fip4_src_address\x18\x01 \x01(\tR\rip4SrcAddress\x12&\n" + + "\x0fip6_src_address\x18\x02 \x01(\tR\rip6SrcAddress\x125\n" + + "\x17sticky_buckets_per_core\x18\x03 \x01(\rR\x14stickyBucketsPerCore\x12!\n" + + "\fflow_timeout\x18\x04 \x01(\rR\vflowTimeout\"\x99\x01\n" + + "\aVPPLBAS\x12\x18\n" + + "\aaddress\x18\x01 \x01(\tR\aaddress\x12\x16\n" + + "\x06weight\x18\x02 \x01(\rR\x06weight\x12\x14\n" + + "\x05flags\x18\x03 \x01(\rR\x05flags\x12\x1f\n" + + "\vnum_buckets\x18\x04 \x01(\rR\n" + + "numBuckets\x12%\n" + + "\x0fin_use_since_ns\x18\x05 \x01(\x03R\finUseSinceNs\"\xd6\x01\n" + + "\bVPPLBVIP\x12\x16\n" + + "\x06prefix\x18\x01 \x01(\tR\x06prefix\x12\x1a\n" + + "\bprotocol\x18\x02 \x01(\rR\bprotocol\x12\x12\n" + + "\x04port\x18\x03 \x01(\rR\x04port\x12\x14\n" + + "\x05encap\x18\x04 \x01(\tR\x05encap\x12*\n" + + "\x11flow_table_length\x18\x05 \x01(\rR\x0fflowTableLength\x12@\n" + + "\x13application_servers\x18\x06 \x03(\v2\x0f.maglev.VPPLBASR\x12applicationServers\"Y\n" + + "\n" + + "VPPLBState\x12%\n" + + "\x04conf\x18\x01 \x01(\v2\x11.maglev.VPPLBConfR\x04conf\x12$\n" + + "\x04vips\x18\x02 \x03(\v2\x10.maglev.VPPLBVIPR\x04vips\"S\n" + + "\x15SyncVPPLBStateRequest\x12(\n" + + "\rfrontend_name\x18\x01 \x01(\tH\x00R\ffrontendName\x88\x01\x01B\x10\n" + + "\x0e_frontend_name\"\x18\n" + + "\x16SyncVPPLBStateResponse\"t\n" + "\x10SetWeightRequest\x12\x1a\n" + "\bfrontend\x18\x01 \x01(\tR\bfrontend\x12\x12\n" + "\x04pool\x18\x02 \x01(\tR\x04pool\x12\x18\n" + @@ -1964,7 +2398,7 @@ const file_proto_maglev_proto_rawDesc = "" + "\x03log\x18\x01 \x01(\v2\x10.maglev.LogEventH\x00R\x03log\x120\n" + "\abackend\x18\x02 \x01(\v2\x14.maglev.BackendEventH\x00R\abackend\x123\n" + "\bfrontend\x18\x03 \x01(\v2\x15.maglev.FrontendEventH\x00R\bfrontendB\a\n" + - "\x05event2\x8c\b\n" + + "\x05event2\xa0\t\n" + "\x06Maglev\x12L\n" + "\rListFrontends\x12\x1c.maglev.ListFrontendsRequest\x1a\x1d.maglev.ListFrontendsResponse\x12?\n" + "\vGetFrontend\x12\x1a.maglev.GetFrontendRequest\x1a\x14.maglev.FrontendInfo\x12I\n" + @@ -1982,7 +2416,9 @@ const file_proto_maglev_proto_rawDesc = "" + "\vCheckConfig\x12\x1a.maglev.CheckConfigRequest\x1a\x1b.maglev.CheckConfigResponse\x12I\n" + "\fReloadConfig\x12\x1b.maglev.ReloadConfigRequest\x1a\x1c.maglev.ReloadConfigResponse\x128\n" + "\n" + - "GetVPPInfo\x12\x19.maglev.GetVPPInfoRequest\x1a\x0f.maglev.VPPInfoB.Z,git.ipng.ch/ipng/vpp-maglev/internal/grpcapib\x06proto3" + "GetVPPInfo\x12\x19.maglev.GetVPPInfoRequest\x1a\x0f.maglev.VPPInfo\x12A\n" + + "\rGetVPPLBState\x12\x1c.maglev.GetVPPLBStateRequest\x1a\x12.maglev.VPPLBState\x12O\n" + + "\x0eSyncVPPLBState\x12\x1d.maglev.SyncVPPLBStateRequest\x1a\x1e.maglev.SyncVPPLBStateResponseB.Z,git.ipng.ch/ipng/vpp-maglev/internal/grpcapib\x06proto3" var ( file_proto_maglev_proto_rawDescOnce sync.Once @@ -1996,7 +2432,7 @@ func file_proto_maglev_proto_rawDescGZIP() []byte { return file_proto_maglev_proto_rawDescData } -var file_proto_maglev_proto_msgTypes = make([]protoimpl.MessageInfo, 31) +var file_proto_maglev_proto_msgTypes = make([]protoimpl.MessageInfo, 38) var file_proto_maglev_proto_goTypes = []any{ (*ListFrontendsRequest)(nil), // 0: maglev.ListFrontendsRequest (*GetFrontendRequest)(nil), // 1: maglev.GetFrontendRequest @@ -2011,71 +2447,85 @@ var file_proto_maglev_proto_goTypes = []any{ (*ReloadConfigResponse)(nil), // 10: maglev.ReloadConfigResponse (*GetVPPInfoRequest)(nil), // 11: maglev.GetVPPInfoRequest (*VPPInfo)(nil), // 12: maglev.VPPInfo - (*SetWeightRequest)(nil), // 13: maglev.SetWeightRequest - (*WatchRequest)(nil), // 14: maglev.WatchRequest - (*ListFrontendsResponse)(nil), // 15: maglev.ListFrontendsResponse - (*PoolBackendInfo)(nil), // 16: maglev.PoolBackendInfo - (*PoolInfo)(nil), // 17: maglev.PoolInfo - (*FrontendInfo)(nil), // 18: maglev.FrontendInfo - (*ListBackendsResponse)(nil), // 19: maglev.ListBackendsResponse - (*ListHealthChecksResponse)(nil), // 20: maglev.ListHealthChecksResponse - (*HTTPCheckParams)(nil), // 21: maglev.HTTPCheckParams - (*TCPCheckParams)(nil), // 22: maglev.TCPCheckParams - (*HealthCheckInfo)(nil), // 23: maglev.HealthCheckInfo - (*BackendInfo)(nil), // 24: maglev.BackendInfo - (*TransitionRecord)(nil), // 25: maglev.TransitionRecord - (*LogAttr)(nil), // 26: maglev.LogAttr - (*LogEvent)(nil), // 27: maglev.LogEvent - (*BackendEvent)(nil), // 28: maglev.BackendEvent - (*FrontendEvent)(nil), // 29: maglev.FrontendEvent - (*Event)(nil), // 30: maglev.Event + (*GetVPPLBStateRequest)(nil), // 13: maglev.GetVPPLBStateRequest + (*VPPLBConf)(nil), // 14: maglev.VPPLBConf + (*VPPLBAS)(nil), // 15: maglev.VPPLBAS + (*VPPLBVIP)(nil), // 16: maglev.VPPLBVIP + (*VPPLBState)(nil), // 17: maglev.VPPLBState + (*SyncVPPLBStateRequest)(nil), // 18: maglev.SyncVPPLBStateRequest + (*SyncVPPLBStateResponse)(nil), // 19: maglev.SyncVPPLBStateResponse + (*SetWeightRequest)(nil), // 20: maglev.SetWeightRequest + (*WatchRequest)(nil), // 21: maglev.WatchRequest + (*ListFrontendsResponse)(nil), // 22: maglev.ListFrontendsResponse + (*PoolBackendInfo)(nil), // 23: maglev.PoolBackendInfo + (*PoolInfo)(nil), // 24: maglev.PoolInfo + (*FrontendInfo)(nil), // 25: maglev.FrontendInfo + (*ListBackendsResponse)(nil), // 26: maglev.ListBackendsResponse + (*ListHealthChecksResponse)(nil), // 27: maglev.ListHealthChecksResponse + (*HTTPCheckParams)(nil), // 28: maglev.HTTPCheckParams + (*TCPCheckParams)(nil), // 29: maglev.TCPCheckParams + (*HealthCheckInfo)(nil), // 30: maglev.HealthCheckInfo + (*BackendInfo)(nil), // 31: maglev.BackendInfo + (*TransitionRecord)(nil), // 32: maglev.TransitionRecord + (*LogAttr)(nil), // 33: maglev.LogAttr + (*LogEvent)(nil), // 34: maglev.LogEvent + (*BackendEvent)(nil), // 35: maglev.BackendEvent + (*FrontendEvent)(nil), // 36: maglev.FrontendEvent + (*Event)(nil), // 37: maglev.Event } var file_proto_maglev_proto_depIdxs = []int32{ - 16, // 0: maglev.PoolInfo.backends:type_name -> maglev.PoolBackendInfo - 17, // 1: maglev.FrontendInfo.pools:type_name -> maglev.PoolInfo - 21, // 2: maglev.HealthCheckInfo.http:type_name -> maglev.HTTPCheckParams - 22, // 3: maglev.HealthCheckInfo.tcp:type_name -> maglev.TCPCheckParams - 25, // 4: maglev.BackendInfo.transitions:type_name -> maglev.TransitionRecord - 26, // 5: maglev.LogEvent.attrs:type_name -> maglev.LogAttr - 25, // 6: maglev.BackendEvent.transition:type_name -> maglev.TransitionRecord - 27, // 7: maglev.Event.log:type_name -> maglev.LogEvent - 28, // 8: maglev.Event.backend:type_name -> maglev.BackendEvent - 29, // 9: maglev.Event.frontend:type_name -> maglev.FrontendEvent - 0, // 10: maglev.Maglev.ListFrontends:input_type -> maglev.ListFrontendsRequest - 1, // 11: maglev.Maglev.GetFrontend:input_type -> maglev.GetFrontendRequest - 2, // 12: maglev.Maglev.ListBackends:input_type -> maglev.ListBackendsRequest - 3, // 13: maglev.Maglev.GetBackend:input_type -> maglev.GetBackendRequest - 4, // 14: maglev.Maglev.PauseBackend:input_type -> maglev.BackendRequest - 4, // 15: maglev.Maglev.ResumeBackend:input_type -> maglev.BackendRequest - 4, // 16: maglev.Maglev.EnableBackend:input_type -> maglev.BackendRequest - 4, // 17: maglev.Maglev.DisableBackend:input_type -> maglev.BackendRequest - 5, // 18: maglev.Maglev.ListHealthChecks:input_type -> maglev.ListHealthChecksRequest - 6, // 19: maglev.Maglev.GetHealthCheck:input_type -> maglev.GetHealthCheckRequest - 13, // 20: maglev.Maglev.SetFrontendPoolBackendWeight:input_type -> maglev.SetWeightRequest - 14, // 21: maglev.Maglev.WatchEvents:input_type -> maglev.WatchRequest - 7, // 22: maglev.Maglev.CheckConfig:input_type -> maglev.CheckConfigRequest - 9, // 23: maglev.Maglev.ReloadConfig:input_type -> maglev.ReloadConfigRequest - 11, // 24: maglev.Maglev.GetVPPInfo:input_type -> maglev.GetVPPInfoRequest - 15, // 25: maglev.Maglev.ListFrontends:output_type -> maglev.ListFrontendsResponse - 18, // 26: maglev.Maglev.GetFrontend:output_type -> maglev.FrontendInfo - 19, // 27: maglev.Maglev.ListBackends:output_type -> maglev.ListBackendsResponse - 24, // 28: maglev.Maglev.GetBackend:output_type -> maglev.BackendInfo - 24, // 29: maglev.Maglev.PauseBackend:output_type -> maglev.BackendInfo - 24, // 30: maglev.Maglev.ResumeBackend:output_type -> maglev.BackendInfo - 24, // 31: maglev.Maglev.EnableBackend:output_type -> maglev.BackendInfo - 24, // 32: maglev.Maglev.DisableBackend:output_type -> maglev.BackendInfo - 20, // 33: maglev.Maglev.ListHealthChecks:output_type -> maglev.ListHealthChecksResponse - 23, // 34: maglev.Maglev.GetHealthCheck:output_type -> maglev.HealthCheckInfo - 18, // 35: maglev.Maglev.SetFrontendPoolBackendWeight:output_type -> maglev.FrontendInfo - 30, // 36: maglev.Maglev.WatchEvents:output_type -> maglev.Event - 8, // 37: maglev.Maglev.CheckConfig:output_type -> maglev.CheckConfigResponse - 10, // 38: maglev.Maglev.ReloadConfig:output_type -> maglev.ReloadConfigResponse - 12, // 39: maglev.Maglev.GetVPPInfo:output_type -> maglev.VPPInfo - 25, // [25:40] is the sub-list for method output_type - 10, // [10:25] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 15, // 0: maglev.VPPLBVIP.application_servers:type_name -> maglev.VPPLBAS + 14, // 1: maglev.VPPLBState.conf:type_name -> maglev.VPPLBConf + 16, // 2: maglev.VPPLBState.vips:type_name -> maglev.VPPLBVIP + 23, // 3: maglev.PoolInfo.backends:type_name -> maglev.PoolBackendInfo + 24, // 4: maglev.FrontendInfo.pools:type_name -> maglev.PoolInfo + 28, // 5: maglev.HealthCheckInfo.http:type_name -> maglev.HTTPCheckParams + 29, // 6: maglev.HealthCheckInfo.tcp:type_name -> maglev.TCPCheckParams + 32, // 7: maglev.BackendInfo.transitions:type_name -> maglev.TransitionRecord + 33, // 8: maglev.LogEvent.attrs:type_name -> maglev.LogAttr + 32, // 9: maglev.BackendEvent.transition:type_name -> maglev.TransitionRecord + 34, // 10: maglev.Event.log:type_name -> maglev.LogEvent + 35, // 11: maglev.Event.backend:type_name -> maglev.BackendEvent + 36, // 12: maglev.Event.frontend:type_name -> maglev.FrontendEvent + 0, // 13: maglev.Maglev.ListFrontends:input_type -> maglev.ListFrontendsRequest + 1, // 14: maglev.Maglev.GetFrontend:input_type -> maglev.GetFrontendRequest + 2, // 15: maglev.Maglev.ListBackends:input_type -> maglev.ListBackendsRequest + 3, // 16: maglev.Maglev.GetBackend:input_type -> maglev.GetBackendRequest + 4, // 17: maglev.Maglev.PauseBackend:input_type -> maglev.BackendRequest + 4, // 18: maglev.Maglev.ResumeBackend:input_type -> maglev.BackendRequest + 4, // 19: maglev.Maglev.EnableBackend:input_type -> maglev.BackendRequest + 4, // 20: maglev.Maglev.DisableBackend:input_type -> maglev.BackendRequest + 5, // 21: maglev.Maglev.ListHealthChecks:input_type -> maglev.ListHealthChecksRequest + 6, // 22: maglev.Maglev.GetHealthCheck:input_type -> maglev.GetHealthCheckRequest + 20, // 23: maglev.Maglev.SetFrontendPoolBackendWeight:input_type -> maglev.SetWeightRequest + 21, // 24: maglev.Maglev.WatchEvents:input_type -> maglev.WatchRequest + 7, // 25: maglev.Maglev.CheckConfig:input_type -> maglev.CheckConfigRequest + 9, // 26: maglev.Maglev.ReloadConfig:input_type -> maglev.ReloadConfigRequest + 11, // 27: maglev.Maglev.GetVPPInfo:input_type -> maglev.GetVPPInfoRequest + 13, // 28: maglev.Maglev.GetVPPLBState:input_type -> maglev.GetVPPLBStateRequest + 18, // 29: maglev.Maglev.SyncVPPLBState:input_type -> maglev.SyncVPPLBStateRequest + 22, // 30: maglev.Maglev.ListFrontends:output_type -> maglev.ListFrontendsResponse + 25, // 31: maglev.Maglev.GetFrontend:output_type -> maglev.FrontendInfo + 26, // 32: maglev.Maglev.ListBackends:output_type -> maglev.ListBackendsResponse + 31, // 33: maglev.Maglev.GetBackend:output_type -> maglev.BackendInfo + 31, // 34: maglev.Maglev.PauseBackend:output_type -> maglev.BackendInfo + 31, // 35: maglev.Maglev.ResumeBackend:output_type -> maglev.BackendInfo + 31, // 36: maglev.Maglev.EnableBackend:output_type -> maglev.BackendInfo + 31, // 37: maglev.Maglev.DisableBackend:output_type -> maglev.BackendInfo + 27, // 38: maglev.Maglev.ListHealthChecks:output_type -> maglev.ListHealthChecksResponse + 30, // 39: maglev.Maglev.GetHealthCheck:output_type -> maglev.HealthCheckInfo + 25, // 40: maglev.Maglev.SetFrontendPoolBackendWeight:output_type -> maglev.FrontendInfo + 37, // 41: maglev.Maglev.WatchEvents:output_type -> maglev.Event + 8, // 42: maglev.Maglev.CheckConfig:output_type -> maglev.CheckConfigResponse + 10, // 43: maglev.Maglev.ReloadConfig:output_type -> maglev.ReloadConfigResponse + 12, // 44: maglev.Maglev.GetVPPInfo:output_type -> maglev.VPPInfo + 17, // 45: maglev.Maglev.GetVPPLBState:output_type -> maglev.VPPLBState + 19, // 46: maglev.Maglev.SyncVPPLBState:output_type -> maglev.SyncVPPLBStateResponse + 30, // [30:47] is the sub-list for method output_type + 13, // [13:30] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_proto_maglev_proto_init() } @@ -2083,8 +2533,9 @@ func file_proto_maglev_proto_init() { if File_proto_maglev_proto != nil { return } - file_proto_maglev_proto_msgTypes[14].OneofWrappers = []any{} - file_proto_maglev_proto_msgTypes[30].OneofWrappers = []any{ + file_proto_maglev_proto_msgTypes[18].OneofWrappers = []any{} + file_proto_maglev_proto_msgTypes[21].OneofWrappers = []any{} + file_proto_maglev_proto_msgTypes[37].OneofWrappers = []any{ (*Event_Log)(nil), (*Event_Backend)(nil), (*Event_Frontend)(nil), @@ -2095,7 +2546,7 @@ func file_proto_maglev_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_maglev_proto_rawDesc), len(file_proto_maglev_proto_rawDesc)), NumEnums: 0, - NumMessages: 31, + NumMessages: 38, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/grpcapi/maglev_grpc.pb.go b/internal/grpcapi/maglev_grpc.pb.go index b181d54..b57dcf6 100644 --- a/internal/grpcapi/maglev_grpc.pb.go +++ b/internal/grpcapi/maglev_grpc.pb.go @@ -34,6 +34,8 @@ const ( Maglev_CheckConfig_FullMethodName = "/maglev.Maglev/CheckConfig" Maglev_ReloadConfig_FullMethodName = "/maglev.Maglev/ReloadConfig" Maglev_GetVPPInfo_FullMethodName = "/maglev.Maglev/GetVPPInfo" + Maglev_GetVPPLBState_FullMethodName = "/maglev.Maglev/GetVPPLBState" + Maglev_SyncVPPLBState_FullMethodName = "/maglev.Maglev/SyncVPPLBState" ) // MaglevClient is the client API for Maglev service. @@ -57,6 +59,8 @@ type MaglevClient interface { CheckConfig(ctx context.Context, in *CheckConfigRequest, opts ...grpc.CallOption) (*CheckConfigResponse, error) ReloadConfig(ctx context.Context, in *ReloadConfigRequest, opts ...grpc.CallOption) (*ReloadConfigResponse, error) GetVPPInfo(ctx context.Context, in *GetVPPInfoRequest, opts ...grpc.CallOption) (*VPPInfo, error) + GetVPPLBState(ctx context.Context, in *GetVPPLBStateRequest, opts ...grpc.CallOption) (*VPPLBState, error) + SyncVPPLBState(ctx context.Context, in *SyncVPPLBStateRequest, opts ...grpc.CallOption) (*SyncVPPLBStateResponse, error) } type maglevClient struct { @@ -226,6 +230,26 @@ func (c *maglevClient) GetVPPInfo(ctx context.Context, in *GetVPPInfoRequest, op return out, nil } +func (c *maglevClient) GetVPPLBState(ctx context.Context, in *GetVPPLBStateRequest, opts ...grpc.CallOption) (*VPPLBState, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(VPPLBState) + err := c.cc.Invoke(ctx, Maglev_GetVPPLBState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *maglevClient) SyncVPPLBState(ctx context.Context, in *SyncVPPLBStateRequest, opts ...grpc.CallOption) (*SyncVPPLBStateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SyncVPPLBStateResponse) + err := c.cc.Invoke(ctx, Maglev_SyncVPPLBState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // MaglevServer is the server API for Maglev service. // All implementations must embed UnimplementedMaglevServer // for forward compatibility. @@ -247,6 +271,8 @@ type MaglevServer interface { CheckConfig(context.Context, *CheckConfigRequest) (*CheckConfigResponse, error) ReloadConfig(context.Context, *ReloadConfigRequest) (*ReloadConfigResponse, error) GetVPPInfo(context.Context, *GetVPPInfoRequest) (*VPPInfo, error) + GetVPPLBState(context.Context, *GetVPPLBStateRequest) (*VPPLBState, error) + SyncVPPLBState(context.Context, *SyncVPPLBStateRequest) (*SyncVPPLBStateResponse, error) mustEmbedUnimplementedMaglevServer() } @@ -302,6 +328,12 @@ func (UnimplementedMaglevServer) ReloadConfig(context.Context, *ReloadConfigRequ func (UnimplementedMaglevServer) GetVPPInfo(context.Context, *GetVPPInfoRequest) (*VPPInfo, error) { return nil, status.Error(codes.Unimplemented, "method GetVPPInfo not implemented") } +func (UnimplementedMaglevServer) GetVPPLBState(context.Context, *GetVPPLBStateRequest) (*VPPLBState, error) { + return nil, status.Error(codes.Unimplemented, "method GetVPPLBState not implemented") +} +func (UnimplementedMaglevServer) SyncVPPLBState(context.Context, *SyncVPPLBStateRequest) (*SyncVPPLBStateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SyncVPPLBState not implemented") +} func (UnimplementedMaglevServer) mustEmbedUnimplementedMaglevServer() {} func (UnimplementedMaglevServer) testEmbeddedByValue() {} @@ -586,6 +618,42 @@ func _Maglev_GetVPPInfo_Handler(srv interface{}, ctx context.Context, dec func(i return interceptor(ctx, in, info, handler) } +func _Maglev_GetVPPLBState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetVPPLBStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MaglevServer).GetVPPLBState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Maglev_GetVPPLBState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MaglevServer).GetVPPLBState(ctx, req.(*GetVPPLBStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Maglev_SyncVPPLBState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SyncVPPLBStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MaglevServer).SyncVPPLBState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Maglev_SyncVPPLBState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MaglevServer).SyncVPPLBState(ctx, req.(*SyncVPPLBStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Maglev_ServiceDesc is the grpc.ServiceDesc for Maglev service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -649,6 +717,14 @@ var Maglev_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetVPPInfo", Handler: _Maglev_GetVPPInfo_Handler, }, + { + MethodName: "GetVPPLBState", + Handler: _Maglev_GetVPPLBState_Handler, + }, + { + MethodName: "SyncVPPLBState", + Handler: _Maglev_SyncVPPLBState_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/internal/grpcapi/server.go b/internal/grpcapi/server.go index be20c0d..cdf6694 100644 --- a/internal/grpcapi/server.go +++ b/internal/grpcapi/server.go @@ -4,6 +4,7 @@ package grpcapi import ( "context" + "errors" "log/slog" "net" @@ -255,6 +256,13 @@ func (s *Server) doReloadConfig() *ReloadConfigResponse { ReloadError: err.Error(), } } + // Push new global LB conf to VPP if anything changed. SetLBConf is a + // no-op when VPP isn't connected or when the values are unchanged. + if s.vppClient != nil { + if err := s.vppClient.SetLBConf(newCfg); err != nil { + slog.Warn("vpp-lb-conf-set-failed", "err", err) + } + } slog.Info("config-reload-done", "frontends", len(newCfg.Frontends)) return &ReloadConfigResponse{Ok: true} } @@ -282,6 +290,84 @@ func (s *Server) GetVPPInfo(_ context.Context, _ *GetVPPInfoRequest) (*VPPInfo, }, nil } +// GetVPPLBState returns a snapshot of the VPP load-balancer plugin state. +func (s *Server) GetVPPLBState(_ context.Context, _ *GetVPPLBStateRequest) (*VPPLBState, error) { + if s.vppClient == nil { + return nil, status.Error(codes.Unavailable, "VPP integration is disabled") + } + state, err := s.vppClient.GetLBStateAll() + if err != nil { + return nil, status.Errorf(codes.Unavailable, "%v", err) + } + return lbStateToProto(state), nil +} + +// SyncVPPLBState runs the LB reconciler. With frontend_name unset it does a +// full sync (SyncLBStateAll), which may remove stale VIPs. With frontend_name +// set it does a single-VIP sync (SyncLBStateVIP) that only adds/updates. +func (s *Server) SyncVPPLBState(_ context.Context, req *SyncVPPLBStateRequest) (*SyncVPPLBStateResponse, error) { + if s.vppClient == nil { + return nil, status.Error(codes.Unavailable, "VPP integration is disabled") + } + cfg := s.checker.Config() + if req.FrontendName != nil && *req.FrontendName != "" { + if err := s.vppClient.SyncLBStateVIP(cfg, *req.FrontendName); err != nil { + if errors.Is(err, vpp.ErrFrontendNotFound) { + return nil, status.Errorf(codes.NotFound, "%v", err) + } + return nil, status.Errorf(codes.Unavailable, "%v", err) + } + return &SyncVPPLBStateResponse{}, nil + } + if err := s.vppClient.SyncLBStateAll(cfg); err != nil { + return nil, status.Errorf(codes.Unavailable, "%v", err) + } + return &SyncVPPLBStateResponse{}, nil +} + +// lbStateToProto converts the vpp package's LBState into the proto message. +func lbStateToProto(s *vpp.LBState) *VPPLBState { + out := &VPPLBState{ + Conf: &VPPLBConf{ + Ip4SrcAddress: ipStringOrEmpty(s.Conf.IP4SrcAddress), + Ip6SrcAddress: ipStringOrEmpty(s.Conf.IP6SrcAddress), + StickyBucketsPerCore: s.Conf.StickyBucketsPerCore, + FlowTimeout: s.Conf.FlowTimeout, + }, + } + for _, v := range s.VIPs { + pv := &VPPLBVIP{ + Prefix: v.Prefix.String(), + Protocol: uint32(v.Protocol), + Port: uint32(v.Port), + Encap: v.Encap, + FlowTableLength: uint32(v.FlowTableLength), + } + for _, a := range v.ASes { + var ts int64 + if !a.InUseSince.IsZero() { + ts = a.InUseSince.UnixNano() + } + pv.ApplicationServers = append(pv.ApplicationServers, &VPPLBAS{ + Address: a.Address.String(), + Weight: uint32(a.Weight), + Flags: uint32(a.Flags), + NumBuckets: a.NumBuckets, + InUseSinceNs: ts, + }) + } + out.Vips = append(out.Vips, pv) + } + return out +} + +func ipStringOrEmpty(ip net.IP) string { + if len(ip) == 0 || ip.IsUnspecified() { + return "" + } + return ip.String() +} + // ---- conversion helpers ---------------------------------------------------- func frontendToProto(name string, fe config.Frontend) *FrontendInfo { diff --git a/internal/vpp/apilog.go b/internal/vpp/apilog.go new file mode 100644 index 0000000..a648f8c --- /dev/null +++ b/internal/vpp/apilog.go @@ -0,0 +1,122 @@ +// Copyright (c) 2026, Pim van Pelt + +package vpp + +import ( + "fmt" + "log/slog" + + "go.fd.io/govpp/api" +) + +// loggedChannel wraps an api.Channel so that every VPP request/reply is +// recorded via slog at DEBUG level. All code in this package MUST send VPP +// messages through a loggedChannel (via Client.apiChannel) so we have a +// complete audit trail of what was sent to the dataplane. +type loggedChannel struct { + ch api.Channel +} + +// apiChannel opens a new API channel wrapped in logging. This is the only +// approved way to talk to VPP; do not call conn.NewAPIChannel directly. +func (c *Client) apiChannel() (*loggedChannel, error) { + c.mu.Lock() + conn := c.apiConn + c.mu.Unlock() + if conn == nil { + return nil, errNotConnected + } + ch, err := conn.NewAPIChannel() + if err != nil { + return nil, err + } + return &loggedChannel{ch: ch}, nil +} + +// Close closes the underlying channel. +func (lc *loggedChannel) Close() { lc.ch.Close() } + +// SendRequest logs the outgoing message and returns a wrapped request context. +func (lc *loggedChannel) SendRequest(msg api.Message) *loggedRequestCtx { + slog.Debug("vpp-api-send", + "msg", msg.GetMessageName(), + "crc", msg.GetCrcString(), + "payload", fmt.Sprintf("%+v", msg), + ) + return &loggedRequestCtx{ + ctx: lc.ch.SendRequest(msg), + name: msg.GetMessageName(), + } +} + +// SendMultiRequest logs the outgoing message and returns a wrapped multi-request context. +func (lc *loggedChannel) SendMultiRequest(msg api.Message) *loggedMultiRequestCtx { + slog.Debug("vpp-api-send-multi", + "msg", msg.GetMessageName(), + "crc", msg.GetCrcString(), + "payload", fmt.Sprintf("%+v", msg), + ) + return &loggedMultiRequestCtx{ + ctx: lc.ch.SendMultiRequest(msg), + name: msg.GetMessageName(), + } +} + +// loggedRequestCtx wraps api.RequestCtx and logs the reply on ReceiveReply. +type loggedRequestCtx struct { + ctx api.RequestCtx + name string +} + +func (r *loggedRequestCtx) ReceiveReply(msg api.Message) error { + err := r.ctx.ReceiveReply(msg) + if err != nil { + slog.Debug("vpp-api-recv", + "req", r.name, + "reply", msg.GetMessageName(), + "err", err, + ) + return err + } + slog.Debug("vpp-api-recv", + "req", r.name, + "reply", msg.GetMessageName(), + "payload", fmt.Sprintf("%+v", msg), + ) + return nil +} + +// loggedMultiRequestCtx wraps api.MultiRequestCtx and logs each reply. +type loggedMultiRequestCtx struct { + ctx api.MultiRequestCtx + name string + seq int +} + +func (r *loggedMultiRequestCtx) ReceiveReply(msg api.Message) (bool, error) { + stop, err := r.ctx.ReceiveReply(msg) + if err != nil { + slog.Debug("vpp-api-recv-multi", + "req", r.name, + "reply", msg.GetMessageName(), + "seq", r.seq, + "err", err, + ) + return stop, err + } + if stop { + slog.Debug("vpp-api-recv-multi-done", + "req", r.name, + "count", r.seq, + ) + return stop, nil + } + slog.Debug("vpp-api-recv-multi", + "req", r.name, + "reply", msg.GetMessageName(), + "seq", r.seq, + "payload", fmt.Sprintf("%+v", msg), + ) + r.seq++ + return stop, nil +} diff --git a/internal/vpp/binapi/interface_types/interface_types.ba.go b/internal/vpp/binapi/interface_types/interface_types.ba.go new file mode 100644 index 0000000..25547af --- /dev/null +++ b/internal/vpp/binapi/interface_types/interface_types.ba.go @@ -0,0 +1,304 @@ +// Code generated by GoVPP's binapi-generator. DO NOT EDIT. + +// Package interface_types contains generated bindings for API file interface_types.api. +// +// Contents: +// - 1 alias +// - 7 enums +package interface_types + +import ( + "strconv" + + api "go.fd.io/govpp/api" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the GoVPP api package it is being compiled against. +// A compilation error at this line likely means your copy of the +// GoVPP api package needs to be updated. +const _ = api.GoVppAPIPackageIsVersion2 + +const ( + APIFile = "interface_types" + APIVersion = "1.0.0" + VersionCrc = 0x7f2ba79a +) + +// Direction defines enum 'direction'. +type Direction uint8 + +const ( + RX Direction = 0 + TX Direction = 1 +) + +var ( + Direction_name = map[uint8]string{ + 0: "RX", + 1: "TX", + } + Direction_value = map[string]uint8{ + "RX": 0, + "TX": 1, + } +) + +func (x Direction) String() string { + s, ok := Direction_name[uint8(x)] + if ok { + return s + } + return "Direction(" + strconv.Itoa(int(x)) + ")" +} + +// IfStatusFlags defines enum 'if_status_flags'. +type IfStatusFlags uint32 + +const ( + IF_STATUS_API_FLAG_ADMIN_UP IfStatusFlags = 1 + IF_STATUS_API_FLAG_LINK_UP IfStatusFlags = 2 +) + +var ( + IfStatusFlags_name = map[uint32]string{ + 1: "IF_STATUS_API_FLAG_ADMIN_UP", + 2: "IF_STATUS_API_FLAG_LINK_UP", + } + IfStatusFlags_value = map[string]uint32{ + "IF_STATUS_API_FLAG_ADMIN_UP": 1, + "IF_STATUS_API_FLAG_LINK_UP": 2, + } +) + +func (x IfStatusFlags) String() string { + s, ok := IfStatusFlags_name[uint32(x)] + if ok { + return s + } + str := func(n uint32) string { + s, ok := IfStatusFlags_name[uint32(n)] + if ok { + return s + } + return "IfStatusFlags(" + strconv.Itoa(int(n)) + ")" + } + for i := uint32(0); i <= 32; i++ { + val := uint32(x) + if val&(1< + +package vpp + +import ( + "bytes" + "fmt" + "log/slog" + "net" + + "git.ipng.ch/ipng/vpp-maglev/internal/config" + ip_types "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/ip_types" + lb "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/lb" +) + +// SetLBConf sends lb_conf to VPP with the global load-balancer settings from +// cfg. Called on VPP connect (startup and reconnect) and after every +// successful config reload. Returns nil if VPP is not connected (silently +// skipped — the next connect will push the conf). +// +// The values sent are cached on the Client; if SetLBConf is called twice in +// a row with unchanged values, no API call is made and no log is emitted. +func (c *Client) SetLBConf(cfg *config.Config) error { + if !c.IsConnected() { + return nil + } + + req := &lb.LbConf{ + IP4SrcAddress: ip_types.IP4Address(ip4Bytes(cfg.VPP.LB.IPv4SrcAddress)), + IP6SrcAddress: ip_types.IP6Address(ip6Bytes(cfg.VPP.LB.IPv6SrcAddress)), + StickyBucketsPerCore: cfg.VPP.LB.StickyBucketsPerCore, + FlowTimeout: uint32(cfg.VPP.LB.FlowTimeout.Seconds()), + } + + // Skip if nothing changed since the last successful push. + c.mu.Lock() + prev := c.lastLBConf + c.mu.Unlock() + if prev != nil && + bytes.Equal(prev.IP4SrcAddress[:], req.IP4SrcAddress[:]) && + bytes.Equal(prev.IP6SrcAddress[:], req.IP6SrcAddress[:]) && + prev.StickyBucketsPerCore == req.StickyBucketsPerCore && + prev.FlowTimeout == req.FlowTimeout { + return nil + } + + ch, err := c.apiChannel() + if err != nil { + return err + } + defer ch.Close() + + reply := &lb.LbConfReply{} + if err := ch.SendRequest(req).ReceiveReply(reply); err != nil { + return fmt.Errorf("lb_conf: %w", err) + } + if reply.Retval != 0 { + return fmt.Errorf("lb_conf: retval=%d", reply.Retval) + } + + c.mu.Lock() + c.lastLBConf = req + c.mu.Unlock() + + slog.Info("vpp-lb-conf-set", + "ipv4-src", ipStringFromCfg(cfg.VPP.LB.IPv4SrcAddress), + "ipv6-src", ipStringFromCfg(cfg.VPP.LB.IPv6SrcAddress), + "sticky-buckets-per-core", req.StickyBucketsPerCore, + "flow-timeout", cfg.VPP.LB.FlowTimeout.String()) + return nil +} + +// ip4Bytes returns the 4-byte representation of an IPv4 address, or all-zero +// if ip is nil/unset. +func ip4Bytes(ip net.IP) [4]byte { + var out [4]byte + if ip == nil { + return out + } + if b := ip.To4(); b != nil { + copy(out[:], b) + } + return out +} + +// ip6Bytes returns the 16-byte representation of an IPv6 address, or all-zero +// if ip is nil/unset. +func ip6Bytes(ip net.IP) [16]byte { + var out [16]byte + if ip == nil { + return out + } + copy(out[:], ip.To16()) + return out +} + +// ipStringFromCfg renders an IP for logging; returns "unset" if nil. +func ipStringFromCfg(ip net.IP) string { + if ip == nil { + return "unset" + } + return ip.String() +} diff --git a/internal/vpp/lbstate.go b/internal/vpp/lbstate.go new file mode 100644 index 0000000..bff9696 --- /dev/null +++ b/internal/vpp/lbstate.go @@ -0,0 +1,267 @@ +// Copyright (c) 2026, Pim van Pelt + +package vpp + +import ( + "fmt" + "net" + "time" + + lb "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/lb" + lb_types "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/lb_types" +) + +// LBConf mirrors VPP's lb_conf_get_reply: global LB plugin settings. +type LBConf struct { + IP4SrcAddress net.IP + IP6SrcAddress net.IP + StickyBucketsPerCore uint32 + FlowTimeout uint32 +} + +// LBVIP mirrors VPP's lb_vip_details plus the set of application servers +// attached to this VIP (from lb_as_v2_details). +type LBVIP struct { + Prefix *net.IPNet // VIP address + prefix length + Protocol uint8 // IP proto (6=TCP, 17=UDP, 255=any) + Port uint16 // 0 = all-port VIP + Encap string // gre4|gre6|l3dsr|nat4|nat6 + SrvType string // clusterip|nodeport + Dscp uint8 + TargetPort uint16 + FlowTableLength uint16 + ASes []LBAS +} + +// LBAS mirrors VPP's lb_as_v2_details: one application server bound to a VIP. +type LBAS struct { + Address net.IP + Weight uint8 + Flags uint8 // bit 0 = used (alive), bit 1 = flushed + NumBuckets uint32 + InUseSince time.Time // from VPP seconds-since-epoch (0 = never) +} + +// LBState is a snapshot of the VPP LB plugin state. +type LBState struct { + Conf LBConf + VIPs []LBVIP +} + +// GetLBStateAll fetches a full snapshot of the LB plugin state (global config +// plus every VIP and its application servers). +// Returns an error if VPP is not connected. +func (c *Client) GetLBStateAll() (*LBState, error) { + ch, err := c.apiChannel() + if err != nil { + return nil, err + } + defer ch.Close() + + state := &LBState{} + + conf, err := getLBConf(ch) + if err != nil { + return nil, err + } + state.Conf = conf + + vips, err := dumpAllVIPs(ch) + if err != nil { + return nil, err + } + for i := range vips { + ases, err := dumpASesForVIP(ch, vips[i].Protocol, vips[i].Port) + if err != nil { + return nil, err + } + vips[i].ASes = ases + } + state.VIPs = vips + return state, nil +} + +// GetLBStateVIP fetches a single VIP from VPP. Returns (nil, nil) if the VIP +// does not exist in VPP (caller must treat absence as "needs to be added"). +// Returns an error only on transport/VPP failures. +func (c *Client) GetLBStateVIP(prefix *net.IPNet, protocol uint8, port uint16) (*LBVIP, error) { + ch, err := c.apiChannel() + if err != nil { + return nil, err + } + defer ch.Close() + return lookupVIP(ch, prefix, protocol, port) +} + +// ---- low-level helpers (used by both Get and Sync paths) ------------------- + +func getLBConf(ch *loggedChannel) (LBConf, error) { + reply := &lb.LbConfGetReply{} + if err := ch.SendRequest(&lb.LbConfGet{}).ReceiveReply(reply); err != nil { + return LBConf{}, fmt.Errorf("lb_conf_get: %w", err) + } + return LBConf{ + IP4SrcAddress: ip4ToNetIP(reply.IP4SrcAddress), + IP6SrcAddress: ip6ToNetIP(reply.IP6SrcAddress), + StickyBucketsPerCore: reply.StickyBucketsPerCore, + FlowTimeout: reply.FlowTimeout, + }, nil +} + +// dumpAllVIPs returns every VIP known to VPP (metadata only — ASes not populated). +func dumpAllVIPs(ch *loggedChannel) ([]LBVIP, error) { + reqCtx := ch.SendMultiRequest(&lb.LbVipDump{}) + var out []LBVIP + for { + reply := &lb.LbVipDetails{} + stop, err := reqCtx.ReceiveReply(reply) + if err != nil { + return nil, fmt.Errorf("lb_vip_dump: %w", err) + } + if stop { + break + } + out = append(out, vipFromDetails(reply)) + } + return out, nil +} + +// lookupVIP finds a single VIP by (prefix, protocol, port) and returns it +// populated with its application servers, or nil if the VIP does not exist. +func lookupVIP(ch *loggedChannel, prefix *net.IPNet, protocol uint8, port uint16) (*LBVIP, error) { + all, err := dumpAllVIPs(ch) + if err != nil { + return nil, err + } + want := prefix.String() + for i := range all { + if all[i].Prefix.String() != want { + continue + } + if all[i].Protocol != protocol || all[i].Port != port { + continue + } + ases, err := dumpASesForVIP(ch, protocol, port) + if err != nil { + return nil, err + } + all[i].ASes = ases + return &all[i], nil + } + return nil, nil +} + +// dumpASesForVIP returns the application servers bound to the VIP identified +// by (protocol, port). VPP's lb_as_v2_dump filter is used; we also guard +// defensively against replies for other VIPs. +func dumpASesForVIP(ch *loggedChannel, protocol uint8, port uint16) ([]LBAS, error) { + req := &lb.LbAsV2Dump{ + Protocol: protocol, + Port: port, + } + reqCtx := ch.SendMultiRequest(req) + var out []LBAS + for { + reply := &lb.LbAsV2Details{} + stop, err := reqCtx.ReceiveReply(reply) + if err != nil { + return nil, fmt.Errorf("lb_as_v2_dump: %w", err) + } + if stop { + break + } + if reply.Vip.Port != port || uint8(reply.Vip.Protocol) != protocol { + continue + } + var inUse time.Time + if reply.InUseSince != 0 { + inUse = time.Unix(int64(reply.InUseSince), 0) + } + out = append(out, LBAS{ + Address: reply.AppSrv.ToIP(), + Weight: reply.Weight, + Flags: reply.Flags, + NumBuckets: reply.NumBuckets, + InUseSince: inUse, + }) + } + return out, nil +} + +// vipFromDetails builds an LBVIP (without ASes) from a VPP lb_vip_details reply. +func vipFromDetails(reply *lb.LbVipDetails) LBVIP { + return LBVIP{ + Prefix: lbVipPrefix(reply.Vip), + Protocol: uint8(reply.Vip.Protocol), + Port: reply.Vip.Port, + Encap: encapString(reply.Encap), + SrvType: srvTypeString(reply.SrvType), + Dscp: uint8(reply.Dscp), + TargetPort: reply.TargetPort, + FlowTableLength: reply.FlowTableLength, + } +} + +// lbVipPrefix converts a VPP lb_vip's address+prefix to a *net.IPNet. +func lbVipPrefix(v lb_types.LbVip) *net.IPNet { + ip := v.Pfx.Address.ToIP() + bits := 32 + if ip.To4() == nil { + bits = 128 + } + return &net.IPNet{ + IP: ip, + Mask: net.CIDRMask(int(v.Pfx.Len), bits), + } +} + +func ip4ToNetIP(a [4]byte) net.IP { + // VPP reports 255.255.255.255 when no IPv4 src is configured. + if a == [4]byte{0xff, 0xff, 0xff, 0xff} { + return nil + } + return net.IPv4(a[0], a[1], a[2], a[3]).To4() +} + +func ip6ToNetIP(a [16]byte) net.IP { + // VPP reports all-ones when no IPv6 src is configured. + allOnes := true + for _, b := range a { + if b != 0xff { + allOnes = false + break + } + } + if allOnes { + return nil + } + ip := make(net.IP, 16) + copy(ip, a[:]) + return ip +} + +func encapString(e lb_types.LbEncapType) string { + switch e { + case lb_types.LB_API_ENCAP_TYPE_GRE4: + return "gre4" + case lb_types.LB_API_ENCAP_TYPE_GRE6: + return "gre6" + case lb_types.LB_API_ENCAP_TYPE_L3DSR: + return "l3dsr" + case lb_types.LB_API_ENCAP_TYPE_NAT4: + return "nat4" + case lb_types.LB_API_ENCAP_TYPE_NAT6: + return "nat6" + } + return fmt.Sprintf("unknown(%d)", e) +} + +func srvTypeString(t lb_types.LbSrvType) string { + switch t { + case lb_types.LB_API_SRV_TYPE_CLUSTERIP: + return "clusterip" + case lb_types.LB_API_SRV_TYPE_NODEPORT: + return "nodeport" + } + return fmt.Sprintf("unknown(%d)", t) +} diff --git a/internal/vpp/lbsync.go b/internal/vpp/lbsync.go new file mode 100644 index 0000000..b05fdbd --- /dev/null +++ b/internal/vpp/lbsync.go @@ -0,0 +1,477 @@ +// Copyright (c) 2026, Pim van Pelt + +package vpp + +import ( + "errors" + "fmt" + "log/slog" + "net" + + "git.ipng.ch/ipng/vpp-maglev/internal/config" + ip_types "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/ip_types" + lb "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/lb" + lb_types "git.ipng.ch/ipng/vpp-maglev/internal/vpp/binapi/lb_types" +) + +// ErrFrontendNotFound is returned by SyncLBStateVIP when the caller asks for +// a frontend name that does not exist in the config. +var ErrFrontendNotFound = errors.New("frontend not found in config") + +// vipKey uniquely identifies a VPP LB VIP by its prefix, protocol, and port. +type vipKey struct { + prefix string // canonical CIDR form + protocol uint8 + port uint16 +} + +// desiredVIP is the sync's view of one VIP derived from the maglev config. +type desiredVIP struct { + Prefix *net.IPNet + Protocol uint8 // 6=TCP, 17=UDP, 255=any + Port uint16 + ASes map[string]desiredAS // keyed by AS IP string +} + +// desiredAS is one application server to be installed under a VIP. +type desiredAS struct { + Address net.IP + Weight uint8 // 0-100 +} + +// syncStats counts changes made to the dataplane during a sync run. +type syncStats struct { + vipAdd int + vipDel int + asAdd int + asDel int + asWeight int +} + +// SyncLBStateAll reconciles the full VPP load-balancer state with the given +// config. For every frontend in cfg: +// - if the VIP does not exist in VPP, create it; +// - for every pool backend, add the application server if missing, or +// update its weight if different. +// +// VIPs and ASes present in VPP but absent from the config are removed. +// Returns an error if any VPP API call fails. +func (c *Client) SyncLBStateAll(cfg *config.Config) error { + if !c.IsConnected() { + return errNotConnected + } + + cur, err := c.GetLBStateAll() + if err != nil { + return fmt.Errorf("read VPP LB state: %w", err) + } + desired := desiredFromConfig(cfg) + + ch, err := c.apiChannel() + if err != nil { + return err + } + defer ch.Close() + + slog.Info("vpp-lbsync-start", + "scope", "all", + "vips-desired", len(desired), + "vips-current", len(cur.VIPs)) + + // Index both sides by (prefix, protocol, port). + curByKey := make(map[vipKey]LBVIP, len(cur.VIPs)) + for _, v := range cur.VIPs { + curByKey[makeVIPKey(v.Prefix, v.Protocol, v.Port)] = v + } + desByKey := make(map[vipKey]desiredVIP, len(desired)) + for _, d := range desired { + desByKey[makeVIPKey(d.Prefix, d.Protocol, d.Port)] = d + } + + var st syncStats + + // ---- pass 1: remove VIPs that are in VPP but not in config ---- + for k, v := range curByKey { + if _, keep := desByKey[k]; keep { + continue + } + if err := removeVIP(ch, v, &st); err != nil { + return err + } + } + + // ---- pass 2: add/update VIPs that are in config ---- + for k, d := range desByKey { + cur, existing := curByKey[k] + var curPtr *LBVIP + if existing { + curPtr = &cur + } + if err := reconcileVIP(ch, d, curPtr, &st); err != nil { + return err + } + } + + slog.Info("vpp-lbsync-done", + "scope", "all", + "vip-added", st.vipAdd, + "vip-removed", st.vipDel, + "as-added", st.asAdd, + "as-removed", st.asDel, + "as-weight-updated", st.asWeight) + return nil +} + +// SyncLBStateVIP reconciles a single VIP (identified by frontend name) with +// the given config. Unlike SyncLBStateAll, it never removes VIPs: if the +// frontend is missing from cfg, SyncLBStateVIP returns ErrFrontendNotFound. +// This is the right tool for targeted updates on a busy load-balancer with +// many VIPs — only one VIP is read from VPP and only its ASes are modified. +func (c *Client) SyncLBStateVIP(cfg *config.Config, feName string) error { + if !c.IsConnected() { + return errNotConnected + } + fe, ok := cfg.Frontends[feName] + if !ok { + return fmt.Errorf("%q: %w", feName, ErrFrontendNotFound) + } + d := desiredFromFrontend(cfg, fe) + + cur, err := c.GetLBStateVIP(d.Prefix, d.Protocol, d.Port) + if err != nil { + return fmt.Errorf("read VPP VIP state: %w", err) + } + + ch, err := c.apiChannel() + if err != nil { + return err + } + defer ch.Close() + + slog.Info("vpp-lbsync-start", + "scope", "vip", + "frontend", feName, + "prefix", d.Prefix.String(), + "protocol", protocolName(d.Protocol), + "port", d.Port) + + var st syncStats + if err := reconcileVIP(ch, d, cur, &st); err != nil { + return err + } + slog.Info("vpp-lbsync-done", + "scope", "vip", + "frontend", feName, + "vip-added", st.vipAdd, + "as-added", st.asAdd, + "as-removed", st.asDel, + "as-weight-updated", st.asWeight) + return nil +} + +// reconcileVIP brings one VIP's state in VPP into alignment with the desired +// state. If cur is nil the VIP is added from scratch; otherwise ASes are +// added, removed, and reweighted individually. Stats are accumulated into st. +func reconcileVIP(ch *loggedChannel, d desiredVIP, cur *LBVIP, st *syncStats) error { + if cur == nil { + if err := addVIP(ch, d); err != nil { + return err + } + st.vipAdd++ + for _, as := range d.ASes { + if err := addAS(ch, d.Prefix, d.Protocol, d.Port, as); err != nil { + return err + } + st.asAdd++ + } + return nil + } + + // VIP exists in both — reconcile ASes. + curASes := make(map[string]LBAS, len(cur.ASes)) + for _, a := range cur.ASes { + curASes[a.Address.String()] = a + } + + // Remove ASes that are in VPP but not desired. + for addr, a := range curASes { + if _, keep := d.ASes[addr]; keep { + continue + } + if err := delAS(ch, cur.Prefix, cur.Protocol, cur.Port, a.Address); err != nil { + return err + } + st.asDel++ + } + + // Add new ASes, update weights on existing ones. + for addr, a := range d.ASes { + c, hit := curASes[addr] + if !hit { + if err := addAS(ch, d.Prefix, d.Protocol, d.Port, a); err != nil { + return err + } + st.asAdd++ + continue + } + if c.Weight != a.Weight { + if err := setASWeight(ch, d.Prefix, d.Protocol, d.Port, a); err != nil { + return err + } + st.asWeight++ + } + } + return nil +} + +// removeVIP flushes all ASes from a VIP and then deletes the VIP itself. +func removeVIP(ch *loggedChannel, v LBVIP, st *syncStats) error { + for _, as := range v.ASes { + if err := delAS(ch, v.Prefix, v.Protocol, v.Port, as.Address); err != nil { + return err + } + st.asDel++ + } + if err := delVIP(ch, v.Prefix, v.Protocol, v.Port); err != nil { + return err + } + st.vipDel++ + return nil +} + +// desiredFromConfig flattens every frontend in cfg into a desired VIP set. +func desiredFromConfig(cfg *config.Config) []desiredVIP { + out := make([]desiredVIP, 0, len(cfg.Frontends)) + for _, fe := range cfg.Frontends { + out = append(out, desiredFromFrontend(cfg, fe)) + } + return out +} + +// desiredFromFrontend builds the desired VIP for a single frontend. +// +// All backends across all pools of a frontend are merged into a single +// application-server list so VPP knows about every backend that could ever +// receive traffic. Weights are assigned as follows: +// +// - primary (first) pool: the backend's configured weight +// - any subsequent pool: weight 0 (backend is known but receives no traffic) +// +// This preserves the pool priority model: higher layers can later flip +// secondary-pool backends to non-zero weights on failover without needing to +// add/remove ASes in the dataplane. When the same backend appears in multiple +// pools, the first pool it appears in wins. +func desiredFromFrontend(cfg *config.Config, fe config.Frontend) desiredVIP { + bits := 32 + if fe.Address.To4() == nil { + bits = 128 + } + d := desiredVIP{ + Prefix: &net.IPNet{IP: fe.Address, Mask: net.CIDRMask(bits, bits)}, + Protocol: protocolFromConfig(fe.Protocol), + Port: fe.Port, + ASes: make(map[string]desiredAS), + } + for poolIdx, pool := range fe.Pools { + for bName, pb := range pool.Backends { + b, ok := cfg.Backends[bName] + if !ok || !b.Enabled || b.Address == nil { + continue + } + addr := b.Address.String() + if _, already := d.ASes[addr]; already { + continue + } + var w uint8 + if poolIdx == 0 { + w = clampWeight(pb.Weight) + } // secondary pools: weight 0 (default) + d.ASes[addr] = desiredAS{Address: b.Address, Weight: w} + } + } + return d +} + +// ---- API call helpers ------------------------------------------------------ + +// defaultFlowsTableLength is sent as NewFlowsTableLength in lb_add_del_vip_v2. +// The .api file declares default=1024 but that default is only applied by VAT/ +// the CLI parser, not when a raw message is marshalled over the socket. If we +// send 0, the plugin's vec_validate explodes (OOM / panic). Must be a power of +// two — 1024 matches the default that would have been applied via CLI. +const defaultFlowsTableLength = 1024 + +func addVIP(ch *loggedChannel, d desiredVIP) error { + encap := encapForIP(d.Prefix.IP) + req := &lb.LbAddDelVipV2{ + Pfx: ip_types.NewAddressWithPrefix(*d.Prefix), + Protocol: d.Protocol, + Port: d.Port, + Encap: encap, + Type: lb_types.LB_API_SRV_TYPE_CLUSTERIP, + NewFlowsTableLength: defaultFlowsTableLength, + IsDel: false, + } + reply := &lb.LbAddDelVipV2Reply{} + if err := ch.SendRequest(req).ReceiveReply(reply); err != nil { + return fmt.Errorf("lb_add_del_vip_v2 add %s: %w", d.Prefix, err) + } + if reply.Retval != 0 { + return fmt.Errorf("lb_add_del_vip_v2 add %s: retval=%d", d.Prefix, reply.Retval) + } + slog.Debug("vpp-lbsync-vip-add", + "prefix", d.Prefix.String(), + "protocol", protocolName(d.Protocol), + "port", d.Port, + "encap", encapName(encap)) + return nil +} + +func delVIP(ch *loggedChannel, prefix *net.IPNet, protocol uint8, port uint16) error { + req := &lb.LbAddDelVipV2{ + Pfx: ip_types.NewAddressWithPrefix(*prefix), + Protocol: protocol, + Port: port, + IsDel: true, + } + reply := &lb.LbAddDelVipV2Reply{} + if err := ch.SendRequest(req).ReceiveReply(reply); err != nil { + return fmt.Errorf("lb_add_del_vip_v2 del %s: %w", prefix, err) + } + if reply.Retval != 0 { + return fmt.Errorf("lb_add_del_vip_v2 del %s: retval=%d", prefix, reply.Retval) + } + slog.Debug("vpp-lbsync-vip-del", + "prefix", prefix.String(), + "protocol", protocolName(protocol), + "port", port) + return nil +} + +func addAS(ch *loggedChannel, prefix *net.IPNet, protocol uint8, port uint16, a desiredAS) error { + req := &lb.LbAddDelAsV2{ + Pfx: ip_types.NewAddressWithPrefix(*prefix), + Protocol: protocol, + Port: port, + AsAddress: ip_types.NewAddress(a.Address), + Weight: a.Weight, + IsDel: false, + } + reply := &lb.LbAddDelAsV2Reply{} + if err := ch.SendRequest(req).ReceiveReply(reply); err != nil { + return fmt.Errorf("lb_add_del_as_v2 add %s@%s: %w", a.Address, prefix, err) + } + if reply.Retval != 0 { + return fmt.Errorf("lb_add_del_as_v2 add %s@%s: retval=%d", a.Address, prefix, reply.Retval) + } + slog.Debug("vpp-lbsync-as-add", + "vip", prefix.String(), + "protocol", protocolName(protocol), + "port", port, + "address", a.Address.String(), + "weight", a.Weight) + return nil +} + +func delAS(ch *loggedChannel, prefix *net.IPNet, protocol uint8, port uint16, addr net.IP) error { + req := &lb.LbAddDelAsV2{ + Pfx: ip_types.NewAddressWithPrefix(*prefix), + Protocol: protocol, + Port: port, + AsAddress: ip_types.NewAddress(addr), + IsDel: true, + IsFlush: true, + } + reply := &lb.LbAddDelAsV2Reply{} + if err := ch.SendRequest(req).ReceiveReply(reply); err != nil { + return fmt.Errorf("lb_add_del_as_v2 del %s@%s: %w", addr, prefix, err) + } + if reply.Retval != 0 { + return fmt.Errorf("lb_add_del_as_v2 del %s@%s: retval=%d", addr, prefix, reply.Retval) + } + slog.Debug("vpp-lbsync-as-del", + "vip", prefix.String(), + "protocol", protocolName(protocol), + "port", port, + "address", addr.String()) + return nil +} + +func setASWeight(ch *loggedChannel, prefix *net.IPNet, protocol uint8, port uint16, a desiredAS) error { + req := &lb.LbAsSetWeight{ + Pfx: ip_types.NewAddressWithPrefix(*prefix), + Protocol: protocol, + Port: port, + AsAddress: ip_types.NewAddress(a.Address), + Weight: a.Weight, + } + reply := &lb.LbAsSetWeightReply{} + if err := ch.SendRequest(req).ReceiveReply(reply); err != nil { + return fmt.Errorf("lb_as_set_weight %s@%s: %w", a.Address, prefix, err) + } + if reply.Retval != 0 { + return fmt.Errorf("lb_as_set_weight %s@%s: retval=%d", a.Address, prefix, reply.Retval) + } + slog.Debug("vpp-lbsync-as-weight", + "vip", prefix.String(), + "protocol", protocolName(protocol), + "port", port, + "address", a.Address.String(), + "weight", a.Weight) + return nil +} + +// ---- utility --------------------------------------------------------------- + +func makeVIPKey(prefix *net.IPNet, protocol uint8, port uint16) vipKey { + return vipKey{prefix: prefix.String(), protocol: protocol, port: port} +} + +func protocolFromConfig(s string) uint8 { + switch s { + case "tcp": + return 6 + case "udp": + return 17 + } + return 255 // any +} + +func protocolName(p uint8) string { + switch p { + case 6: + return "tcp" + case 17: + return "udp" + case 255: + return "any" + } + return fmt.Sprintf("%d", p) +} + +func encapForIP(ip net.IP) lb_types.LbEncapType { + if ip.To4() != nil { + return lb_types.LB_API_ENCAP_TYPE_GRE4 + } + return lb_types.LB_API_ENCAP_TYPE_GRE6 +} + +func encapName(e lb_types.LbEncapType) string { + switch e { + case lb_types.LB_API_ENCAP_TYPE_GRE4: + return "gre4" + case lb_types.LB_API_ENCAP_TYPE_GRE6: + return "gre6" + } + return fmt.Sprintf("%d", e) +} + +func clampWeight(w int) uint8 { + if w < 0 { + return 0 + } + if w > 100 { + return 100 + } + return uint8(w) +} diff --git a/proto/maglev.proto b/proto/maglev.proto index 16ce6f1..1cbfc82 100644 --- a/proto/maglev.proto +++ b/proto/maglev.proto @@ -21,6 +21,8 @@ service Maglev { rpc CheckConfig(CheckConfigRequest) returns (CheckConfigResponse); rpc ReloadConfig(ReloadConfigRequest) returns (ReloadConfigResponse); rpc GetVPPInfo(GetVPPInfoRequest) returns (VPPInfo); + rpc GetVPPLBState(GetVPPLBStateRequest) returns (VPPLBState); + rpc SyncVPPLBState(SyncVPPLBStateRequest) returns (SyncVPPLBStateResponse); } // ---- requests --------------------------------------------------------------- @@ -75,6 +77,55 @@ message VPPInfo { int64 connecttime_ns = 6; // unix timestamp (ns) when maglevd connected to VPP } +// ---- VPP load-balancer state ------------------------------------------------ + +message GetVPPLBStateRequest {} + +// VPPLBConf mirrors VPP's lb_conf_get_reply: global LB plugin settings. +message VPPLBConf { + string ip4_src_address = 1; + string ip6_src_address = 2; + uint32 sticky_buckets_per_core = 3; + uint32 flow_timeout = 4; +} + +// VPPLBAS is one application server attached to a VIP. +message VPPLBAS { + string address = 1; + uint32 weight = 2; // 0-100 + uint32 flags = 3; // VPP AS flags (bit 0 = used, bit 1 = flushed) + uint32 num_buckets = 4; + int64 in_use_since_ns = 5; // unix timestamp (ns), 0 if never used +} + +// VPPLBVIP mirrors VPP's lb_vip_details plus the attached application servers. +// Note: srv_type, dscp, and target_port are intentionally omitted — maglevd +// only supports GRE encap, so NAT/L3DSR-specific fields don't apply. +message VPPLBVIP { + string prefix = 1; // CIDR, e.g. 192.0.2.1/32 + uint32 protocol = 2; // 6=TCP, 17=UDP, 255=any + uint32 port = 3; // 0 = all-port VIP + string encap = 4; // gre4|gre6|l3dsr|nat4|nat6 + uint32 flow_table_length = 5; + repeated VPPLBAS application_servers = 6; +} + +message VPPLBState { + VPPLBConf conf = 1; + repeated VPPLBVIP vips = 2; +} + +// SyncVPPLBStateRequest triggers a reconciliation between the maglev config +// and the VPP load-balancer dataplane. When frontend_name is set, only that +// frontend's VIP is synced (SyncLBStateVIP) and no VIPs are removed. When +// unset, a full reconciliation runs (SyncLBStateAll), which will also remove +// stale VIPs from VPP. +message SyncVPPLBStateRequest { + optional string frontend_name = 1; +} + +message SyncVPPLBStateResponse {} + message SetWeightRequest { string frontend = 1; string pool = 2; diff --git a/tests/01-maglevd/maglevd-lab/maglev.yaml b/tests/01-maglevd/maglevd-lab/maglev.yaml index 42e5ad7..baea876 100644 --- a/tests/01-maglevd/maglevd-lab/maglev.yaml +++ b/tests/01-maglevd/maglevd-lab/maglev.yaml @@ -2,6 +2,11 @@ maglev: healthchecker: transition-history: 5 + vpp: + lb: + ipv4-src-address: 10.0.0.1 + ipv6-src-address: 2001:db8::1 + healthchecks: http-check: type: http